@0disoft/laqu
Advanced tools
+5
-5
@@ -1,2 +0,2 @@ | ||
| import type { TaskSnapshot, TaskStatus } from "./task-store.js"; | ||
| import type { TaskSnapshot, TaskSummaryCounts, TaskStatus } from "./task-store.js"; | ||
| export declare const LAQU_EVENT_SCHEMA = "laqu.event"; | ||
@@ -16,7 +16,7 @@ export declare const LAQU_EVENT_SCHEMA_VERSION = 1; | ||
| readonly title: string; | ||
| readonly parentId: string | undefined; | ||
| readonly parentId?: string; | ||
| readonly status: TaskStatus; | ||
| readonly progress: LaquEventProgress; | ||
| readonly message: string | undefined; | ||
| readonly detail: string | undefined; | ||
| readonly message?: string; | ||
| readonly detail?: string; | ||
| readonly depth: number; | ||
@@ -51,3 +51,3 @@ }; | ||
| export declare function logEvent(message: string, createdAt: number): LaquLogEvent; | ||
| export declare function summaryEvent(tasks: readonly TaskSnapshot[], createdAt: number): LaquSummaryEvent; | ||
| export declare function summaryEvent(counts: TaskSummaryCounts, createdAt: number): LaquSummaryEvent; | ||
| //# sourceMappingURL=events.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEnF,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAC9C,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,YAAY,GAAG,gBAAgB,CAAC;AAExE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,iBAAiB,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,OAAO,yBAAyB,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;QACtC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;QAC5B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;QACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;QACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;QACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,YAAa,SAAQ,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/D,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC;IACnE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAElF,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,aAAa,CAiB3D;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAQzE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAgBhG"} | ||
| {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,YAAY,EACZ,iBAAiB,EACjB,UAAU,EACX,MAAM,iBAAiB,CAAC;AAEzB,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAC9C,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,YAAY,GAAG,gBAAgB,CAAC;AAExE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,iBAAiB,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,OAAO,yBAAyB,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;QAC5B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;QACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,YAAa,SAAQ,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/D,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC;IACnE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAElF,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,aAAa,CAiB3D;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAQzE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAe3F"} |
+10
-22
@@ -12,8 +12,8 @@ export const LAQU_EVENT_SCHEMA = "laqu.event"; | ||
| title: task.title, | ||
| parentId: task.parentId, | ||
| status: task.status, | ||
| progress: eventProgress(task.aggregate), | ||
| message: task.message, | ||
| detail: task.detail, | ||
| depth: task.depth, | ||
| ...(task.parentId === undefined ? {} : { parentId: task.parentId }), | ||
| ...(task.message === undefined ? {} : { message: task.message }), | ||
| ...(task.detail === undefined ? {} : { detail: task.detail }), | ||
| }, | ||
@@ -31,4 +31,3 @@ }; | ||
| } | ||
| export function summaryEvent(tasks, createdAt) { | ||
| const flatTasks = flattenTasks(tasks); | ||
| export function summaryEvent(counts, createdAt) { | ||
| return { | ||
@@ -40,8 +39,8 @@ schema: LAQU_EVENT_SCHEMA, | ||
| tasks: { | ||
| total: flatTasks.length, | ||
| running: countByStatus(flatTasks, "running"), | ||
| succeeded: countByStatus(flatTasks, "succeeded"), | ||
| failed: countByStatus(flatTasks, "failed"), | ||
| cancelled: countByStatus(flatTasks, "cancelled"), | ||
| skipped: countByStatus(flatTasks, "skipped"), | ||
| total: counts.total, | ||
| running: counts.running, | ||
| succeeded: counts.succeeded, | ||
| failed: counts.failed, | ||
| cancelled: counts.cancelled, | ||
| skipped: counts.skipped, | ||
| }, | ||
@@ -60,12 +59,1 @@ }; | ||
| } | ||
| function flattenTasks(tasks) { | ||
| const flatTasks = []; | ||
| for (const task of tasks) { | ||
| flatTasks.push(task); | ||
| flatTasks.push(...flattenTasks(task.children)); | ||
| } | ||
| return flatTasks; | ||
| } | ||
| function countByStatus(tasks, status) { | ||
| return tasks.filter((task) => task.status === status).length; | ||
| } |
+1
-1
@@ -5,3 +5,3 @@ export { createLaqu, createProgressRuntime } from "./runtime.js"; | ||
| export { LAQU_EVENT_SCHEMA, LAQU_EVENT_SCHEMA_VERSION, type LaquEvent, type LaquEventBase, type LaquEventProgress, type LaquLogEvent, type LaquSummaryEvent, type LaquTaskEvent, } from "./events.js"; | ||
| export type { ChannelRole, OutputFormat, ProgressPolicy, ProgressRuntime, RuntimeEnvironment, RuntimeOptions, StreamCapability, StreamTarget, TaskHandle, TaskOptions, ThemeInput, ThemeTokens, } from "./types.js"; | ||
| export type { ChannelRole, OutputFormat, ProgressPolicy, ProgressRuntime, RuntimeEnvironment, RuntimeOptions, RuntimeRetentionOptions, StreamCapability, StreamTarget, TaskHandle, TaskOptions, ThemeInput, ThemeTokens, } from "./types.js"; | ||
| //# sourceMappingURL=index.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,IAAI,EACJ,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,SAAS,EACT,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,KAAK,SAAS,EACd,KAAK,YAAY,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,WAAW,EACX,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAC"} | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,IAAI,EACJ,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,SAAS,EACT,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,KAAK,SAAS,EACd,KAAK,YAAY,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,WAAW,EACX,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAC"} |
@@ -1,2 +0,2 @@ | ||
| import type { Frame, Renderer } from "./renderer.js"; | ||
| import type { Frame, JsonSerialization, Renderer } from "./renderer.js"; | ||
| import type { RuntimeSnapshot } from "./task-store.js"; | ||
@@ -20,6 +20,9 @@ import type { StreamTarget } from "./types.js"; | ||
| private readonly live; | ||
| private readonly jsonSerialization; | ||
| private readonly backpressureTimeoutMs; | ||
| readonly lease: TerminalLease; | ||
| constructor(target: StreamTarget, renderer: Renderer, live: boolean); | ||
| constructor(target: StreamTarget, renderer: Renderer, live: boolean, jsonSerialization?: JsonSerialization, backpressureTimeoutMs?: number); | ||
| render(snapshot: RuntimeSnapshot): void; | ||
| writeFrame(frame: Frame): void; | ||
| finalize(snapshot: RuntimeSnapshot): void; | ||
| flush(): Promise<void>; | ||
@@ -26,0 +29,0 @@ close(): Promise<void>; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"output-coordinator.d.ts","sourceRoot":"","sources":["../src/output-coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,qBAAa,aAAa;IACxB,MAAM,UAAS;IACf,iBAAiB,SAAK;IACtB,gBAAgB,SAAK;IACrB,kBAAkB,UAAS;IAC3B,UAAU,SAAK;IACf,WAAW,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,SAAS,CAAC;IACvD,YAAY,EAAE,KAAK,GAAG,SAAS,CAAC;IAChC,oBAAoB,UAAS;IAC7B,aAAa,EAAE,SAAS,MAAM,EAAE,CAAM;CACvC;AAED,qBAAa,iBAAiB;;IAM1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPvB,QAAQ,CAAC,KAAK,gBAAuB;gBAKlB,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,OAAO;IAGhC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAOvC,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAWxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IActB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA2F7B"} | ||
| {"version":3,"file":"output-coordinator.d.ts","sourceRoot":"","sources":["../src/output-coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/C,qBAAa,aAAa;IACxB,MAAM,UAAS;IACf,iBAAiB,SAAK;IACtB,gBAAgB,SAAK;IACrB,kBAAkB,UAAS;IAC3B,UAAU,SAAK;IACf,WAAW,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,SAAS,CAAC;IACvD,YAAY,EAAE,KAAK,GAAG,SAAS,CAAC;IAChC,oBAAoB,UAAS;IAC7B,aAAa,EAAE,SAAS,MAAM,EAAE,CAAM;CACvC;AAED,qBAAa,iBAAiB;;IAS1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,qBAAqB;IAZxC,QAAQ,CAAC,KAAK,gBAAuB;gBAQlB,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,OAAO,EACb,iBAAiB,GAAE,iBAA0B,EAC7C,qBAAqB,SAAQ;IAGhD,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAOvC,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAW9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAOnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IActB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA4I7B"} |
@@ -16,9 +16,16 @@ export class TerminalLease { | ||
| live; | ||
| jsonSerialization; | ||
| backpressureTimeoutMs; | ||
| lease = new TerminalLease(); | ||
| #waitingForDrain = false; | ||
| #drainPromise; | ||
| constructor(target, renderer, live) { | ||
| #drainTimer; | ||
| #jsonArrayStarted = false; | ||
| #jsonArrayNeedsComma = false; | ||
| constructor(target, renderer, live, jsonSerialization = "none", backpressureTimeoutMs = 1_000) { | ||
| this.target = target; | ||
| this.renderer = renderer; | ||
| this.live = live; | ||
| this.jsonSerialization = jsonSerialization; | ||
| this.backpressureTimeoutMs = backpressureTimeoutMs; | ||
| } | ||
@@ -36,3 +43,3 @@ render(snapshot) { | ||
| if (this.#waitingForDrain) { | ||
| this.lease.pendingFrame = frame; | ||
| this.lease.pendingFrame = mergePendingFrame(this.lease.pendingFrame, frame); | ||
| return; | ||
@@ -42,2 +49,8 @@ } | ||
| } | ||
| finalize(snapshot) { | ||
| if (this.lease.closed) { | ||
| return; | ||
| } | ||
| this.writeFrame(this.renderer.finalize?.(snapshot) ?? { kind: "none" }); | ||
| } | ||
| async flush() { | ||
@@ -61,2 +74,6 @@ if (this.#drainPromise !== undefined) { | ||
| await this.flush(); | ||
| if (this.jsonSerialization === "array") { | ||
| this.#writeRaw(this.#jsonArrayStarted ? "]\n" : "[]\n"); | ||
| await this.flush(); | ||
| } | ||
| if (this.live) { | ||
@@ -78,2 +95,4 @@ const cursor = this.#showCursor(); | ||
| this.lease.lastLiveLines = []; | ||
| this.#jsonArrayStarted = false; | ||
| this.#jsonArrayNeedsComma = false; | ||
| } | ||
@@ -83,3 +102,3 @@ #writeNow(frame) { | ||
| case "live": | ||
| this.#writeLive(frame.lines); | ||
| this.#writeLive(frame.scrollbackLines, frame.lines); | ||
| return; | ||
@@ -90,3 +109,3 @@ case "plain": | ||
| case "json": | ||
| this.#writeRaw(`${frame.events.map((event) => JSON.stringify(event)).join("\n")}\n`); | ||
| this.#writeJson(frame.events); | ||
| return; | ||
@@ -97,9 +116,30 @@ case "none": | ||
| } | ||
| #writeLive(lines) { | ||
| if (sameLines(lines, this.lease.lastLiveLines)) { | ||
| #writeJson(events) { | ||
| if (events.length === 0) { | ||
| return; | ||
| } | ||
| if (this.jsonSerialization === "array") { | ||
| let chunk = ""; | ||
| if (!this.#jsonArrayStarted) { | ||
| chunk += "["; | ||
| this.#jsonArrayStarted = true; | ||
| } | ||
| for (const event of events) { | ||
| chunk += `${this.#jsonArrayNeedsComma ? "," : ""}${JSON.stringify(event)}`; | ||
| this.#jsonArrayNeedsComma = true; | ||
| } | ||
| this.#writeRaw(chunk); | ||
| return; | ||
| } | ||
| this.#writeRaw(`${events.map((event) => JSON.stringify(event)).join("\n")}\n`); | ||
| } | ||
| #writeLive(scrollbackLines, lines) { | ||
| if (scrollbackLines.length === 0 && sameLines(lines, this.lease.lastLiveLines)) { | ||
| return; | ||
| } | ||
| const cursor = this.#hideCursor(); | ||
| const clear = this.lease.renderedLineCount > 0 ? eraseLines(this.lease.renderedLineCount) : ""; | ||
| const chunk = `${cursor}${clear}${lines.join("\n")}\u001b[0m`; | ||
| const scrollback = scrollbackLines.length > 0 ? `${scrollbackLines.join("\n")}\n` : ""; | ||
| const liveLines = lines.length > 0 ? lines.join("\n") : ""; | ||
| const chunk = `${cursor}${clear}${scrollback}${liveLines}\u001b[0m`; | ||
| this.lease.renderedLineCount = lines.length; | ||
@@ -129,9 +169,22 @@ this.lease.partialLineKnownByUs = lines.length > 0; | ||
| const accepted = this.target.write(chunk); | ||
| if (accepted) { | ||
| if (accepted !== false) { | ||
| return; | ||
| } | ||
| if (this.target.on === undefined || this.target.off === undefined) { | ||
| return; | ||
| } | ||
| this.#waitingForDrain = true; | ||
| this.#drainPromise = new Promise((resolve) => { | ||
| const onDrain = () => { | ||
| const settle = (replayPending) => { | ||
| if (!this.#waitingForDrain) { | ||
| return; | ||
| } | ||
| if (this.#drainTimer !== undefined) { | ||
| clearTimeout(this.#drainTimer); | ||
| this.#drainTimer = undefined; | ||
| } | ||
| this.target.off?.("drain", onDrain); | ||
| this.target.off?.("error", onError); | ||
| this.target.off?.("close", onClose); | ||
| this.target.off?.("finish", onFinish); | ||
| this.#waitingForDrain = false; | ||
@@ -141,3 +194,3 @@ this.#drainPromise = undefined; | ||
| this.lease.pendingFrame = undefined; | ||
| if (pending !== undefined) { | ||
| if (replayPending && pending !== undefined) { | ||
| this.#writeNow(pending); | ||
@@ -147,3 +200,11 @@ } | ||
| }; | ||
| const onDrain = () => settle(true); | ||
| const onError = () => settle(false); | ||
| const onClose = () => settle(false); | ||
| const onFinish = () => settle(false); | ||
| this.target.on?.("drain", onDrain); | ||
| this.target.on?.("error", onError); | ||
| this.target.on?.("close", onClose); | ||
| this.target.on?.("finish", onFinish); | ||
| this.#drainTimer = setTimeout(() => settle(false), this.backpressureTimeoutMs); | ||
| }); | ||
@@ -155,2 +216,8 @@ } | ||
| } | ||
| function mergePendingFrame(previous, next) { | ||
| if (previous?.kind === "json" && next.kind === "json") { | ||
| return { kind: "json", events: [...previous.events, ...next.events] }; | ||
| } | ||
| return next; | ||
| } | ||
| function eraseLines(count) { | ||
@@ -157,0 +224,0 @@ let output = "\r\u001b[2K"; |
@@ -7,2 +7,3 @@ import { type LaquEvent } from "./events.js"; | ||
| readonly kind: "live"; | ||
| readonly scrollbackLines: readonly string[]; | ||
| readonly lines: readonly string[]; | ||
@@ -20,2 +21,3 @@ } | { | ||
| render(snapshot: RuntimeSnapshot): Frame; | ||
| finalize?(snapshot: RuntimeSnapshot): Frame; | ||
| } | ||
@@ -25,3 +27,5 @@ export interface RendererDecision { | ||
| readonly live: boolean; | ||
| readonly jsonSerialization: JsonSerialization; | ||
| } | ||
| export type JsonSerialization = "none" | "ndjson" | "array"; | ||
| export interface RendererOptions { | ||
@@ -37,2 +41,3 @@ readonly format: OutputFormat; | ||
| export declare class AnsiLiveRenderer implements Renderer { | ||
| #private; | ||
| private readonly theme; | ||
@@ -55,2 +60,3 @@ private readonly columns; | ||
| render(snapshot: RuntimeSnapshot): Frame; | ||
| finalize(snapshot: RuntimeSnapshot): Frame; | ||
| } | ||
@@ -57,0 +63,0 @@ export declare class NullRenderer implements Renderer { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEjF,OAAO,KAAK,EAAE,eAAe,EAA4B,MAAM,iBAAiB,CAAC;AAEjF,MAAM,MAAM,KAAK,GACb;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC7D;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAA;CAAE,GAChE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9B,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK,CAAC;CAC1C;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,gBAAgB,CA0BzE;AAED,qBAAa,gBAAiB,YAAW,QAAQ;IAE7C,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAFP,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM;IAGlC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK;CAMzC;AAED,qBAAa,gBAAiB,YAAW,QAAQ;;IAK7C,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAFP,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM;IAGlC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK;CAqBzC;AAED,qBAAa,iBAAkB,YAAW,QAAQ;;IAIhD,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK;CA8BzC;AAED,qBAAa,YAAa,YAAW,QAAQ;IAC3C,MAAM,IAAI,KAAK;CAGhB"} | ||
| {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEjF,OAAO,KAAK,EAAa,eAAe,EAA4B,MAAM,iBAAiB,CAAC;AAE5F,MAAM,MAAM,KAAK,GACb;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC7D;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAA;CAAE,GAChE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9B,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK,CAAC;IACzC,QAAQ,CAAC,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK,CAAC;CAC7C;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;CAC/C;AAED,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE5D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,gBAAgB,CAkCzE;AAED,qBAAa,gBAAiB,YAAW,QAAQ;;IAI7C,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAFP,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM;IAGlC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK;CAWzC;AAED,qBAAa,gBAAiB,YAAW,QAAQ;;IAK7C,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAFP,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM;IAGlC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK;CAuBzC;AAED,qBAAa,iBAAkB,YAAW,QAAQ;;IAKhD,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK;IA0BxC,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,KAAK;CAS3C;AAkBD,qBAAa,YAAa,YAAW,QAAQ;IAC3C,MAAM,IAAI,KAAK;CAGhB"} |
+73
-24
| import { logEvent, summaryEvent, taskEvent } from "./events.js"; | ||
| import { renderSegments, text } from "./theme.js"; | ||
| import { truncateToColumns } from "./width.js"; | ||
| import { sanitizeText, truncateToColumns } from "./width.js"; | ||
| export function chooseRenderer(options) { | ||
| if (options.policy === "silent" || options.policy === "never") { | ||
| return { renderer: new NullRenderer(), live: false }; | ||
| return { renderer: new NullRenderer(), live: false, jsonSerialization: "none" }; | ||
| } | ||
| if (options.policy === "jsonl" || options.format === "json" || options.format === "ndjson") { | ||
| return { renderer: new JsonEventRenderer(), live: false }; | ||
| return { | ||
| renderer: new JsonEventRenderer(), | ||
| live: false, | ||
| jsonSerialization: options.policy === "jsonl" || options.format === "ndjson" ? "ndjson" : "array", | ||
| }; | ||
| } | ||
@@ -15,2 +19,3 @@ if (options.policy === "plain") { | ||
| live: false, | ||
| jsonSerialization: "none", | ||
| }; | ||
@@ -23,2 +28,3 @@ } | ||
| live: true, | ||
| jsonSerialization: "none", | ||
| }; | ||
@@ -29,2 +35,3 @@ } | ||
| live: false, | ||
| jsonSerialization: "none", | ||
| }; | ||
@@ -36,2 +43,3 @@ } | ||
| maxRows; | ||
| #seenLogSequence = 0; | ||
| constructor(theme, columns, maxRows) { | ||
@@ -43,4 +51,8 @@ this.theme = theme; | ||
| render(snapshot) { | ||
| const newLogs = logsAfterSequence(snapshot.logs, this.#seenLogSequence); | ||
| const scrollbackLines = renderLogLines(newLogs, this.theme, this.columns); | ||
| this.#seenLogSequence = lastLogSequence(snapshot.logs, this.#seenLogSequence); | ||
| return { | ||
| kind: "live", | ||
| scrollbackLines, | ||
| lines: rowsForSnapshot(snapshot, this.theme, this.columns, this.maxRows), | ||
@@ -55,3 +67,3 @@ }; | ||
| #seenTaskStates = new Map(); | ||
| #seenLogs = 0; | ||
| #seenLogSequence = 0; | ||
| constructor(theme, columns, maxRows) { | ||
@@ -64,7 +76,8 @@ this.theme = theme; | ||
| const lines = []; | ||
| for (const log of snapshot.logs.slice(this.#seenLogs)) { | ||
| lines.push(truncateToColumns(log.message, this.columns, this.theme.tokens)); | ||
| } | ||
| this.#seenLogs = snapshot.logs.length; | ||
| for (const row of flattenTasks(snapshot.tasks).slice(0, this.maxRows)) { | ||
| const newLogs = logsAfterSequence(snapshot.logs, this.#seenLogSequence); | ||
| lines.push(...renderLogLines(newLogs, this.theme, this.columns)); | ||
| this.#seenLogSequence = lastLogSequence(snapshot.logs, this.#seenLogSequence); | ||
| const rows = flattenTasks(snapshot.tasks).slice(0, this.maxRows); | ||
| pruneSeenTaskStates(this.#seenTaskStates, rows); | ||
| for (const row of rows) { | ||
| const state = `${row.status}:${row.message ?? ""}:${row.detail ?? ""}:${progressText(row, this.theme)}`; | ||
@@ -82,10 +95,13 @@ if (this.#seenTaskStates.get(row.id) === state) { | ||
| #seenTaskStates = new Map(); | ||
| #seenLogs = 0; | ||
| #seenLogSequence = 0; | ||
| #summaryEmitted = false; | ||
| render(snapshot) { | ||
| const events = []; | ||
| for (const log of snapshot.logs.slice(this.#seenLogs)) { | ||
| const tasks = flattenTasks(snapshot.tasks); | ||
| pruneSeenTaskStates(this.#seenTaskStates, tasks); | ||
| for (const log of logsAfterSequence(snapshot.logs, this.#seenLogSequence)) { | ||
| events.push(logEvent(log.message, log.createdAt)); | ||
| } | ||
| this.#seenLogs = snapshot.logs.length; | ||
| for (const task of flattenTasks(snapshot.tasks)) { | ||
| this.#seenLogSequence = lastLogSequence(snapshot.logs, this.#seenLogSequence); | ||
| for (const task of tasks) { | ||
| const ratio = task.aggregate.kind === "ratio" ? task.aggregate.ratio : undefined; | ||
@@ -100,5 +116,10 @@ const overrun = task.aggregate.kind === "ratio" ? task.aggregate.overrun : undefined; | ||
| } | ||
| if (events.length > 0 && | ||
| flattenTasks(snapshot.tasks).every((task) => task.status !== "running")) { | ||
| events.push(summaryEvent(snapshot.tasks, snapshot.createdAt)); | ||
| return events.length === 0 ? { kind: "none" } : { kind: "json", events }; | ||
| } | ||
| finalize(snapshot) { | ||
| const rendered = this.render(snapshot); | ||
| const events = rendered.kind === "json" ? [...rendered.events] : []; | ||
| if (!this.#summaryEmitted && snapshot.summary.total > 0) { | ||
| events.push(summaryEvent(snapshot.summary, snapshot.createdAt)); | ||
| this.#summaryEmitted = true; | ||
| } | ||
@@ -108,2 +129,11 @@ return events.length === 0 ? { kind: "none" } : { kind: "json", events }; | ||
| } | ||
| function renderLogLines(logs, theme, columns) { | ||
| return logs.map((log) => truncateToColumns(sanitizeText(log.message), columns, theme.tokens)); | ||
| } | ||
| function logsAfterSequence(logs, seenSequence) { | ||
| return logs.filter((log) => log.sequence > seenSequence); | ||
| } | ||
| function lastLogSequence(logs, fallback) { | ||
| return logs.at(-1)?.sequence ?? fallback; | ||
| } | ||
| export class NullRenderer { | ||
@@ -120,3 +150,3 @@ render() { | ||
| if (hidden > 0) { | ||
| output.push(truncateToColumns(`${hidden} more tasks…`, columns, theme.tokens)); | ||
| output.push(truncateToColumns(`${hidden} more tasks...`, columns, theme.tokens)); | ||
| } | ||
@@ -127,8 +157,26 @@ return output; | ||
| const rows = []; | ||
| for (const task of tasks) { | ||
| const stack = [...tasks].reverse(); | ||
| while (stack.length > 0) { | ||
| const task = stack.pop(); | ||
| if (task === undefined) { | ||
| continue; | ||
| } | ||
| rows.push(task); | ||
| rows.push(...flattenTasks(task.children)); | ||
| for (let index = task.children.length - 1; index >= 0; index -= 1) { | ||
| const child = task.children[index]; | ||
| if (child !== undefined) { | ||
| stack.push(child); | ||
| } | ||
| } | ||
| } | ||
| return rows; | ||
| } | ||
| function pruneSeenTaskStates(seenTaskStates, currentTasks) { | ||
| const currentIds = new Set(currentTasks.map((task) => task.id)); | ||
| for (const id of seenTaskStates.keys()) { | ||
| if (!currentIds.has(id)) { | ||
| seenTaskStates.delete(id); | ||
| } | ||
| } | ||
| } | ||
| function renderTaskRow(task, theme, columns) { | ||
@@ -138,6 +186,7 @@ const symbol = statusSymbol(task, theme); | ||
| const progress = progressText(task, theme); | ||
| const message = task.message === undefined ? "" : `${theme.tokens.gap}${task.message}`; | ||
| const detail = task.detail === undefined | ||
| ? "" | ||
| : `${theme.tokens.gap}${theme.format(text(task.detail, "muted"))}`; | ||
| const safeTitle = sanitizeText(task.title); | ||
| const safeMessage = task.message === undefined ? undefined : sanitizeText(task.message); | ||
| const safeDetail = task.detail === undefined ? undefined : sanitizeText(task.detail); | ||
| const message = safeMessage === undefined ? "" : `${theme.tokens.gap}${safeMessage}`; | ||
| const detail = safeDetail === undefined ? "" : `${theme.tokens.gap}${theme.format(text(safeDetail, "muted"))}`; | ||
| const row = renderSegments(theme, [ | ||
@@ -147,3 +196,3 @@ text(indent), | ||
| text(theme.tokens.gap), | ||
| text(task.title), | ||
| text(safeTitle), | ||
| text(progress === "" ? "" : `${theme.tokens.gap}${progress}`, "accent"), | ||
@@ -150,0 +199,0 @@ text(message), |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAEV,eAAe,EAEf,cAAc,EAKf,MAAM,YAAY,CAAC;AAIpB,wBAAgB,UAAU,CAAC,OAAO,GAAE,cAAmB,GAAG,eAAe,CAExE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,cAAmB,GAAG,eAAe,CAsBnF"} | ||
| {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAEV,eAAe,EAEf,cAAc,EAKf,MAAM,YAAY,CAAC;AASpB,wBAAgB,UAAU,CAAC,OAAO,GAAE,cAAmB,GAAG,eAAe,CAExE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,cAAmB,GAAG,eAAe,CAoCnF"} |
+125
-33
@@ -6,2 +6,3 @@ import { OutputCoordinator } from "./output-coordinator.js"; | ||
| const defaultFlushHz = 15; | ||
| const liveStreamLeases = new WeakSet(); | ||
| export function createLaqu(options = {}) { | ||
@@ -15,4 +16,4 @@ return createProgressRuntime(options); | ||
| const policy = options.progressPolicy ?? "auto"; | ||
| const theme = compileTheme({ useColor: env.NO_COLOR === undefined, ...options.theme }); | ||
| const decision = chooseRenderer({ | ||
| const theme = compileTheme({ useColor: defaultUseColor(capability, env), ...options.theme }); | ||
| const rendererOptions = { | ||
| format: options.format ?? "human", | ||
@@ -24,7 +25,15 @@ policy, | ||
| maxRows: options.maxRows ?? 12, | ||
| }; | ||
| const initialDecision = chooseRenderer(rendererOptions); | ||
| const liveStreamLease = initialDecision.live ? acquireLiveStreamLease(stderr) : undefined; | ||
| const decision = initialDecision.live && liveStreamLease === undefined | ||
| ? chooseRenderer({ ...rendererOptions, policy: "plain" }) | ||
| : initialDecision; | ||
| const store = new TaskStore({ | ||
| maxLogs: options.retention?.maxLogs, | ||
| maxTerminalTasks: options.retention?.maxTerminalTasks, | ||
| }); | ||
| const store = new TaskStore(); | ||
| const coordinator = new OutputCoordinator(stderr, decision.renderer, decision.live); | ||
| const runtime = new LaquRuntime(store, coordinator, policy); | ||
| if (options.manageProcessLifecycle !== false) { | ||
| const coordinator = new OutputCoordinator(stderr, decision.renderer, decision.live, decision.jsonSerialization); | ||
| const runtime = new LaquRuntime(store, coordinator, policy, liveStreamLease); | ||
| if (options.manageProcessLifecycle === true) { | ||
| runtime.manageProcessLifecycle(); | ||
@@ -38,10 +47,15 @@ } | ||
| policy; | ||
| liveStreamLease; | ||
| #timer; | ||
| #flushPromise; | ||
| #closePromise; | ||
| #dirty = false; | ||
| #closing = false; | ||
| #closed = false; | ||
| #processLifecycle; | ||
| constructor(store, coordinator, policy) { | ||
| constructor(store, coordinator, policy, liveStreamLease) { | ||
| this.store = store; | ||
| this.coordinator = coordinator; | ||
| this.policy = policy; | ||
| this.liveStreamLease = liveStreamLease; | ||
| } | ||
@@ -57,2 +71,5 @@ async task(title, optionsOrCallback, maybeCallback) { | ||
| options.signal?.addEventListener("abort", onAbort, { once: true }); | ||
| if (options.signal?.aborted === true) { | ||
| handle.cancel("aborted"); | ||
| } | ||
| try { | ||
@@ -64,8 +81,12 @@ const result = await callback(handle); | ||
| catch (error) { | ||
| if (options.signal?.aborted === true) { | ||
| handle.cancel("aborted"); | ||
| if (this.#acceptsMutations()) { | ||
| if (options.signal?.aborted === true) { | ||
| this.store.forceTerminalUpdate(handle.id, { status: "cancelled", message: "aborted" }); | ||
| } | ||
| else { | ||
| const message = error instanceof Error ? error.message : undefined; | ||
| this.store.forceTerminalUpdate(handle.id, { status: "failed", message }); | ||
| } | ||
| this.markDirty(true); | ||
| } | ||
| else { | ||
| handle.fail(error); | ||
| } | ||
| throw error; | ||
@@ -79,4 +100,5 @@ } | ||
| createTask(title, options = {}) { | ||
| this.#assertAcceptsMutations(); | ||
| const id = this.store.createTask(title, options); | ||
| const handle = new StoreTaskHandle(id, this.store, (immediate) => this.markDirty(immediate)); | ||
| const handle = new StoreTaskHandle(id, this.store, (immediate) => this.markDirty(immediate), () => this.#assertAcceptsMutations()); | ||
| this.markDirty(true); | ||
@@ -86,2 +108,3 @@ return handle; | ||
| log(message) { | ||
| this.#assertAcceptsMutations(); | ||
| this.store.addLog(message); | ||
@@ -91,27 +114,46 @@ this.markDirty(true); | ||
| async flush() { | ||
| if (this.#timer !== undefined) { | ||
| clearTimeout(this.#timer); | ||
| this.#timer = undefined; | ||
| } | ||
| this.#dirty = false; | ||
| this.coordinator.render(this.store.snapshot()); | ||
| await this.coordinator.flush(); | ||
| this.#flushPromise ??= this.#flushOnce().finally(() => { | ||
| this.#flushPromise = undefined; | ||
| }); | ||
| await this.#flushPromise; | ||
| } | ||
| async close() { | ||
| this.#closePromise ??= this.#closeOnce(); | ||
| await this.#closePromise; | ||
| } | ||
| manageProcessLifecycle() { | ||
| this.#processLifecycle ??= new ProcessLifecycleLease(() => { | ||
| return this.close(); | ||
| }); | ||
| } | ||
| async #flushOnce() { | ||
| do { | ||
| if (this.#timer !== undefined) { | ||
| clearTimeout(this.#timer); | ||
| this.#timer = undefined; | ||
| } | ||
| this.#dirty = false; | ||
| this.coordinator.render(this.store.snapshot()); | ||
| await this.coordinator.flush(); | ||
| } while (this.#dirty && !this.#closed && this.policy !== "silent" && this.policy !== "never"); | ||
| } | ||
| async #closeOnce() { | ||
| if (this.#closed) { | ||
| return; | ||
| } | ||
| this.#closing = true; | ||
| this.#processLifecycle?.dispose(); | ||
| this.#processLifecycle = undefined; | ||
| await this.flush(); | ||
| await this.coordinator.close(); | ||
| this.#closed = true; | ||
| try { | ||
| await this.flush(); | ||
| this.#closed = true; | ||
| this.coordinator.finalize(this.store.snapshot()); | ||
| await this.coordinator.close(); | ||
| } | ||
| finally { | ||
| this.liveStreamLease?.release(); | ||
| } | ||
| } | ||
| manageProcessLifecycle() { | ||
| this.#processLifecycle ??= new ProcessLifecycleLease(() => { | ||
| return this.close(); | ||
| }); | ||
| } | ||
| markDirty(immediate = false) { | ||
| if (this.#closed || this.policy === "silent" || this.policy === "never") { | ||
| if (this.#closing || this.#closed || this.policy === "silent" || this.policy === "never") { | ||
| return; | ||
@@ -134,2 +176,10 @@ } | ||
| } | ||
| #acceptsMutations() { | ||
| return !this.#closing && !this.#closed; | ||
| } | ||
| #assertAcceptsMutations() { | ||
| if (!this.#acceptsMutations()) { | ||
| throw new Error("Laqu runtime is closing"); | ||
| } | ||
| } | ||
| } | ||
@@ -154,5 +204,9 @@ class ProcessLifecycleLease { | ||
| }; | ||
| this.#onRejection = () => { | ||
| this.#onRejection = (reason) => { | ||
| process.exitCode = 1; | ||
| void cleanup(); | ||
| void cleanup().finally(() => { | ||
| setImmediate(() => { | ||
| throw reason instanceof Error ? reason : new Error("Unhandled promise rejection"); | ||
| }); | ||
| }); | ||
| }; | ||
@@ -175,8 +229,11 @@ process.once("SIGINT", this.#onSignal); | ||
| onChange; | ||
| constructor(id, store, onChange) { | ||
| assertWritable; | ||
| constructor(id, store, onChange, assertWritable) { | ||
| this.id = id; | ||
| this.store = store; | ||
| this.onChange = onChange; | ||
| this.assertWritable = assertWritable; | ||
| } | ||
| setTotal(total) { | ||
| this.assertWritable(); | ||
| this.store.update(this.id, { progress: setTotalProgress(total) }); | ||
@@ -186,2 +243,3 @@ this.onChange(false); | ||
| setCompleted(completed) { | ||
| this.assertWritable(); | ||
| this.store.update(this.id, { | ||
@@ -193,2 +251,3 @@ progress: setCompletedProgress(completed, this.store.getProgress(this.id)), | ||
| advance(delta) { | ||
| this.assertWritable(); | ||
| this.store.update(this.id, { | ||
@@ -200,2 +259,3 @@ progress: advanceProgress(delta, this.store.getProgress(this.id)), | ||
| setRatio(ratio) { | ||
| this.assertWritable(); | ||
| this.store.update(this.id, { progress: ratioProgress(ratio) }); | ||
@@ -208,2 +268,3 @@ this.onChange(false); | ||
| setIndeterminate(message) { | ||
| this.assertWritable(); | ||
| this.store.update(this.id, { progress: { kind: "indeterminate" }, message }); | ||
@@ -213,2 +274,3 @@ this.onChange(false); | ||
| setMessage(message) { | ||
| this.assertWritable(); | ||
| this.store.update(this.id, { message }); | ||
@@ -218,2 +280,3 @@ this.onChange(false); | ||
| setDetail(detail) { | ||
| this.assertWritable(); | ||
| this.store.update(this.id, { detail }); | ||
@@ -223,2 +286,3 @@ this.onChange(false); | ||
| succeed(message) { | ||
| this.assertWritable(); | ||
| this.store.update(this.id, { status: "succeeded", message }); | ||
@@ -228,2 +292,3 @@ this.onChange(true); | ||
| fail(error) { | ||
| this.assertWritable(); | ||
| const message = error instanceof Error ? error.message : undefined; | ||
@@ -234,2 +299,3 @@ this.store.update(this.id, { status: "failed", message }); | ||
| cancel(message) { | ||
| this.assertWritable(); | ||
| this.store.update(this.id, { status: "cancelled", message }); | ||
@@ -239,5 +305,6 @@ this.onChange(true); | ||
| child(title, options = {}) { | ||
| this.assertWritable(); | ||
| const id = this.store.createTask(title, options, this.id); | ||
| this.onChange(true); | ||
| return new StoreTaskHandle(id, this.store, this.onChange); | ||
| return new StoreTaskHandle(id, this.store, this.onChange, this.assertWritable); | ||
| } | ||
@@ -254,1 +321,26 @@ } | ||
| } | ||
| function defaultUseColor(capability, env) { | ||
| if (env.NO_COLOR !== undefined) { | ||
| return false; | ||
| } | ||
| if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "0") { | ||
| return true; | ||
| } | ||
| return capability === "tty"; | ||
| } | ||
| function acquireLiveStreamLease(stream) { | ||
| if (liveStreamLeases.has(stream)) { | ||
| return undefined; | ||
| } | ||
| liveStreamLeases.add(stream); | ||
| let released = false; | ||
| return { | ||
| release() { | ||
| if (released) { | ||
| return; | ||
| } | ||
| released = true; | ||
| liveStreamLeases.delete(stream); | ||
| }, | ||
| }; | ||
| } |
+15
-0
@@ -47,7 +47,17 @@ import type { TaskOptions } from "./types.js"; | ||
| readonly logs: readonly LogRecord[]; | ||
| readonly summary: TaskSummaryCounts; | ||
| readonly createdAt: number; | ||
| } | ||
| export interface TaskSummaryCounts { | ||
| readonly total: number; | ||
| readonly running: number; | ||
| readonly succeeded: number; | ||
| readonly failed: number; | ||
| readonly cancelled: number; | ||
| readonly skipped: number; | ||
| } | ||
| export interface LogRecord { | ||
| readonly message: string; | ||
| readonly createdAt: number; | ||
| readonly sequence: number; | ||
| } | ||
@@ -68,4 +78,9 @@ interface TaskNode { | ||
| #private; | ||
| constructor(options?: { | ||
| readonly maxLogs?: number | undefined; | ||
| readonly maxTerminalTasks?: number | undefined; | ||
| }); | ||
| createTask(title: string, options?: TaskOptions, parentId?: string): string; | ||
| update(id: string, update: Partial<Omit<TaskNode, "id" | "parentId" | "children">>): void; | ||
| forceTerminalUpdate(id: string, update: Pick<Partial<Omit<TaskNode, "id" | "parentId" | "children">>, "status" | "message">): void; | ||
| getProgress(id: string): ProgressState; | ||
@@ -72,0 +87,0 @@ addLog(message: string): void; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../src/task-store.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../src/task-store.ts"],"names":[],"mappings":"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"} |
+285
-37
@@ -5,3 +5,21 @@ export class TaskStore { | ||
| #logs = []; | ||
| #prunedTerminalIds = new Set(); | ||
| #snapshottedTerminalIds = new Set(); | ||
| #terminalOrder = []; | ||
| #maxLogs; | ||
| #maxTerminalTasks; | ||
| #summaryCounts = { | ||
| total: 0, | ||
| running: 0, | ||
| succeeded: 0, | ||
| failed: 0, | ||
| cancelled: 0, | ||
| skipped: 0, | ||
| }; | ||
| #nextId = 1; | ||
| #nextLogSequence = 1; | ||
| constructor(options = {}) { | ||
| this.#maxLogs = validatedMaxRecords(options.maxLogs ?? 1_000, "maxLogs"); | ||
| this.#maxTerminalTasks = validatedMaxRecords(options.maxTerminalTasks ?? 1_000, "maxTerminalTasks"); | ||
| } | ||
| createTask(title, options = {}, parentId) { | ||
@@ -19,3 +37,3 @@ const id = `task-${this.#nextId}`; | ||
| detail: options.detail, | ||
| weight: options.weight ?? 1, | ||
| weight: validatedWeight(options.weight), | ||
| children: [], | ||
@@ -25,2 +43,4 @@ updatedAt: now, | ||
| this.#tasks.set(id, node); | ||
| this.#summaryCounts.total += 1; | ||
| this.#summaryCounts.running += 1; | ||
| if (parentId === undefined) { | ||
@@ -31,2 +51,8 @@ this.#rootIds.push(id); | ||
| const parent = this.#requireNode(parentId); | ||
| if (isTerminalStatus(parent.status)) { | ||
| this.#tasks.delete(id); | ||
| this.#summaryCounts.total -= 1; | ||
| this.#summaryCounts.running -= 1; | ||
| throw new Error(`Cannot create child task under terminal task: ${parentId}`); | ||
| } | ||
| parent.children.push(id); | ||
@@ -38,53 +64,103 @@ parent.updatedAt = now; | ||
| update(id, update) { | ||
| const node = this.#requireNode(id); | ||
| if (update.title !== undefined) { | ||
| node.title = update.title; | ||
| const node = this.#tasks.get(id); | ||
| if (node === undefined && this.#prunedTerminalIds.has(id)) { | ||
| return; | ||
| } | ||
| if (update.status !== undefined) { | ||
| node.status = update.status; | ||
| if (node === undefined) { | ||
| throw new Error(`Unknown task id: ${id}`); | ||
| } | ||
| if (update.progress !== undefined) { | ||
| node.progress = update.progress; | ||
| if (isTerminalStatus(node.status)) { | ||
| return; | ||
| } | ||
| if (Object.hasOwn(update, "message")) { | ||
| node.message = update.message; | ||
| const previousStatus = node.status; | ||
| applyUpdate(node, update); | ||
| this.#recordStatusTransition(node.id, previousStatus, node.status); | ||
| } | ||
| forceTerminalUpdate(id, update) { | ||
| const node = this.#tasks.get(id); | ||
| if (node === undefined && this.#prunedTerminalIds.has(id)) { | ||
| return; | ||
| } | ||
| if (Object.hasOwn(update, "detail")) { | ||
| node.detail = update.detail; | ||
| if (node === undefined) { | ||
| throw new Error(`Unknown task id: ${id}`); | ||
| } | ||
| if (update.weight !== undefined) { | ||
| node.weight = update.weight; | ||
| } | ||
| node.updatedAt = Date.now(); | ||
| const previousStatus = node.status; | ||
| applyUpdate(node, update); | ||
| this.#recordStatusTransition(node.id, previousStatus, node.status); | ||
| } | ||
| getProgress(id) { | ||
| return this.#requireNode(id).progress; | ||
| const node = this.#tasks.get(id); | ||
| if (node === undefined && this.#prunedTerminalIds.has(id)) { | ||
| return { kind: "none" }; | ||
| } | ||
| if (node === undefined) { | ||
| throw new Error(`Unknown task id: ${id}`); | ||
| } | ||
| return node.progress; | ||
| } | ||
| addLog(message) { | ||
| this.#logs.push({ message, createdAt: Date.now() }); | ||
| if (this.#maxLogs === 0) { | ||
| this.#nextLogSequence += 1; | ||
| return; | ||
| } | ||
| this.#logs.push({ message, createdAt: Date.now(), sequence: this.#nextLogSequence }); | ||
| this.#nextLogSequence += 1; | ||
| const excess = this.#logs.length - this.#maxLogs; | ||
| if (excess > 0) { | ||
| this.#logs.splice(0, excess); | ||
| } | ||
| } | ||
| snapshot() { | ||
| return { | ||
| tasks: this.#rootIds.map((id) => this.#snapshotNode(id, 0)), | ||
| const snapshot = { | ||
| tasks: this.#snapshotTasks(), | ||
| logs: [...this.#logs], | ||
| summary: { ...this.#summaryCounts }, | ||
| createdAt: Date.now(), | ||
| }; | ||
| this.#pruneTerminalTasks(); | ||
| rememberTerminalTasks(snapshot.tasks, this.#snapshottedTerminalIds); | ||
| return snapshot; | ||
| } | ||
| #snapshotNode(id, depth) { | ||
| const node = this.#requireNode(id); | ||
| const children = node.children.map((childId) => this.#snapshotNode(childId, depth + 1)); | ||
| return { | ||
| id: node.id, | ||
| parentId: node.parentId, | ||
| title: node.title, | ||
| status: node.status, | ||
| progress: node.progress, | ||
| aggregate: aggregateProgress(node.progress, children), | ||
| message: node.message, | ||
| detail: node.detail, | ||
| weight: node.weight, | ||
| depth, | ||
| children, | ||
| updatedAt: node.updatedAt, | ||
| }; | ||
| #snapshotTasks() { | ||
| const snapshots = new Map(); | ||
| const stack = []; | ||
| for (let index = this.#rootIds.length - 1; index >= 0; index -= 1) { | ||
| const id = this.#rootIds[index]; | ||
| if (id !== undefined) { | ||
| stack.push({ id, depth: 0, visited: false }); | ||
| } | ||
| } | ||
| while (stack.length > 0) { | ||
| const item = stack.pop(); | ||
| if (item === undefined) { | ||
| continue; | ||
| } | ||
| const node = this.#requireNode(item.id); | ||
| if (!item.visited) { | ||
| stack.push({ id: item.id, depth: item.depth, visited: true }); | ||
| for (let index = node.children.length - 1; index >= 0; index -= 1) { | ||
| const childId = node.children[index]; | ||
| if (childId !== undefined) { | ||
| stack.push({ id: childId, depth: item.depth + 1, visited: false }); | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| const children = node.children.map((childId) => requireSnapshot(snapshots, childId)); | ||
| snapshots.set(node.id, { | ||
| id: node.id, | ||
| parentId: node.parentId, | ||
| title: node.title, | ||
| status: node.status, | ||
| progress: node.progress, | ||
| aggregate: aggregateProgress(node.progress, children), | ||
| message: node.message, | ||
| detail: node.detail, | ||
| weight: node.weight, | ||
| depth: item.depth, | ||
| children, | ||
| updatedAt: node.updatedAt, | ||
| }); | ||
| } | ||
| return this.#rootIds.map((id) => requireSnapshot(snapshots, id)); | ||
| } | ||
@@ -94,2 +170,5 @@ #requireNode(id) { | ||
| if (node === undefined) { | ||
| if (this.#prunedTerminalIds.has(id)) { | ||
| throw new Error(`Task id was pruned after terminal retention: ${id}`); | ||
| } | ||
| throw new Error(`Unknown task id: ${id}`); | ||
@@ -99,2 +178,60 @@ } | ||
| } | ||
| #recordStatusTransition(id, previousStatus, nextStatus) { | ||
| if (previousStatus === nextStatus) { | ||
| return; | ||
| } | ||
| decrementSummaryStatus(this.#summaryCounts, previousStatus); | ||
| incrementSummaryStatus(this.#summaryCounts, nextStatus); | ||
| if (!isTerminalStatus(previousStatus) && isTerminalStatus(nextStatus)) { | ||
| this.#terminalOrder.push(id); | ||
| } | ||
| } | ||
| #pruneTerminalTasks() { | ||
| let retainedTerminalTasks = this.#countRetainedTerminalTasks(); | ||
| while (retainedTerminalTasks > this.#maxTerminalTasks) { | ||
| const orderIndex = this.#terminalOrder.findIndex((id) => { | ||
| const node = this.#tasks.get(id); | ||
| return (node === undefined || | ||
| (this.#snapshottedTerminalIds.has(id) && | ||
| isTerminalStatus(node.status) && | ||
| node.children.length === 0)); | ||
| }); | ||
| if (orderIndex === -1) { | ||
| return; | ||
| } | ||
| const id = this.#terminalOrder.splice(orderIndex, 1)[0]; | ||
| if (id === undefined) { | ||
| return; | ||
| } | ||
| const node = this.#tasks.get(id); | ||
| if (node === undefined || !isTerminalStatus(node.status) || node.children.length > 0) { | ||
| continue; | ||
| } | ||
| this.#removeTaskNode(node); | ||
| retainedTerminalTasks -= 1; | ||
| } | ||
| } | ||
| #countRetainedTerminalTasks() { | ||
| let count = 0; | ||
| for (const node of this.#tasks.values()) { | ||
| if (isTerminalStatus(node.status)) { | ||
| count += 1; | ||
| } | ||
| } | ||
| return count; | ||
| } | ||
| #removeTaskNode(node) { | ||
| this.#tasks.delete(node.id); | ||
| this.#prunedTerminalIds.add(node.id); | ||
| this.#snapshottedTerminalIds.delete(node.id); | ||
| if (node.parentId === undefined) { | ||
| removeArrayItem(this.#rootIds, node.id); | ||
| return; | ||
| } | ||
| const parent = this.#tasks.get(node.parentId); | ||
| if (parent !== undefined) { | ||
| removeArrayItem(parent.children, node.id); | ||
| parent.updatedAt = Date.now(); | ||
| } | ||
| } | ||
| } | ||
@@ -139,2 +276,3 @@ export function setTotalProgress(total, current = 0) { | ||
| if (options.completed !== undefined) { | ||
| assertFiniteNonNegative(options.completed, "completed"); | ||
| return { kind: "counter", current: options.completed }; | ||
@@ -186,2 +324,88 @@ } | ||
| } | ||
| function isTerminalStatus(status) { | ||
| return (status === "succeeded" || status === "failed" || status === "skipped" || status === "cancelled"); | ||
| } | ||
| function incrementSummaryStatus(counts, status) { | ||
| switch (status) { | ||
| case "running": | ||
| counts.running += 1; | ||
| return; | ||
| case "succeeded": | ||
| counts.succeeded += 1; | ||
| return; | ||
| case "failed": | ||
| counts.failed += 1; | ||
| return; | ||
| case "cancelled": | ||
| counts.cancelled += 1; | ||
| return; | ||
| case "skipped": | ||
| counts.skipped += 1; | ||
| return; | ||
| case "pending": | ||
| return; | ||
| } | ||
| } | ||
| function decrementSummaryStatus(counts, status) { | ||
| switch (status) { | ||
| case "running": | ||
| counts.running -= 1; | ||
| return; | ||
| case "succeeded": | ||
| counts.succeeded -= 1; | ||
| return; | ||
| case "failed": | ||
| counts.failed -= 1; | ||
| return; | ||
| case "cancelled": | ||
| counts.cancelled -= 1; | ||
| return; | ||
| case "skipped": | ||
| counts.skipped -= 1; | ||
| return; | ||
| case "pending": | ||
| return; | ||
| } | ||
| } | ||
| function validatedWeight(weight) { | ||
| if (weight === undefined) { | ||
| return 1; | ||
| } | ||
| assertFiniteNonNegative(weight, "weight"); | ||
| return weight; | ||
| } | ||
| function validatedMaxRecords(value, name) { | ||
| if (!Number.isSafeInteger(value) || value < 0) { | ||
| throw new TypeError(`${name} must be a safe non-negative integer`); | ||
| } | ||
| return value; | ||
| } | ||
| function requireSnapshot(snapshots, id) { | ||
| const snapshot = snapshots.get(id); | ||
| if (snapshot === undefined) { | ||
| throw new Error(`Missing task snapshot: ${id}`); | ||
| } | ||
| return snapshot; | ||
| } | ||
| function applyUpdate(node, update) { | ||
| if (update.title !== undefined) { | ||
| node.title = update.title; | ||
| } | ||
| if (update.status !== undefined) { | ||
| node.status = update.status; | ||
| } | ||
| if (update.progress !== undefined) { | ||
| node.progress = update.progress; | ||
| } | ||
| if (Object.hasOwn(update, "message")) { | ||
| node.message = update.message; | ||
| } | ||
| if (Object.hasOwn(update, "detail")) { | ||
| node.detail = update.detail; | ||
| } | ||
| if (update.weight !== undefined) { | ||
| node.weight = validatedWeight(update.weight); | ||
| } | ||
| node.updatedAt = Date.now(); | ||
| } | ||
| function assertFiniteNonNegative(value, name) { | ||
@@ -195,1 +419,25 @@ if (!Number.isFinite(value) || value < 0) { | ||
| } | ||
| function removeArrayItem(items, item) { | ||
| const index = items.indexOf(item); | ||
| if (index !== -1) { | ||
| items.splice(index, 1); | ||
| } | ||
| } | ||
| function rememberTerminalTasks(tasks, seenTerminalIds) { | ||
| const stack = [...tasks].reverse(); | ||
| while (stack.length > 0) { | ||
| const task = stack.pop(); | ||
| if (task === undefined) { | ||
| continue; | ||
| } | ||
| if (isTerminalStatus(task.status)) { | ||
| seenTerminalIds.add(task.id); | ||
| } | ||
| for (let index = task.children.length - 1; index >= 0; index -= 1) { | ||
| const child = task.children[index]; | ||
| if (child !== undefined) { | ||
| stack.push(child); | ||
| } | ||
| } | ||
| } | ||
| } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC;CAC5C;AAyBD,wBAAgB,YAAY,CAAC,KAAK,GAAE,UAAe,GAAG,aAAa,CAgBlE;AAED,wBAAgB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,iBAAiB,CAE7F;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,CAEvE;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GACrC,MAAM,CAER"} | ||
| {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG1D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC;CAC5C;AAyBD,wBAAgB,YAAY,CAAC,KAAK,GAAE,UAAe,GAAG,aAAa,CAiBlE;AAED,wBAAgB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,iBAAiB,CAE7F;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,CAEvE;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GACrC,MAAM,CAER"} |
+4
-2
@@ -0,1 +1,2 @@ | ||
| import { sanitizeText } from "./width.js"; | ||
| const defaultTokens = { | ||
@@ -31,6 +32,7 @@ successSymbol: "✓", | ||
| } | ||
| const safeText = sanitizeText(segment.text); | ||
| if (!useColor || segment.style === undefined) { | ||
| return segment.text; | ||
| return safeText; | ||
| } | ||
| return `${styles[segment.style]}${segment.text}${styles.reset}`; | ||
| return `${styles[segment.style]}${safeText}${styles.reset}`; | ||
| }, | ||
@@ -37,0 +39,0 @@ }; |
+7
-2
@@ -10,4 +10,4 @@ export type OutputFormat = "human" | "json" | "ndjson"; | ||
| write(chunk: string): boolean; | ||
| on?(event: "drain", listener: () => void): unknown; | ||
| off?(event: "drain", listener: () => void): unknown; | ||
| on?(event: "drain" | "error" | "close" | "finish", listener: (...args: readonly unknown[]) => void): unknown; | ||
| off?(event: "drain" | "error" | "close" | "finish", listener: (...args: readonly unknown[]) => void): unknown; | ||
| } | ||
@@ -30,5 +30,10 @@ export interface RuntimeEnvironment { | ||
| readonly maxRows?: number; | ||
| readonly retention?: RuntimeRetentionOptions; | ||
| readonly theme?: ThemeInput; | ||
| readonly manageProcessLifecycle?: boolean; | ||
| } | ||
| export interface RuntimeRetentionOptions { | ||
| readonly maxLogs?: number | undefined; | ||
| readonly maxTerminalTasks?: number | undefined; | ||
| } | ||
| export interface TaskOptions { | ||
@@ -35,0 +40,0 @@ readonly total?: number; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} |
+1
-0
@@ -15,2 +15,3 @@ export interface WidthOptions { | ||
| export declare function stripAnsi(input: string): string; | ||
| export declare function sanitizeText(input: string): string; | ||
| export declare function graphemes(input: string): string[]; | ||
@@ -17,0 +18,0 @@ export declare function displayWidth(input: string, options?: WidthOptions): number; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"width.d.ts","sourceRoot":"","sources":["../src/width.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAQtD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,CAkBvD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAGjD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAW9E;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,YAAiB,GACzB,MAAM,CAkCR;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,YAAiB,GACzB,MAAM,EAAE,CAoCV"} | ||
| {"version":3,"file":"width.d.ts","sourceRoot":"","sources":["../src/width.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAUtD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,CAkBvD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEjD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAW9E;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,YAAiB,GACzB,MAAM,CAkCR;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,YAAiB,GACzB,MAAM,EAAE,CAoCV"} |
+5
-1
| // CSI, OSC, and common one-byte ESC sequences. | ||
| const ansiPattern = new RegExp(String.raw `\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\)|[@-Z\\-_])`, "g"); | ||
| const unsafeControlPattern = new RegExp(String.raw `[\u0000-\u0008\u000a-\u001f\u007f-\u009f]`, "g"); | ||
| const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); | ||
| export function tokenizeAnsi(input) { | ||
@@ -25,4 +27,6 @@ const tokens = []; | ||
| } | ||
| export function sanitizeText(input) { | ||
| return stripAnsi(input).replaceAll(unsafeControlPattern, (control) => control === "\n" || control === "\r" ? " " : "\uFFFD"); | ||
| } | ||
| export function graphemes(input) { | ||
| const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); | ||
| return Array.from(segmenter.segment(input), (segment) => segment.segment); | ||
@@ -29,0 +33,0 @@ } |
+3
-2
| { | ||
| "name": "@0disoft/laqu", | ||
| "version": "1.0.2", | ||
| "version": "1.0.4", | ||
| "description": "Reliable terminal progress and live CLI rendering primitives for TypeScript.", | ||
@@ -73,3 +73,4 @@ "keywords": [ | ||
| "node": ">=24.0.0" | ||
| } | ||
| }, | ||
| "packageManager": "bun@1.3.14" | ||
| } |
+30
-6
@@ -54,2 +54,4 @@ # laqu | ||
| After `progress.close()` starts, the runtime stops accepting new tasks, logs, and task handle updates. Create a new runtime for later progress output. | ||
| ## Logs | ||
@@ -66,2 +68,17 @@ | ||
| ## Process Lifecycle | ||
| `laqu` does not install process-level signal or exception handlers by default. Applications that | ||
| already own shutdown should keep the default and call `progress.close()` from their own cleanup | ||
| path. | ||
| Short-lived CLI commands that want `laqu` to flush progress output during `SIGINT`, `SIGTERM`, | ||
| `uncaughtException`, or `unhandledRejection` can opt in: | ||
| ```ts | ||
| const progress = createLaqu({ | ||
| manageProcessLifecycle: true, | ||
| }); | ||
| ``` | ||
| ## Public Imports | ||
@@ -90,2 +107,3 @@ | ||
| - human live rendering is enabled only when the status stream is a TTY and the environment is not CI. | ||
| - only one runtime owns live rendering for a stream at a time; concurrent runtimes on that same stream fall back to plain append rendering until the live owner closes. | ||
| - CI, pipe, dumb terminal, and non-TTY output fall back to plain append rendering unless a different policy is requested. | ||
@@ -96,4 +114,5 @@ - JSON/NDJSON progress events do not go to stdout unless the caller explicitly passes a separate status stream that points there. | ||
| const progress = createLaqu({ | ||
| format: "json", | ||
| format: "ndjson", | ||
| progressPolicy: "jsonl", | ||
| retention: { maxLogs: 1000, maxTerminalTasks: 1000 }, | ||
| stderr: process.stderr, | ||
@@ -103,4 +122,7 @@ }); | ||
| Machine-readable progress events use a versioned schema: | ||
| Machine-readable progress events use a versioned schema. `format: "json"` writes one parseable JSON array when the runtime closes; `format: "ndjson"` and `progressPolicy: "jsonl"` write newline-delimited event objects as work progresses. | ||
| The runtime retains the newest 1000 log records and 1000 terminal task records by default so long-running commands do not keep unbounded output buffers. Set `retention.maxLogs` or `retention.maxTerminalTasks` to smaller non-negative integers when only the latest output window should be rendered or emitted. Terminal task pruning affects retained task rows and task events only after the terminal task has been snapshotted for rendering; summary events keep lifecycle counts for all tasks created by the runtime. | ||
| Task event fields such as `parentId`, `message`, and `detail` are omitted when they are absent. | ||
| ```json | ||
@@ -185,9 +207,11 @@ { | ||
| GitHub Actions publishes npm releases from maintainer-created version tags. The tag must match `package.json` exactly, for example `v1.0.2` for version `1.0.2`. | ||
| GitHub Actions publishes npm releases from maintainer-created version tags. The tag must match `package.json` exactly, for example `v1.0.4` for version `1.0.4`. | ||
| ```sh | ||
| git tag -a v1.0.2 -m "v1.0.2" | ||
| git push origin main v1.0.2 | ||
| git tag -a v1.0.4 -m "v1.0.4" | ||
| git push origin main v1.0.4 | ||
| ``` | ||
| The npm package must define a Trusted Publisher connection for GitHub Actions with organization/user `0disoft`, repository `laqu`, workflow filename `release.yml`, no environment name, and `npm publish` allowed. On a matching tag push, the workflow installs dependencies, checks the package, runs a dry pack verification, publishes `@0disoft/laqu` to npm through OIDC, and creates a GitHub Release with generated notes. | ||
| The npm package must define a Trusted Publisher connection for GitHub Actions with organization/user `0disoft`, repository `laqu`, workflow filename `release.yml`, environment name `npm`, and `npm publish` allowed. The GitHub repository must also define an `npm` environment with required reviewers and a deployment tag rule that allows only `v*.*.*` tags. | ||
| On a matching tag push, the workflow first verifies the tag, package metadata, tests, build, and dry pack output with read-only repository permissions. The publish job then waits on the `npm` environment gate, repeats package verification on the tagged commit, packs the release tarball, uploads that exact tarball as a retained workflow artifact, publishes the same tarball to npm with provenance through OIDC, and creates a GitHub Release with generated notes. |
94253
29.99%1887
34.12%212
12.77%