ugly-app 0.1.809 → 0.1.810
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/native/facades/task.d.ts +25 -1
- package/dist/native/facades/task.d.ts.map +1 -1
- package/dist/native/facades/task.js +60 -4
- package/dist/native/facades/task.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/native/facades/task.test.ts +62 -0
- package/src/native/facades/task.ts +69 -3
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.810";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
|
@@ -28,6 +28,30 @@ export interface TaskFacade {
|
|
|
28
28
|
params?: unknown;
|
|
29
29
|
id?: string;
|
|
30
30
|
}): Promise<TaskHandle>;
|
|
31
|
+
/**
|
|
32
|
+
* Idempotent upsert of a background task by stable `id`. Reuses the running task
|
|
33
|
+
* when its bundle (`entry`) matches; otherwise replaces it — stops the stale/dead
|
|
34
|
+
* one, starts fresh, and waits until it is ready — then returns the handle. Use
|
|
35
|
+
* this instead of hand-rolling enum→compare→stop→start: it also absorbs the host
|
|
36
|
+
* stop→start-same-id exit race (a killed child's async `exit`, keyed by id, can
|
|
37
|
+
* otherwise clobber the freshly started task to 'exited'). `reused` is false when
|
|
38
|
+
* a NEW child was started — signalling the caller to re-establish per-task
|
|
39
|
+
* `listen()` subscriptions (a replaced task drops the old host listener).
|
|
40
|
+
*/
|
|
41
|
+
ensure(opts: {
|
|
42
|
+
id: string;
|
|
43
|
+
entry: string;
|
|
44
|
+
kind?: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
params?: unknown;
|
|
47
|
+
/** Max wait for the fresh task to report ready (default 20s). */
|
|
48
|
+
readyTimeoutMs?: number;
|
|
49
|
+
/** Readiness poll interval (default 100ms). */
|
|
50
|
+
pollMs?: number;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
handle: TaskHandle;
|
|
53
|
+
reused: boolean;
|
|
54
|
+
}>;
|
|
31
55
|
/** List this app's background tasks (origin-scoped on the host). */
|
|
32
56
|
enum(): Promise<TaskInfo[]>;
|
|
33
57
|
/** Get a handle to an already-running task by id without spawning. */
|
|
@@ -52,6 +76,6 @@ type Subscribe = (event: string, cb: (data: unknown) => void) => () => void;
|
|
|
52
76
|
* listen→ subscribe('task.event:'+id) (host fans events out as { event, data })
|
|
53
77
|
* Assembled client-side so it's identical local (desktop) or proxied (mobile/web).
|
|
54
78
|
*/
|
|
55
|
-
export declare function createTaskFacade(invoke: Invoke, subscribe: Subscribe,
|
|
79
|
+
export declare function createTaskFacade(invoke: Invoke, subscribe: Subscribe, whenReady?: () => Promise<void>): TaskFacade;
|
|
56
80
|
export {};
|
|
57
81
|
//# sourceMappingURL=task.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../../src/native/facades/task.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAGnF;AAED,2EAA2E;AAC3E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,+FAA+F;IAC/F,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACpE,0FAA0F;IAC1F,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjE,gEAAgE;IAChE,IAAI,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,UAAU;IACzB,yFAAyF;IACzF,KAAK,CAAC,IAAI,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACxB,oEAAoE;IACpE,IAAI,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC5B,sEAAsE;IACtE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAAC;IAC/B,oCAAoC;IACpC,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7E,0EAA0E;IAC1E,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAChF,yBAAyB;IACzB,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACjD;AAKD,KAAK,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AACtE,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;AAE5E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../../src/native/facades/task.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAGnF;AAED,2EAA2E;AAC3E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,+FAA+F;IAC/F,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACpE,0FAA0F;IAC1F,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjE,gEAAgE;IAChE,IAAI,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,UAAU;IACzB,yFAAyF;IACzF,KAAK,CAAC,IAAI,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACxB;;;;;;;;;OASG;IACH,MAAM,CAAC,IAAI,EAAE;QACX,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,iEAAiE;QACjE,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,+CAA+C;QAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACrD,oEAAoE;IACpE,IAAI,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC5B,sEAAsE;IACtE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAAC;IAC/B,oCAAoC;IACpC,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7E,0EAA0E;IAC1E,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAChF,yBAAyB;IACzB,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACjD;AAKD,KAAK,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AACtE,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;AAE5E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,UAAU,CA6ElH"}
|
|
@@ -18,13 +18,31 @@ export function taskEntryUrl(name, buildId, origin) {
|
|
|
18
18
|
* listen→ subscribe('task.event:'+id) (host fans events out as { event, data })
|
|
19
19
|
* Assembled client-side so it's identical local (desktop) or proxied (mobile/web).
|
|
20
20
|
*/
|
|
21
|
-
export function createTaskFacade(invoke, subscribe,
|
|
21
|
+
export function createTaskFacade(invoke, subscribe, whenReady) {
|
|
22
22
|
const listen = (id, onEvent) => subscribe(`task.event:${id}`, (d) => {
|
|
23
23
|
const f = (d ?? {});
|
|
24
24
|
onEvent(f.event ?? 'event', f.data);
|
|
25
25
|
});
|
|
26
26
|
const call = (id, method, payload) => invoke('task.call', { id, method, payload });
|
|
27
27
|
const stop = (id) => invoke('task.stop', { id });
|
|
28
|
+
const enumTasks = async () => (await invoke('task.enum', undefined)).tasks;
|
|
29
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
30
|
+
// Poll enum until the task reports 'running'. Throws on a terminal 'exited'/'error'
|
|
31
|
+
// (a genuine startup failure — or, transiently, the stop→start clobber the caller
|
|
32
|
+
// retries past) and on timeout.
|
|
33
|
+
const waitRunning = async (id, timeoutMs, pollMs) => {
|
|
34
|
+
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
for (;;) {
|
|
36
|
+
const t = (await enumTasks()).find((x) => x.id === id);
|
|
37
|
+
if (t?.status === 'running')
|
|
38
|
+
return;
|
|
39
|
+
if (t && (t.status === 'exited' || t.status === 'error'))
|
|
40
|
+
throw new Error(`task failed to start: ${t.status}`);
|
|
41
|
+
if (Date.now() >= deadline)
|
|
42
|
+
throw new Error('task did not become ready in time');
|
|
43
|
+
await sleep(pollMs);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
28
46
|
const handle = (id) => ({
|
|
29
47
|
id,
|
|
30
48
|
listen: (onEvent) => listen(id, onEvent),
|
|
@@ -33,13 +51,51 @@ export function createTaskFacade(invoke, subscribe, ensure) {
|
|
|
33
51
|
});
|
|
34
52
|
return {
|
|
35
53
|
async start(opts) {
|
|
36
|
-
if (
|
|
37
|
-
await
|
|
54
|
+
if (whenReady)
|
|
55
|
+
await whenReady();
|
|
38
56
|
const r = (await invoke('task.start', opts));
|
|
39
57
|
return handle(r.id);
|
|
40
58
|
},
|
|
59
|
+
async ensure(opts) {
|
|
60
|
+
const { id, entry, kind, label, params } = opts;
|
|
61
|
+
const readyTimeoutMs = opts.readyTimeoutMs ?? 20_000;
|
|
62
|
+
const pollMs = opts.pollMs ?? 100;
|
|
63
|
+
const existing = (await enumTasks()).find((t) => t.id === id);
|
|
64
|
+
const alive = !!existing && existing.status !== 'exited' && existing.status !== 'error';
|
|
65
|
+
// Reuse a healthy task whose bundle matches. `entry` pins the buildId, so a
|
|
66
|
+
// task left from a prior deploy (different entry) is never reused.
|
|
67
|
+
if (alive && existing.entry === entry)
|
|
68
|
+
return { handle: handle(id), reused: true };
|
|
69
|
+
// Replace. Stop→start on the SAME id races the host: `stop` kills the old child
|
|
70
|
+
// asynchronously and its late `exit` (keyed by id) can fire AFTER the fresh task
|
|
71
|
+
// registers, clobbering it to 'exited'. So stop, let the old child die (settle),
|
|
72
|
+
// then start; retry with a growing settle if a slow-dying child still clobbers.
|
|
73
|
+
if (existing) {
|
|
74
|
+
try {
|
|
75
|
+
await stop(id);
|
|
76
|
+
}
|
|
77
|
+
catch { /* already gone */ }
|
|
78
|
+
}
|
|
79
|
+
const settles = alive ? [350, 900, 1800] : [0];
|
|
80
|
+
let lastErr;
|
|
81
|
+
for (const settleMs of settles) {
|
|
82
|
+
if (settleMs > 0)
|
|
83
|
+
await sleep(settleMs);
|
|
84
|
+
if (whenReady)
|
|
85
|
+
await whenReady();
|
|
86
|
+
await invoke('task.start', { id, entry, kind, label, params });
|
|
87
|
+
try {
|
|
88
|
+
await waitRunning(id, readyTimeoutMs, pollMs);
|
|
89
|
+
return { handle: handle(id), reused: false };
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
lastErr = e; // likely the clobber — retry after a longer settle
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw lastErr;
|
|
96
|
+
},
|
|
41
97
|
async enum() {
|
|
42
|
-
return (
|
|
98
|
+
return enumTasks();
|
|
43
99
|
},
|
|
44
100
|
attach: (id) => handle(id),
|
|
45
101
|
call,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task.js","sourceRoot":"","sources":["../../../src/native/facades/task.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAAe,EAAE,MAAe;IACzE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAChF,OAAO,GAAG,IAAI,IAAI,OAAO,UAAU,IAAI,KAAK,CAAC;AAC/C,CAAC;
|
|
1
|
+
{"version":3,"file":"task.js","sourceRoot":"","sources":["../../../src/native/facades/task.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.810",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
|
|
6
6
|
"files": [
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.810";
|
|
@@ -72,4 +72,66 @@ describe('createTaskFacade', () => {
|
|
|
72
72
|
expect(invoke).toHaveBeenCalledWith('task.call', { id: 'task_9', method: 'ping', payload: undefined });
|
|
73
73
|
expect(invoke).toHaveBeenCalledWith('task.stop', { id: 'task_9' });
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
const rec = (over: Partial<{ status: string; entry: string }>) => ({
|
|
77
|
+
id: 'coding:s', kind: 'coding', label: 'x', origin: 'o', startedAt: 1,
|
|
78
|
+
status: 'running', entry: 'E1', ...over,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('ensure reuses a running task whose entry matches (no stop/start)', async () => {
|
|
82
|
+
const invoke = vi.fn(async (ch: string) =>
|
|
83
|
+
ch === 'task.enum' ? { tasks: [rec({ entry: 'E1' })] } : undefined);
|
|
84
|
+
const r = await createTaskFacade(invoke, vi.fn()).ensure({ id: 'coding:s', entry: 'E1', kind: 'coding' });
|
|
85
|
+
expect(r.reused).toBe(true);
|
|
86
|
+
expect(r.handle.id).toBe('coding:s');
|
|
87
|
+
expect(invoke).not.toHaveBeenCalledWith('task.stop', expect.anything());
|
|
88
|
+
expect(invoke).not.toHaveBeenCalledWith('task.start', expect.anything());
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('ensure replaces a stale-entry task: stop old, start fresh, reused=false', async () => {
|
|
92
|
+
let started = false;
|
|
93
|
+
const invoke = vi.fn(async (ch: string) => {
|
|
94
|
+
if (ch === 'task.enum') return { tasks: [rec({ entry: started ? 'E2' : 'E1' })] };
|
|
95
|
+
if (ch === 'task.stop') return { stopped: true };
|
|
96
|
+
if (ch === 'task.start') { started = true; return { id: 'coding:s' }; }
|
|
97
|
+
return undefined;
|
|
98
|
+
});
|
|
99
|
+
const r = await createTaskFacade(invoke, vi.fn())
|
|
100
|
+
.ensure({ id: 'coding:s', entry: 'E2', kind: 'coding', readyTimeoutMs: 2000, pollMs: 1 });
|
|
101
|
+
expect(r.reused).toBe(false);
|
|
102
|
+
expect(invoke).toHaveBeenCalledWith('task.stop', { id: 'coding:s' });
|
|
103
|
+
expect(invoke).toHaveBeenCalledWith('task.start', expect.objectContaining({ id: 'coding:s', entry: 'E2' }));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('ensure starts fresh when no task exists, without stopping', async () => {
|
|
107
|
+
let started = false;
|
|
108
|
+
const invoke = vi.fn(async (ch: string) => {
|
|
109
|
+
if (ch === 'task.enum') return { tasks: started ? [rec({ entry: 'E1' })] : [] };
|
|
110
|
+
if (ch === 'task.start') { started = true; return { id: 'coding:s' }; }
|
|
111
|
+
return undefined;
|
|
112
|
+
});
|
|
113
|
+
const r = await createTaskFacade(invoke, vi.fn()).ensure({ id: 'coding:s', entry: 'E1', pollMs: 1 });
|
|
114
|
+
expect(r.reused).toBe(false);
|
|
115
|
+
expect(invoke).not.toHaveBeenCalledWith('task.stop', expect.anything());
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('ensure retries past a transient exited (the stop→start clobber race)', async () => {
|
|
119
|
+
let starts = 0;
|
|
120
|
+
const invoke = vi.fn(async (ch: string) => {
|
|
121
|
+
if (ch === 'task.enum') {
|
|
122
|
+
// stale E1 until first start; the first fresh task is clobbered to 'exited'
|
|
123
|
+
// (race), the second start lands 'running'.
|
|
124
|
+
if (starts === 0) return { tasks: [rec({ entry: 'E1' })] };
|
|
125
|
+
if (starts === 1) return { tasks: [rec({ entry: 'E2', status: 'exited' })] };
|
|
126
|
+
return { tasks: [rec({ entry: 'E2', status: 'running' })] };
|
|
127
|
+
}
|
|
128
|
+
if (ch === 'task.stop') return { stopped: true };
|
|
129
|
+
if (ch === 'task.start') { starts += 1; return { id: 'coding:s' }; }
|
|
130
|
+
return undefined;
|
|
131
|
+
});
|
|
132
|
+
const r = await createTaskFacade(invoke, vi.fn())
|
|
133
|
+
.ensure({ id: 'coding:s', entry: 'E2', kind: 'coding', readyTimeoutMs: 5000, pollMs: 1 });
|
|
134
|
+
expect(r.reused).toBe(false);
|
|
135
|
+
expect(starts).toBe(2); // clobbered once, succeeded on retry
|
|
136
|
+
});
|
|
75
137
|
});
|
|
@@ -32,6 +32,27 @@ export interface TaskFacade {
|
|
|
32
32
|
params?: unknown;
|
|
33
33
|
id?: string;
|
|
34
34
|
}): Promise<TaskHandle>;
|
|
35
|
+
/**
|
|
36
|
+
* Idempotent upsert of a background task by stable `id`. Reuses the running task
|
|
37
|
+
* when its bundle (`entry`) matches; otherwise replaces it — stops the stale/dead
|
|
38
|
+
* one, starts fresh, and waits until it is ready — then returns the handle. Use
|
|
39
|
+
* this instead of hand-rolling enum→compare→stop→start: it also absorbs the host
|
|
40
|
+
* stop→start-same-id exit race (a killed child's async `exit`, keyed by id, can
|
|
41
|
+
* otherwise clobber the freshly started task to 'exited'). `reused` is false when
|
|
42
|
+
* a NEW child was started — signalling the caller to re-establish per-task
|
|
43
|
+
* `listen()` subscriptions (a replaced task drops the old host listener).
|
|
44
|
+
*/
|
|
45
|
+
ensure(opts: {
|
|
46
|
+
id: string;
|
|
47
|
+
entry: string;
|
|
48
|
+
kind?: string;
|
|
49
|
+
label?: string;
|
|
50
|
+
params?: unknown;
|
|
51
|
+
/** Max wait for the fresh task to report ready (default 20s). */
|
|
52
|
+
readyTimeoutMs?: number;
|
|
53
|
+
/** Readiness poll interval (default 100ms). */
|
|
54
|
+
pollMs?: number;
|
|
55
|
+
}): Promise<{ handle: TaskHandle; reused: boolean }>;
|
|
35
56
|
/** List this app's background tasks (origin-scoped on the host). */
|
|
36
57
|
enum(): Promise<TaskInfo[]>;
|
|
37
58
|
/** Get a handle to an already-running task by id without spawning. */
|
|
@@ -59,7 +80,7 @@ type Subscribe = (event: string, cb: (data: unknown) => void) => () => void;
|
|
|
59
80
|
* listen→ subscribe('task.event:'+id) (host fans events out as { event, data })
|
|
60
81
|
* Assembled client-side so it's identical local (desktop) or proxied (mobile/web).
|
|
61
82
|
*/
|
|
62
|
-
export function createTaskFacade(invoke: Invoke, subscribe: Subscribe,
|
|
83
|
+
export function createTaskFacade(invoke: Invoke, subscribe: Subscribe, whenReady?: () => Promise<void>): TaskFacade {
|
|
63
84
|
const listen = (id: string, onEvent: (event: string, data: unknown) => void): (() => void) =>
|
|
64
85
|
subscribe(`task.event:${id}`, (d) => {
|
|
65
86
|
const f = (d ?? {}) as { event?: string; data?: unknown };
|
|
@@ -69,6 +90,22 @@ export function createTaskFacade(invoke: Invoke, subscribe: Subscribe, ensure?:
|
|
|
69
90
|
invoke('task.call', { id, method, payload }) as Promise<R>;
|
|
70
91
|
const stop = (id: string): Promise<{ stopped: boolean }> =>
|
|
71
92
|
invoke('task.stop', { id }) as Promise<{ stopped: boolean }>;
|
|
93
|
+
const enumTasks = async (): Promise<TaskInfo[]> =>
|
|
94
|
+
((await invoke('task.enum', undefined)) as { tasks: TaskInfo[] }).tasks;
|
|
95
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
96
|
+
// Poll enum until the task reports 'running'. Throws on a terminal 'exited'/'error'
|
|
97
|
+
// (a genuine startup failure — or, transiently, the stop→start clobber the caller
|
|
98
|
+
// retries past) and on timeout.
|
|
99
|
+
const waitRunning = async (id: string, timeoutMs: number, pollMs: number): Promise<void> => {
|
|
100
|
+
const deadline = Date.now() + timeoutMs;
|
|
101
|
+
for (;;) {
|
|
102
|
+
const t = (await enumTasks()).find((x) => x.id === id);
|
|
103
|
+
if (t?.status === 'running') return;
|
|
104
|
+
if (t && (t.status === 'exited' || t.status === 'error')) throw new Error(`task failed to start: ${t.status}`);
|
|
105
|
+
if (Date.now() >= deadline) throw new Error('task did not become ready in time');
|
|
106
|
+
await sleep(pollMs);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
72
109
|
|
|
73
110
|
const handle = (id: string): TaskHandle => ({
|
|
74
111
|
id,
|
|
@@ -79,12 +116,41 @@ export function createTaskFacade(invoke: Invoke, subscribe: Subscribe, ensure?:
|
|
|
79
116
|
|
|
80
117
|
return {
|
|
81
118
|
async start(opts) {
|
|
82
|
-
if (
|
|
119
|
+
if (whenReady) await whenReady();
|
|
83
120
|
const r = (await invoke('task.start', opts)) as { id: string };
|
|
84
121
|
return handle(r.id);
|
|
85
122
|
},
|
|
123
|
+
async ensure(opts) {
|
|
124
|
+
const { id, entry, kind, label, params } = opts;
|
|
125
|
+
const readyTimeoutMs = opts.readyTimeoutMs ?? 20_000;
|
|
126
|
+
const pollMs = opts.pollMs ?? 100;
|
|
127
|
+
const existing = (await enumTasks()).find((t) => t.id === id);
|
|
128
|
+
const alive = !!existing && existing.status !== 'exited' && existing.status !== 'error';
|
|
129
|
+
// Reuse a healthy task whose bundle matches. `entry` pins the buildId, so a
|
|
130
|
+
// task left from a prior deploy (different entry) is never reused.
|
|
131
|
+
if (alive && existing.entry === entry) return { handle: handle(id), reused: true };
|
|
132
|
+
// Replace. Stop→start on the SAME id races the host: `stop` kills the old child
|
|
133
|
+
// asynchronously and its late `exit` (keyed by id) can fire AFTER the fresh task
|
|
134
|
+
// registers, clobbering it to 'exited'. So stop, let the old child die (settle),
|
|
135
|
+
// then start; retry with a growing settle if a slow-dying child still clobbers.
|
|
136
|
+
if (existing) { try { await stop(id); } catch { /* already gone */ } }
|
|
137
|
+
const settles = alive ? [350, 900, 1800] : [0];
|
|
138
|
+
let lastErr: unknown;
|
|
139
|
+
for (const settleMs of settles) {
|
|
140
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
141
|
+
if (whenReady) await whenReady();
|
|
142
|
+
await invoke('task.start', { id, entry, kind, label, params });
|
|
143
|
+
try {
|
|
144
|
+
await waitRunning(id, readyTimeoutMs, pollMs);
|
|
145
|
+
return { handle: handle(id), reused: false };
|
|
146
|
+
} catch (e) {
|
|
147
|
+
lastErr = e; // likely the clobber — retry after a longer settle
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw lastErr;
|
|
151
|
+
},
|
|
86
152
|
async enum() {
|
|
87
|
-
return (
|
|
153
|
+
return enumTasks();
|
|
88
154
|
},
|
|
89
155
|
attach: (id) => handle(id),
|
|
90
156
|
call,
|