@cortexkit/opencode-magic-context 0.49.35 → 0.49.37
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/README.md +1 -1
- package/dist/auto-search-worker.js +163 -160
- package/dist/chunk-embedding-write-worker.js +58 -56
- package/dist/{compaction-marker-candidates-g0p67ced.js → compaction-marker-candidates-5p1qkve9.js} +3 -1
- package/dist/compaction-marker-candidates-worker.js +24 -23
- package/dist/{compartment-chunk-embedding-y4136are.js → compartment-chunk-embedding-ypktm0tk.js} +5 -4
- package/dist/{conflict-warning-hook-hhw8z5ak.js → conflict-warning-hook-b75jn458.js} +6 -5
- package/dist/{context-fk8e3h9p.js → context-ngk2jz60.js} +501 -97
- package/dist/{embedding-backfill-q6n72sy0.js → embedding-backfill-rma0v2ct.js} +9 -8
- package/dist/embedding-worker.js +2 -2
- package/dist/features/magic-context/compaction-marker-candidates.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts +5 -3
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-gate.d.ts +2 -0
- package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-state.d.ts +9 -0
- package/dist/features/magic-context/dreamer/verify-state.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify.d.ts +2 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/migration-worker-client.d.ts +3 -0
- package/dist/features/magic-context/migration-worker-client.d.ts.map +1 -1
- package/dist/features/magic-context/migration-worker.d.ts +15 -0
- package/dist/features/magic-context/migration-worker.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +2 -2
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/search-rerank.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +1 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +4 -0
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/work-metrics-carry-storage.d.ts +10 -0
- package/dist/features/magic-context/work-metrics-carry-storage.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts +5 -1
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-publication.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/issue-423-fixture.d.ts +1 -1
- package/dist/hooks/magic-context/issue-423-fixture.d.ts.map +1 -1
- package/dist/hooks/magic-context/issue-424-fixture.d.ts +1 -1
- package/dist/hooks/magic-context/issue-424-fixture.d.ts.map +1 -1
- package/dist/hooks/magic-context/project-structural-sentinels.d.ts +25 -0
- package/dist/hooks/magic-context/project-structural-sentinels.d.ts.map +1 -0
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts +8 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/raw-ordinal-warmup.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +2 -0
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/release-recovery-prefix.d.ts +9 -0
- package/dist/hooks/magic-context/release-recovery-prefix.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-structural-noise.d.ts +6 -3
- package/dist/hooks/magic-context/strip-structural-noise.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-context-state.d.ts +3 -0
- package/dist/hooks/magic-context/transform-context-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +3 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-cpjk5jd2.js → index-1d042x6w.js} +16 -14
- package/dist/{index-jn0ddrhp.js → index-21fgrfrf.js} +8 -6
- package/dist/{index-pq0dpwrc.js → index-2rs7912w.js} +2 -2
- package/dist/{index-ezv6dz3y.js → index-4ks89h5a.js} +4 -4
- package/dist/{index-vzhhr1ht.js → index-4y35msh5.js} +1 -2
- package/dist/{index-f297z1n8.js → index-72czy45z.js} +5 -5
- package/dist/index-7v05d9b7.js +58 -0
- package/dist/{index-f9b3vtt1.js → index-850vthn3.js} +8 -2
- package/dist/{index-ksv0a31x.js → index-8jjjmhab.js} +12 -1
- package/dist/index-96ztjvkn.js +204 -0
- package/dist/{index-gyaa159j.js → index-9fgmkfaf.js} +6 -3
- package/dist/{index-4bgk5ztz.js → index-9tdvbdv1.js} +187 -90
- package/dist/{index-2v6nvy3k.js → index-ae1ar4v1.js} +1 -1
- package/dist/{index-faxb7mck.js → index-b1ydbp95.js} +1 -1
- package/dist/{index-nxrsns5x.js → index-c5dzns43.js} +75 -75
- package/dist/{index-zhqwda72.js → index-dcjkw41m.js} +1 -1
- package/dist/{index-x2mrq5xv.js → index-dfdg2cgg.js} +179 -49
- package/dist/{index-87ctnzz9.js → index-dz48h1wx.js} +2 -2
- package/dist/{index-b6xz3qbe.js → index-gnmhbyyc.js} +366 -83
- package/dist/{index-qx8j6kds.js → index-j105mf16.js} +2 -2
- package/dist/{index-p56dszfp.js → index-jggt5gf2.js} +1 -1
- package/dist/{index-xwwsbsd3.js → index-pp8vdn01.js} +5 -5
- package/dist/{index-dy7w6wm4.js → index-prw8apnc.js} +5 -5
- package/dist/{index-qncw9hey.js → index-r2epb000.js} +33 -3
- package/dist/{index-brg2w6qc.js → index-tn9drm1a.js} +16 -14
- package/dist/{index-pswsyf1w.js → index-ty4vhe4n.js} +3 -3
- package/dist/{index-pztqab6k.js → index-vd2ywmfe.js} +1 -1
- package/dist/{index-3j9t1v4n.js → index-wcwhhtdh.js} +10 -9
- package/dist/{index-1rhgxqs8.js → index-xd2p95m7.js} +1 -1
- package/dist/{index-0c7p8mq5.js → index-xnm0wq1r.js} +1 -1
- package/dist/{index-3d3872c6.js → index-ys7j6679.js} +4 -4
- package/dist/index.js +38 -37
- package/dist/{message-fts-rowid-map-xg7hr0mk.js → message-fts-rowid-map-t6qsyg9s.js} +4 -3
- package/dist/{message-index-bounded-narw7bnz.js → message-index-bounded-wfk6aqxk.js} +9 -8
- package/dist/migration-worker.js +45 -40
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{project-embedding-registry-tdz7985n.js → project-embedding-registry-3yyegjma.js} +8 -7
- package/dist/raw-ordinal-warmup-worker.js +114 -118
- package/dist/{read-session-chunk-vmg1zjg0.js → read-session-chunk-26yrrhyz.js} +7 -6
- package/dist/{read-session-raw-mncmzbm9.js → read-session-raw-j7c9w57v.js} +3 -2
- package/dist/{recomp-orchestrator-qyyhnc9b.js → recomp-orchestrator-wk509qkp.js} +20 -19
- package/dist/{render-c5p0k846.js → render-h1qarr8a.js} +13 -12
- package/dist/{search-f6hwvcxk.js → search-5h9fydyq.js} +16 -15
- package/dist/{search-archive-0ny7p3bw.js → search-archive-q79hyqxs.js} +9 -8
- package/dist/{send-session-notification-0gwcz3fw.js → send-session-notification-14p3yje4.js} +5 -4
- package/dist/shared/model-suggestion-retry.d.ts +7 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-async-transport.d.ts.map +1 -1
- package/dist/shared/reusable-worker-entry.d.ts +4 -0
- package/dist/shared/reusable-worker-entry.d.ts.map +1 -0
- package/dist/shared/reusable-worker.d.ts +6 -0
- package/dist/shared/reusable-worker.d.ts.map +1 -0
- package/dist/shared/reusable-worker.fixture.d.ts +2 -0
- package/dist/shared/reusable-worker.fixture.d.ts.map +1 -0
- package/dist/shared/worker-shutdown.d.ts +1 -1
- package/dist/shared/worker-shutdown.d.ts.map +1 -1
- package/dist/{storage-tags-zx1023km.js → storage-tags-vj0yj7ed.js} +1 -1
- package/dist/{store-620rjzt9.js → store-s3eykdjt.js} +1 -1
- package/dist/{transform-postprocess-phase-807ff6t2.js → transform-postprocess-phase-4gj0mtcg.js} +21 -20
- package/dist/v2/auto-search-worker.js +163 -160
- package/dist/v2/chunk-embedding-write-worker.js +59 -57
- package/dist/v2/compaction-marker-candidates-worker.js +24 -23
- package/dist/v2/{compaction-marker-candidates-t1aq6gsj.js → compaction-marker-candidates-yeb2ky46.js} +4 -2
- package/dist/v2/{compartment-chunk-embedding-ncsk0r9e.js → compartment-chunk-embedding-qfk791bd.js} +6 -5
- package/dist/v2/{context-znqfkap4.js → context-05x7kr7f.js} +1027 -215
- package/dist/v2/{embedding-backfill-11j425e0.js → embedding-backfill-00tw59rx.js} +10 -9
- package/dist/v2/embedding-worker.js +2 -2
- package/dist/v2/fold/code-mode-catalog.d.ts +3 -0
- package/dist/v2/fold/code-mode-catalog.d.ts.map +1 -0
- package/dist/v2/fold/host-media.d.ts +7 -3
- package/dist/v2/fold/host-media.d.ts.map +1 -1
- package/dist/v2/fold/idle.d.ts +1 -0
- package/dist/v2/fold/idle.d.ts.map +1 -1
- package/dist/v2/fold/manual-baseline.d.ts +8 -0
- package/dist/v2/fold/manual-baseline.d.ts.map +1 -0
- package/dist/v2/fold/native-replay-v3.d.ts +7 -0
- package/dist/v2/fold/native-replay-v3.d.ts.map +1 -1
- package/dist/v2/fold/native-replay.d.ts +24 -7
- package/dist/v2/fold/native-replay.d.ts.map +1 -1
- package/dist/v2/fold/restore.d.ts.map +1 -1
- package/dist/v2/fold/summary.d.ts +4 -0
- package/dist/v2/fold/summary.d.ts.map +1 -0
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/channel2.d.ts +6 -0
- package/dist/v2/hooks/channel2.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/restore-rows.d.ts +12 -2
- package/dist/v2/hooks/restore-rows.d.ts.map +1 -1
- package/dist/v2/hooks/synthetic-admission.d.ts +7 -0
- package/dist/v2/hooks/synthetic-admission.d.ts.map +1 -0
- package/dist/v2/hooks/usage-persist.d.ts.map +1 -1
- package/dist/v2/{message-fts-rowid-map-rmk571mb.js → message-fts-rowid-map-z86avy0n.js} +5 -4
- package/dist/v2/{message-index-bounded-620bppw7.js → message-index-bounded-w14496s9.js} +9 -8
- package/dist/v2/migration-worker.js +46 -41
- package/dist/v2/{project-embedding-registry-bm5fe1a8.js → project-embedding-registry-jqq2qeqb.js} +9 -8
- package/dist/v2/raw-ordinal-warmup-worker.js +114 -118
- package/dist/v2/{read-session-chunk-qfwv1k71.js → read-session-chunk-tqnh1ret.js} +7 -6
- package/dist/v2/{read-session-raw-q294t8xs.js → read-session-raw-qpp6x9kg.js} +4 -3
- package/dist/v2/{recomp-orchestrator-fcnjref7.js → recomp-orchestrator-9cjk6aej.js} +19 -18
- package/dist/v2/{render-98a1qv0b.js → render-5dz1knf2.js} +13 -12
- package/dist/v2/{search-z1zqzsmh.js → search-8yfet25y.js} +15 -14
- package/dist/v2/{search-archive-r0y1bafr.js → search-archive-z6z9jqwf.js} +9 -8
- package/dist/v2/{send-session-notification-fst6hkg7.js → send-session-notification-kxp47tsw.js} +4 -3
- package/dist/v2/{server-cpjk5jd2.js → server-1d042x6w.js} +16 -14
- package/dist/v2/{server-fqj6ddpr.js → server-5by3vdzs.js} +186 -89
- package/dist/v2/{server-5jp38p98.js → server-6emy0v9a.js} +1 -2
- package/dist/v2/{server-rktnzs89.js → server-6vzh6wej.js} +1 -1
- package/dist/v2/{server-qa74bj91.js → server-7094pk3w.js} +9 -8
- package/dist/v2/{server-bxpz2dcn.js → server-7acq7815.js} +8 -6
- package/dist/v2/server-7v05d9b7.js +58 -0
- package/dist/v2/{server-24g8r2d0.js → server-7zacmty5.js} +75 -75
- package/dist/v2/server-96ztjvkn.js +204 -0
- package/dist/v2/{server-hnknwaz8.js → server-a9w5f0yw.js} +3 -3
- package/dist/v2/{server-9k0mjef7.js → server-avdveykk.js} +1 -1
- package/dist/v2/{server-hsf0qsn5.js → server-cp1a03vc.js} +1 -1
- package/dist/v2/{server-r3chnxs5.js → server-dfq5vb8j.js} +2 -2
- package/dist/v2/{server-x4m6xcn4.js → server-e51xmr4d.js} +6 -6
- package/dist/v2/{server-7ccxt4nq.js → server-g3gqq4ac.js} +6 -3
- package/dist/v2/{server-kkxzwa6w.js → server-gkse1wyz.js} +3 -3
- package/dist/v2/{server-1xesqm5h.js → server-jd7vzwn4.js} +15 -13
- package/dist/v2/{server-ws5exe6q.js → server-nmyganc8.js} +1 -1
- package/dist/v2/{server-q0z2q2dx.js → server-nq3cb7be.js} +1 -1
- package/dist/v2/{server-55v5hpth.js → server-qjyp27vf.js} +2 -2
- package/dist/v2/{server-xbtw0p9t.js → server-r4430x9m.js} +9 -3
- package/dist/v2/{server-qf2kzsh2.js → server-t8y64xj7.js} +4 -4
- package/dist/v2/{server-s9xgyyj9.js → server-tnzbk64q.js} +34 -4
- package/dist/v2/{server-51779zs2.js → server-v0p37hm2.js} +4 -1
- package/dist/v2/{server-bpvsvtph.js → server-xfjjbrkt.js} +12 -1
- package/dist/v2/server.js +4 -4
- package/dist/v2/{storage-tags-19s0ebf0.js → storage-tags-cw07ky67.js} +1 -1
- package/dist/v2/{store-vdm8v3jd.js → store-z3qe67cg.js} +1 -1
- package/dist/v2/{v2-archive-reader-t2c0eypn.js → v2-archive-reader-z7wr42ry.js} +3 -3
- package/dist/v2/{wrapup-orchestrator-aqpqsveg.js → wrapup-orchestrator-j1v62bkj.js} +19 -18
- package/dist/{v2-archive-reader-tm2eayp2.js → v2-archive-reader-028z6kck.js} +2 -2
- package/dist/{wrapup-orchestrator-k5yp19a6.js → wrapup-orchestrator-xg9avt2p.js} +21 -20
- package/package.json +1 -1
- package/src/shared/model-suggestion-retry.ts +1 -1
- package/src/shared/prompt-async-transport.test.ts +64 -3
- package/src/shared/prompt-async-transport.ts +61 -16
- package/src/shared/reusable-worker-entry.ts +58 -0
- package/src/shared/reusable-worker-reset.test.ts +108 -0
- package/src/shared/reusable-worker.fixture.ts +35 -0
- package/src/shared/reusable-worker.test.ts +253 -0
- package/src/shared/reusable-worker.ts +240 -0
- package/src/shared/worker-shutdown.ts +21 -17
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parentPort, threadId } from "node:worker_threads";
|
|
2
|
+
import { serveWorkerJobs } from "./reusable-worker-entry";
|
|
3
|
+
|
|
4
|
+
serveWorkerJobs<{
|
|
5
|
+
crash?: boolean;
|
|
6
|
+
hang?: boolean;
|
|
7
|
+
late?: boolean;
|
|
8
|
+
exitCode?: boolean;
|
|
9
|
+
value?: string;
|
|
10
|
+
shared?: SharedArrayBuffer;
|
|
11
|
+
}>(async (input, port) => {
|
|
12
|
+
if (input.crash) throw new Error("injected worker crash");
|
|
13
|
+
if (input.hang) return await new Promise<void>(() => {});
|
|
14
|
+
if (input.exitCode) process.exitCode = 1;
|
|
15
|
+
if (input.shared) {
|
|
16
|
+
const state = new Int32Array(input.shared);
|
|
17
|
+
port.postMessage({ threadId, shared: Atomics.load(state, 0) });
|
|
18
|
+
await new Promise<void>((resolve) =>
|
|
19
|
+
port.once("message", () => {
|
|
20
|
+
port.postMessage({ threadId, shared: Atomics.add(state, 0, 1) });
|
|
21
|
+
resolve();
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
port.close();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (input.late)
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
port.postMessage({ value: "stale" });
|
|
30
|
+
// Bypass the job port to verify the owner ignores a mismatched job id.
|
|
31
|
+
parentPort?.postMessage({ jobId: -1, value: { value: "stale" } });
|
|
32
|
+
}, 20);
|
|
33
|
+
port.postMessage({ threadId, value: input.value });
|
|
34
|
+
port.close();
|
|
35
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { MessageChannel, type Worker } from "node:worker_threads";
|
|
8
|
+
import { reuseWorker } from "./reusable-worker";
|
|
9
|
+
import { withTestTempDir } from "./test-temp-dir";
|
|
10
|
+
|
|
11
|
+
const entry = new URL("./reusable-worker.fixture.ts", import.meta.url);
|
|
12
|
+
let key = 0;
|
|
13
|
+
function start(path: string, input: Record<string, unknown> = {}, idleMs = 60_000) {
|
|
14
|
+
return reuseWorker(entry, { workerData: { path, ...input } }, idleMs);
|
|
15
|
+
}
|
|
16
|
+
async function result(worker: Worker) {
|
|
17
|
+
const reply = once(worker, "message");
|
|
18
|
+
const exit = once(worker, "exit");
|
|
19
|
+
const [message] = await reply;
|
|
20
|
+
await exit;
|
|
21
|
+
return message as { threadId: number; value?: string };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("reusable job workers", () => {
|
|
25
|
+
test("reuses a thread but gives each job fresh input and port state", async () => {
|
|
26
|
+
const path = `reuse-${++key}`;
|
|
27
|
+
const first = await result(start(path, { value: "first" }));
|
|
28
|
+
const second = await result(start(path, { value: "second" }));
|
|
29
|
+
expect(second.threadId).toBe(first.threadId);
|
|
30
|
+
expect(second.value).toBe("second");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("replaces a crashed worker and fails its job", async () => {
|
|
34
|
+
const path = `crash-${++key}`;
|
|
35
|
+
const first = await result(start(path));
|
|
36
|
+
const crashed = start(path, { crash: true });
|
|
37
|
+
const error = new Promise<Error>((resolve) => crashed.once("error", resolve));
|
|
38
|
+
const exit = new Promise<void>((resolve) => crashed.once("exit", () => resolve()));
|
|
39
|
+
expect((await error).message).toContain("injected worker crash");
|
|
40
|
+
await exit;
|
|
41
|
+
const next = await result(start(path));
|
|
42
|
+
expect(next.threadId).not.toBe(first.threadId);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("terminates a hung job and replaces its thread", async () => {
|
|
46
|
+
const path = `hang-${++key}`;
|
|
47
|
+
const first = await result(start(path));
|
|
48
|
+
const hung = start(path, { hang: true });
|
|
49
|
+
// Allow dispatch, then terminate the worker to exercise the hard-stop behavior.
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
51
|
+
await hung.terminate();
|
|
52
|
+
const next = await result(start(path));
|
|
53
|
+
expect(next.threadId).not.toBe(first.threadId);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("shuts the thread down after its idle lifetime", async () => {
|
|
57
|
+
const path = `idle-${++key}`;
|
|
58
|
+
const first = await result(start(path, {}, 20));
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
60
|
+
const next = await result(start(path, {}, 20));
|
|
61
|
+
expect(next.threadId).not.toBe(first.threadId);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("rejects late replies from a released job", async () => {
|
|
65
|
+
const path = `late-${++key}`;
|
|
66
|
+
const prior = start(path, { late: true });
|
|
67
|
+
const oldReplies: unknown[] = [];
|
|
68
|
+
prior.on("message", (reply) => oldReplies.push(reply));
|
|
69
|
+
const first = await result(prior);
|
|
70
|
+
const next = start(path, { hang: true });
|
|
71
|
+
const replies: unknown[] = [];
|
|
72
|
+
next.on("message", (reply) => replies.push(reply));
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
74
|
+
expect(oldReplies).toHaveLength(1);
|
|
75
|
+
expect(replies).toEqual([]);
|
|
76
|
+
await next.terminate();
|
|
77
|
+
expect(first.threadId).toBeGreaterThan(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("queues concurrent jobs without sharing job input", async () => {
|
|
81
|
+
const path = `queue-${++key}`;
|
|
82
|
+
const [a, b] = await Promise.all([
|
|
83
|
+
result(start(path, { value: "a" })),
|
|
84
|
+
result(start(path, { value: "b" })),
|
|
85
|
+
]);
|
|
86
|
+
expect(a.threadId).toBe(b.threadId);
|
|
87
|
+
expect([a.value, b.value]).toEqual(["a", "b"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a crash does not fail a queued job that never entered the thread", async () => {
|
|
91
|
+
const path = `queued-crash-${++key}`;
|
|
92
|
+
const crashed = start(path, { crash: true });
|
|
93
|
+
crashed.on("error", () => {});
|
|
94
|
+
const next = await result(start(path, { value: "unstarted" }));
|
|
95
|
+
expect(next.value).toBe("unstarted");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("keeps different database paths in separate threads", async () => {
|
|
99
|
+
const a = await result(start(`db-a-${++key}`));
|
|
100
|
+
const b = await result(start(`db-b-${++key}`));
|
|
101
|
+
expect(a.threadId).not.toBe(b.threadId);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("copies a job's input before dispatch", async () => {
|
|
105
|
+
const data = { path: `copy-${++key}`, value: "submitted" };
|
|
106
|
+
const job = reuseWorker(entry, { workerData: data });
|
|
107
|
+
data.value = "changed after submission";
|
|
108
|
+
expect((await result(job)).value).toBe("submitted");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("job input keeps shared coordination live in both directions", async () => {
|
|
112
|
+
const state = new Int32Array(new SharedArrayBuffer(4));
|
|
113
|
+
Atomics.store(state, 0, 1);
|
|
114
|
+
const worker = start(`shared-${++key}`, { shared: state.buffer });
|
|
115
|
+
const first = once(worker, "message");
|
|
116
|
+
const exit = once(worker, "exit");
|
|
117
|
+
expect((await first)[0].shared).toBe(1);
|
|
118
|
+
const changed = once(worker, "message");
|
|
119
|
+
Atomics.store(state, 0, 7);
|
|
120
|
+
worker.postMessage({ read: true });
|
|
121
|
+
expect((await changed)[0].shared).toBe(7);
|
|
122
|
+
await exit;
|
|
123
|
+
expect(Atomics.load(state, 0)).toBe(8);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a nonzero job exit fails the lease and replaces the thread", async () => {
|
|
127
|
+
const path = `exit-code-${++key}`;
|
|
128
|
+
const first = await result(start(path));
|
|
129
|
+
const failed = start(path, { exitCode: true });
|
|
130
|
+
expect((await once(failed, "exit"))[0]).toBe(1);
|
|
131
|
+
expect((await result(start(path))).threadId).not.toBe(first.threadId);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("process shutdown releases queued jobs without starting a replacement worker", async () => {
|
|
135
|
+
const path = `shutdown-${++key}`;
|
|
136
|
+
await result(start(path));
|
|
137
|
+
const active = start(path, { hang: true });
|
|
138
|
+
const queued = start(path, { value: "must not run during shutdown" });
|
|
139
|
+
const replies: unknown[] = [];
|
|
140
|
+
queued.on("message", (message) => replies.push(message));
|
|
141
|
+
const activeExit = once(active, "exit");
|
|
142
|
+
const queuedExit = once(queued, "exit");
|
|
143
|
+
await Promise.resolve();
|
|
144
|
+
|
|
145
|
+
// Invoke only the registry's callback: emitting process exit would also
|
|
146
|
+
// tear down Bun's test runner before it could check the queued lease.
|
|
147
|
+
const abortAll = process.listeners("exit").find((listener) => listener.name === "abortAll");
|
|
148
|
+
expect(abortAll).toBeDefined();
|
|
149
|
+
abortAll!();
|
|
150
|
+
|
|
151
|
+
await activeExit;
|
|
152
|
+
await queuedExit;
|
|
153
|
+
expect(replies).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a file ending with queued jobs cannot interrupt the next parallel file", () => {
|
|
157
|
+
withTestTempDir("reusable-worker-files-", (dir) => {
|
|
158
|
+
const outcomes = join(dir, "outcomes.jsonl");
|
|
159
|
+
writeFileSync(outcomes, "");
|
|
160
|
+
writeFileSync(
|
|
161
|
+
join(dir, "01-owner.test.ts"),
|
|
162
|
+
`import { test } from "bun:test";
|
|
163
|
+
import { appendFileSync } from "node:fs";
|
|
164
|
+
import { reuseWorker } from ${JSON.stringify(new URL("./reusable-worker.ts", import.meta.url).href)};
|
|
165
|
+
test("owner finishes with queued work", async () => {
|
|
166
|
+
const entry = new URL(${JSON.stringify(entry.href)});
|
|
167
|
+
const active = reuseWorker(entry, { workerData: { path: "file-exit", hang: true } });
|
|
168
|
+
active.on("error", () => {});
|
|
169
|
+
for (let id = 0; id < 3; id++) {
|
|
170
|
+
const queued = reuseWorker(entry, { workerData: { path: "file-exit", value: "queued" } });
|
|
171
|
+
let replied = false;
|
|
172
|
+
let failed = false;
|
|
173
|
+
queued.on("message", () => { replied = true; });
|
|
174
|
+
queued.on("error", () => { failed = true; });
|
|
175
|
+
queued.on("exit", code => appendFileSync(${JSON.stringify(outcomes)}, JSON.stringify({
|
|
176
|
+
id, outcome: code === 0 && replied && !failed ? "completed" : "failed",
|
|
177
|
+
}) + "\\n"));
|
|
178
|
+
}
|
|
179
|
+
await Promise.resolve();
|
|
180
|
+
});`,
|
|
181
|
+
);
|
|
182
|
+
writeFileSync(
|
|
183
|
+
join(dir, "02-next.test.ts"),
|
|
184
|
+
`import { describe, expect, test } from "bun:test";
|
|
185
|
+
import { readFileSync } from "node:fs";
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
187
|
+
describe("next file", () => {
|
|
188
|
+
test("still registers after every queued client settles once", () => {
|
|
189
|
+
const outcomes = readFileSync(${JSON.stringify(outcomes)}, "utf8").trim().split("\\n").filter(Boolean).map(line => JSON.parse(line));
|
|
190
|
+
expect(outcomes).toHaveLength(3);
|
|
191
|
+
for (let id = 0; id < 3; id++) {
|
|
192
|
+
const client = outcomes.filter(outcome => outcome.id === id);
|
|
193
|
+
expect(client).toHaveLength(1);
|
|
194
|
+
expect(["completed", "failed"]).toContain(client[0].outcome);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});`,
|
|
198
|
+
);
|
|
199
|
+
// The real preload owns file teardown. Bun may discard a bare file's
|
|
200
|
+
// callbacks without notifying its workers' clients during VM teardown.
|
|
201
|
+
const child = spawnSync(
|
|
202
|
+
process.execPath,
|
|
203
|
+
[
|
|
204
|
+
"test",
|
|
205
|
+
"--parallel=1",
|
|
206
|
+
"--preload",
|
|
207
|
+
fileURLToPath(new URL("../../test-preload.ts", import.meta.url)),
|
|
208
|
+
dir,
|
|
209
|
+
],
|
|
210
|
+
{ cwd: dir, encoding: "utf8", timeout: 5_000, windowsHide: true },
|
|
211
|
+
);
|
|
212
|
+
const output = child.stdout + child.stderr;
|
|
213
|
+
expect(output).toContain("2 pass");
|
|
214
|
+
expect(output).toContain("0 fail");
|
|
215
|
+
expect(output).not.toContain("Unhandled error");
|
|
216
|
+
expect(child.status).toBe(0);
|
|
217
|
+
const settled = readFileSync(outcomes, "utf8").trim().split("\n").filter(Boolean);
|
|
218
|
+
expect(settled).toHaveLength(3);
|
|
219
|
+
});
|
|
220
|
+
}, 10_000);
|
|
221
|
+
|
|
222
|
+
test("replacement construction failure settles every queued client exactly once", async () => {
|
|
223
|
+
const path = `replacement-failure-${++key}`;
|
|
224
|
+
const { port1, port2 } = new MessageChannel();
|
|
225
|
+
try {
|
|
226
|
+
const crashed = reuseWorker(entry, {
|
|
227
|
+
workerData: { path, crash: true },
|
|
228
|
+
transferList: [port1],
|
|
229
|
+
});
|
|
230
|
+
crashed.on("error", () => {});
|
|
231
|
+
const clients = [start(path), start(path)].map((worker) => {
|
|
232
|
+
const errors: unknown[] = [];
|
|
233
|
+
const exits: number[] = [];
|
|
234
|
+
worker.on("error", (error) => errors.push(error));
|
|
235
|
+
const done = new Promise<void>((resolve) =>
|
|
236
|
+
worker.on("exit", (code) => {
|
|
237
|
+
exits.push(code);
|
|
238
|
+
resolve();
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
return { errors, exits, done };
|
|
242
|
+
});
|
|
243
|
+
await Promise.all(clients.map((client) => client.done));
|
|
244
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
245
|
+
for (const client of clients) {
|
|
246
|
+
expect(client.errors).toHaveLength(1);
|
|
247
|
+
expect(client.exits).toEqual([1]);
|
|
248
|
+
}
|
|
249
|
+
} finally {
|
|
250
|
+
port2.close();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import * as timers from "node:timers";
|
|
3
|
+
import {
|
|
4
|
+
MessageChannel,
|
|
5
|
+
receiveMessageOnPort,
|
|
6
|
+
Worker,
|
|
7
|
+
type WorkerOptions,
|
|
8
|
+
} from "node:worker_threads";
|
|
9
|
+
import { registerExitAbort, unregisterExitAbort } from "./exit-abort-registry";
|
|
10
|
+
import { inRootAsyncContext } from "./sqlite";
|
|
11
|
+
|
|
12
|
+
const pools = new Map<string, Slot>();
|
|
13
|
+
const liveSlots = new Set<Slot>();
|
|
14
|
+
let nextId = 0;
|
|
15
|
+
const IDLE_MS = 60_000;
|
|
16
|
+
const sharedCloneWorks = (() => {
|
|
17
|
+
const source = new SharedArrayBuffer(1);
|
|
18
|
+
const clone = structuredClone(source);
|
|
19
|
+
new Uint8Array(source)[0] = 1;
|
|
20
|
+
return new Uint8Array(clone)[0] === 1;
|
|
21
|
+
})();
|
|
22
|
+
let cloneChannel: MessageChannel | undefined;
|
|
23
|
+
|
|
24
|
+
function cloneJobValue<T>(value: T, transferList?: readonly object[]): T {
|
|
25
|
+
if (sharedCloneWorks) return structuredClone(value, { transfer: transferList as never });
|
|
26
|
+
// Bun 1.3's structuredClone snapshots shared buffers, but native message
|
|
27
|
+
// transport preserves them. One reusable channel keeps cancellation and
|
|
28
|
+
// writer coordination shared without allocating ports for every job.
|
|
29
|
+
if (!cloneChannel) {
|
|
30
|
+
cloneChannel = inRootAsyncContext(() => new MessageChannel());
|
|
31
|
+
cloneChannel.port1.unref();
|
|
32
|
+
cloneChannel.port2.unref();
|
|
33
|
+
}
|
|
34
|
+
cloneChannel.port1.postMessage(value, transferList as never);
|
|
35
|
+
const packet = receiveMessageOnPort(cloneChannel.port2);
|
|
36
|
+
if (!packet) throw new Error("worker input clone did not arrive synchronously");
|
|
37
|
+
return packet.message as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Slot {
|
|
41
|
+
worker: Worker;
|
|
42
|
+
controller: AbortController;
|
|
43
|
+
queue: Lease[];
|
|
44
|
+
active?: Lease;
|
|
45
|
+
idle?: ReturnType<typeof timers.setTimeout>;
|
|
46
|
+
advance(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class Lease extends EventEmitter {
|
|
50
|
+
readonly id = ++nextId;
|
|
51
|
+
ended = false;
|
|
52
|
+
referenced = true;
|
|
53
|
+
readonly outbox: Array<{ value: unknown; transferList?: readonly object[] }> = [];
|
|
54
|
+
constructor(
|
|
55
|
+
public slot: Slot,
|
|
56
|
+
readonly data: unknown,
|
|
57
|
+
) {
|
|
58
|
+
super();
|
|
59
|
+
}
|
|
60
|
+
postMessage(value: unknown, transferList?: readonly object[]) {
|
|
61
|
+
if (this.ended) return;
|
|
62
|
+
if (this.slot.active === this)
|
|
63
|
+
this.slot.worker.postMessage({ jobId: this.id, value }, transferList as never);
|
|
64
|
+
else this.outbox.push(cloneJobValue({ value, transferList }, transferList));
|
|
65
|
+
}
|
|
66
|
+
start() {
|
|
67
|
+
if (this.referenced) this.slot.worker.ref();
|
|
68
|
+
else this.slot.worker.unref();
|
|
69
|
+
this.emit("online");
|
|
70
|
+
this.slot.worker.postMessage({ jobId: this.id, start: this.data });
|
|
71
|
+
for (const { value, transferList } of this.outbox.splice(0))
|
|
72
|
+
this.postMessage(value, transferList);
|
|
73
|
+
}
|
|
74
|
+
ref() {
|
|
75
|
+
this.referenced = true;
|
|
76
|
+
if (this.slot.active === this) this.slot.worker.ref();
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
unref() {
|
|
80
|
+
this.referenced = false;
|
|
81
|
+
if (this.slot.active === this) this.slot.worker.unref();
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
async terminate(): Promise<number> {
|
|
85
|
+
if (this.ended) return 0;
|
|
86
|
+
if (this.slot.active === this) return await this.slot.worker.terminate();
|
|
87
|
+
this.ended = true;
|
|
88
|
+
this.slot.queue.splice(this.slot.queue.indexOf(this), 1);
|
|
89
|
+
this.emit("exit", 0);
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A job-local Worker interface; only the idle thread, never job state, is reused. */
|
|
95
|
+
export function reuseWorker(entry: URL | string, options: WorkerOptions, idleMs = IDLE_MS): Worker {
|
|
96
|
+
// Native Worker construction copies its input immediately, even if the job
|
|
97
|
+
// cannot start yet. Do not let a queued job observe later caller mutations.
|
|
98
|
+
const data = cloneJobValue(options.workerData);
|
|
99
|
+
const slot = acquireSlot(entry, options, idleMs);
|
|
100
|
+
const lease = new Lease(slot, data);
|
|
101
|
+
enqueue(slot, lease);
|
|
102
|
+
return lease as unknown as Worker;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** End test-owned pools before another test can replace their timers or databases. */
|
|
106
|
+
export async function resetReusableWorkersForTests(): Promise<void> {
|
|
107
|
+
while (liveSlots.size) {
|
|
108
|
+
const slots = [...liveSlots];
|
|
109
|
+
pools.clear();
|
|
110
|
+
await Promise.all(
|
|
111
|
+
slots.map(
|
|
112
|
+
(slot) =>
|
|
113
|
+
new Promise<void>((resolve) => {
|
|
114
|
+
timers.clearTimeout(slot.idle);
|
|
115
|
+
slot.idle = undefined;
|
|
116
|
+
slot.worker.once("exit", () => resolve());
|
|
117
|
+
slot.controller.abort();
|
|
118
|
+
}),
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
cloneChannel?.port1.close();
|
|
123
|
+
cloneChannel?.port2.close();
|
|
124
|
+
cloneChannel = undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function enqueue(slot: Slot, lease: Lease): void {
|
|
128
|
+
timers.clearTimeout(slot.idle);
|
|
129
|
+
lease.slot = slot;
|
|
130
|
+
slot.queue.push(lease);
|
|
131
|
+
// Let callers attach their listeners before a queued job starts.
|
|
132
|
+
queueMicrotask(() => slot.advance());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function acquireSlot(entry: URL | string, options: WorkerOptions, idleMs: number): Slot {
|
|
136
|
+
const data = options.workerData;
|
|
137
|
+
const key = JSON.stringify([
|
|
138
|
+
String(entry),
|
|
139
|
+
data?.path ?? data?.dbPath,
|
|
140
|
+
data?.harness,
|
|
141
|
+
data?.job,
|
|
142
|
+
]);
|
|
143
|
+
let slot = pools.get(key);
|
|
144
|
+
if (!slot) {
|
|
145
|
+
const worker = inRootAsyncContext(
|
|
146
|
+
() => new Worker(entry, { ...options, workerData: { reusableWorker: true } }),
|
|
147
|
+
);
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
slot = { worker, controller, queue: [], advance: () => {} };
|
|
150
|
+
pools.set(key, slot);
|
|
151
|
+
liveSlots.add(slot);
|
|
152
|
+
const owned = slot;
|
|
153
|
+
const advance = () => {
|
|
154
|
+
if (controller.signal.aborted || owned.active) return;
|
|
155
|
+
timers.clearTimeout(owned.idle);
|
|
156
|
+
const lease = owned.queue.shift();
|
|
157
|
+
owned.active = lease;
|
|
158
|
+
if (!lease) {
|
|
159
|
+
worker.unref();
|
|
160
|
+
// A caller's temporary clock must not own a shared pool's lifetime.
|
|
161
|
+
owned.idle = timers.setTimeout(() => {
|
|
162
|
+
if (pools.get(key) === owned) pools.delete(key);
|
|
163
|
+
void worker.terminate();
|
|
164
|
+
}, idleMs);
|
|
165
|
+
owned.idle.unref();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
lease.start();
|
|
169
|
+
};
|
|
170
|
+
owned.advance = advance;
|
|
171
|
+
worker.on(
|
|
172
|
+
"message",
|
|
173
|
+
(message: { jobId: number; value?: unknown; finished?: boolean; code?: number }) => {
|
|
174
|
+
const lease = owned.active;
|
|
175
|
+
if (!lease || message.jobId !== lease.id) return;
|
|
176
|
+
if (!message.finished) {
|
|
177
|
+
lease.emit("message", message.value);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
lease.ended = true;
|
|
181
|
+
owned.active = undefined;
|
|
182
|
+
if (message.code) {
|
|
183
|
+
if (pools.get(key) === owned) pools.delete(key);
|
|
184
|
+
lease.emit("exit", message.code);
|
|
185
|
+
void worker.terminate();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
lease.emit("exit", 0);
|
|
189
|
+
advance();
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
worker.on("error", (error) => {
|
|
193
|
+
if (pools.get(key) === owned) pools.delete(key);
|
|
194
|
+
owned.active?.emit("error", error);
|
|
195
|
+
});
|
|
196
|
+
worker.on("exit", (code) => {
|
|
197
|
+
liveSlots.delete(owned);
|
|
198
|
+
timers.clearTimeout(owned.idle);
|
|
199
|
+
if (pools.get(key) === owned) pools.delete(key);
|
|
200
|
+
const active = owned.active;
|
|
201
|
+
owned.active = undefined;
|
|
202
|
+
if (active && !active.ended) {
|
|
203
|
+
active.ended = true;
|
|
204
|
+
active.emit("exit", code);
|
|
205
|
+
}
|
|
206
|
+
// Only the active job hit the failed thread. Move unstarted jobs to
|
|
207
|
+
// its replacement without retrying any work that may have run.
|
|
208
|
+
// During process shutdown, release queued leases instead of
|
|
209
|
+
// spawning threads into a torn-down host.
|
|
210
|
+
for (const lease of owned.queue.splice(0)) {
|
|
211
|
+
if (controller.signal.aborted) {
|
|
212
|
+
lease.ended = true;
|
|
213
|
+
lease.emit("exit", code);
|
|
214
|
+
} else {
|
|
215
|
+
try {
|
|
216
|
+
enqueue(acquireSlot(entry, options, idleMs), lease);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Bun can destroy a test file's worker communication
|
|
219
|
+
// channel without firing process exit. Report a failed
|
|
220
|
+
// replacement through its lease rather than throwing
|
|
221
|
+
// from this exit callback between files.
|
|
222
|
+
lease.ended = true;
|
|
223
|
+
lease.emit("error", error);
|
|
224
|
+
lease.emit("exit", 1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
controller.signal.addEventListener(
|
|
230
|
+
"abort",
|
|
231
|
+
() => {
|
|
232
|
+
void worker.terminate();
|
|
233
|
+
},
|
|
234
|
+
{ once: true },
|
|
235
|
+
);
|
|
236
|
+
registerExitAbort(controller);
|
|
237
|
+
worker.once("exit", () => unregisterExitAbort(controller));
|
|
238
|
+
}
|
|
239
|
+
return slot;
|
|
240
|
+
}
|
|
@@ -48,6 +48,7 @@ export function isWorkerCloseMessage(message: unknown): boolean {
|
|
|
48
48
|
export function exitOnWorkerClose(
|
|
49
49
|
port: WorkerClosePort | null,
|
|
50
50
|
closeConnections: () => void,
|
|
51
|
+
exitThread = true,
|
|
51
52
|
): { closePort(): void } {
|
|
52
53
|
const listener = (message: unknown) => {
|
|
53
54
|
if (!isWorkerCloseMessage(message)) return;
|
|
@@ -56,27 +57,30 @@ export function exitOnWorkerClose(
|
|
|
56
57
|
} finally {
|
|
57
58
|
// Exit even if a close threw: the thread is ending and a connection
|
|
58
59
|
// that could not close is released by the thread's teardown instead.
|
|
59
|
-
process.exit(0);
|
|
60
|
+
if (exitThread) process.exit(0);
|
|
61
|
+
// A reusable job with no asynchronous work left releases only its
|
|
62
|
+
// job port, after closing the connection and removing this listener.
|
|
63
|
+
closePort();
|
|
60
64
|
}
|
|
61
65
|
};
|
|
62
66
|
if (!port) return { closePort() {} };
|
|
63
|
-
port
|
|
67
|
+
const owner = port;
|
|
68
|
+
owner.on("message", listener);
|
|
64
69
|
let closed = false;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
};
|
|
70
|
+
function closePort() {
|
|
71
|
+
if (closed) return;
|
|
72
|
+
closed = true;
|
|
73
|
+
// Close on a later timer turn, not synchronously: on Bun 1.3.14 a
|
|
74
|
+
// port closed while an entry module with a top-level await is still
|
|
75
|
+
// evaluating does not end the thread. Every current caller closes
|
|
76
|
+
// its port at the end of the module body or later, so the timer runs
|
|
77
|
+
// after evaluation has finished.
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
owner.off("message", listener);
|
|
80
|
+
owner.close();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return { closePort };
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
/** The parts of a worker's parent port that {@link exitOnWorkerClose} uses. */
|