@cotal-ai/lang 0.24.0 → 0.25.0
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 +11 -0
- package/dist/engine/bridge.d.ts +71 -0
- package/dist/engine/bridge.d.ts.map +1 -0
- package/dist/engine/bridge.js +277 -0
- package/dist/engine/bridge.js.map +1 -0
- package/dist/engine/ctx.d.ts +140 -0
- package/dist/engine/ctx.d.ts.map +1 -0
- package/dist/engine/ctx.js +834 -0
- package/dist/engine/ctx.js.map +1 -0
- package/dist/engine/frame.d.ts +69 -0
- package/dist/engine/frame.d.ts.map +1 -0
- package/dist/engine/frame.js +105 -0
- package/dist/engine/frame.js.map +1 -0
- package/dist/engine/host.d.ts +77 -0
- package/dist/engine/host.d.ts.map +1 -0
- package/dist/engine/host.js +134 -0
- package/dist/engine/host.js.map +1 -0
- package/dist/engine/worker-entry.d.ts +26 -0
- package/dist/engine/worker-entry.d.ts.map +1 -0
- package/dist/engine/worker-entry.js +175 -0
- package/dist/engine/worker-entry.js.map +1 -0
- package/dist/engine/worker.d.ts +156 -0
- package/dist/engine/worker.d.ts.map +1 -0
- package/dist/engine/worker.js +123 -0
- package/dist/engine/worker.js.map +1 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +97 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/interpret.d.ts +7 -38
- package/dist/interpret.d.ts.map +1 -1
- package/dist/interpret.js +45 -1003
- package/dist/interpret.js.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +75 -1
- package/dist/journal.js.map +1 -1
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +13 -1
- package/dist/library.js.map +1 -1
- package/dist/perform.d.ts +138 -0
- package/dist/perform.d.ts.map +1 -0
- package/dist/perform.js +1052 -0
- package/dist/perform.js.map +1 -0
- package/dist/pins.d.ts +28 -8
- package/dist/pins.d.ts.map +1 -1
- package/dist/pins.js +31 -11
- package/dist/pins.js.map +1 -1
- package/dist/sim.d.ts +15 -1
- package/dist/sim.d.ts.map +1 -1
- package/dist/sim.js.map +1 -1
- package/dist/transform/emit.d.ts +23 -0
- package/dist/transform/emit.d.ts.map +1 -0
- package/dist/transform/emit.js +934 -0
- package/dist/transform/emit.js.map +1 -0
- package/dist/transform/index.d.ts +34 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +31 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/transform/scope.d.ts +58 -0
- package/dist/transform/scope.d.ts.map +1 -0
- package/dist/transform/scope.js +500 -0
- package/dist/transform/scope.js.map +1 -0
- package/dist/transform/seam.d.ts +78 -0
- package/dist/transform/seam.d.ts.map +1 -0
- package/dist/transform/seam.js +111 -0
- package/dist/transform/seam.js.map +1 -0
- package/dist/values.d.ts +15 -0
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +79 -0
- package/dist/values.js.map +1 -1
- package/package.json +9 -4
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inside the thread: lockdown, one Compartment, and the run.
|
|
3
|
+
*
|
|
4
|
+
* THIS FILE IS THE CONFINEMENT. `lockdown()` runs ONCE, here, at module load and before anything of
|
|
5
|
+
* the run exists - it is irreversible and realm-wide, so it belongs at the top of a realm nobody
|
|
6
|
+
* else owns. Each run then gets a Compartment with ZERO ENDOWMENTS and the transform's module is
|
|
7
|
+
* evaluated in it; the context is the CALL ARGUMENT, not a global, so the program's
|
|
8
|
+
* `globalThis` is empty and the seam is a value it was handed rather than a name it can reach.
|
|
9
|
+
*
|
|
10
|
+
* MEASURED HERE, at ses@2.3.0 on node v26.7.0, rather than assumed:
|
|
11
|
+
* inside a Compartment `Date.now()` and `Math.random()` THROW in secure mode; `process` is
|
|
12
|
+
* undefined; `globalThis` has 0 own keys; and
|
|
13
|
+
* `(function(){}).constructor("return typeof process")()` throws
|
|
14
|
+
* "Function.prototype.constructor is not a valid constructor"
|
|
15
|
+
* confinement, not hiding `({}).constructor === Object` is still true, deliberately: the program
|
|
16
|
+
* shares the realm's intrinsics, it just cannot reach out of it
|
|
17
|
+
* the START compartment KEEPS Date.now, Math.random and process - which is what lets the host
|
|
18
|
+
* half of this thread (the journal, the effect path, the handler) work
|
|
19
|
+
* normally around a program that cannot touch any of them
|
|
20
|
+
*
|
|
21
|
+
* WHAT IS NOT HERE, on purpose: no wall-clock timeout, no `terminate()` on a deadline. A thread
|
|
22
|
+
* killed mid-effect leaves a journal saying a step is pending and nothing that can settle it. The
|
|
23
|
+
* step budget (L4013) and the shared stop flag are the only two things that end a run.
|
|
24
|
+
*/
|
|
25
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
26
|
+
import "ses";
|
|
27
|
+
import { Journal } from "../journal.js";
|
|
28
|
+
import { RuntimeFault } from "../errors.js";
|
|
29
|
+
import { assertCrossable } from "../values.js";
|
|
30
|
+
import { EffectError } from "../effects.js";
|
|
31
|
+
import { bridgedSeam } from "./bridge.js";
|
|
32
|
+
import { runOnEngine } from "./host.js";
|
|
33
|
+
// ONCE PER THREAD, and before the run exists.
|
|
34
|
+
lockdown();
|
|
35
|
+
if (parentPort === null)
|
|
36
|
+
throw new Error("cotal-lang engine worker: no parent port; this module is a worker entry, not a library");
|
|
37
|
+
const port = parentPort;
|
|
38
|
+
const { request, stop, bridge } = workerData;
|
|
39
|
+
/**
|
|
40
|
+
* The run's cancellation, read where it has always been read.
|
|
41
|
+
*
|
|
42
|
+
* Synchronous by necessity - the effect path asks between effects - which is why it is shared memory
|
|
43
|
+
* rather than a message. The length is loaded atomically because the writer publishes it last.
|
|
44
|
+
*/
|
|
45
|
+
const stopLength = new Int32Array(stop, 0, 1);
|
|
46
|
+
const stopBytes = new Uint8Array(stop, 4);
|
|
47
|
+
const shouldStop = () => {
|
|
48
|
+
const n = Atomics.load(stopLength, 0);
|
|
49
|
+
// `.slice` copies out of shared memory: a decoder is not handed a buffer that can change under it.
|
|
50
|
+
return n > 0 ? new TextDecoder().decode(stopBytes.slice(0, n)) : undefined;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The confined evaluator: a Compartment per run, with nothing in it.
|
|
54
|
+
*
|
|
55
|
+
* `runOnEngine` takes this rather than defaulting to one, and that is the security posture rather
|
|
56
|
+
* than an inconvenience - turning a module string into a function is exactly where confinement is
|
|
57
|
+
* applied or lost, so the caller says which it is and a reviewer can see it.
|
|
58
|
+
*/
|
|
59
|
+
const confined = (module) => {
|
|
60
|
+
const compartment = new Compartment();
|
|
61
|
+
const factory = compartment.evaluate(module);
|
|
62
|
+
if (typeof factory !== "function") {
|
|
63
|
+
throw new RuntimeFault("L1000", `the transformed module must evaluate to a function taking the context, and this one evaluated to ${typeof factory}`);
|
|
64
|
+
}
|
|
65
|
+
return factory;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The seam this run performs against, by the route the request chose.
|
|
69
|
+
*
|
|
70
|
+
* `"bridged"`: the handler and the durable store live in the HOST, and both halves ride the port
|
|
71
|
+
* this thread was handed (`bridge.ts`); the journal is built HERE, over the recorded entries and
|
|
72
|
+
* that store, so every append is durable in the host before the effect it precedes fires. A
|
|
73
|
+
* module-named handler keeps its whole path in the thread, journal and all, as it always has.
|
|
74
|
+
*/
|
|
75
|
+
async function buildSeam() {
|
|
76
|
+
if (request.handler === "bridged") {
|
|
77
|
+
if (bridge === undefined) {
|
|
78
|
+
throw new RuntimeFault("L1000", `request ${request.runId} names the bridged handler route but the thread was started without a bridge port; runInWorker is the caller that wires one`);
|
|
79
|
+
}
|
|
80
|
+
const seam = bridgedSeam(bridge.port, bridge.clock);
|
|
81
|
+
return {
|
|
82
|
+
handler: seam.handler,
|
|
83
|
+
journal: new Journal({ run: request.runId, entries: request.entries ?? [], store: seam.store }),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
handler: await buildHandler(request.handler.module, request.handler.export, request.handler.config),
|
|
88
|
+
// Exactly the shape this route always had: a journal only when there are entries to resume,
|
|
89
|
+
// and the fresh case left to the engine's own default.
|
|
90
|
+
...(request.entries !== undefined ? { journal: new Journal({ run: request.runId, entries: request.entries }) } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function buildHandler(module, exportName, config) {
|
|
94
|
+
const mod = (await import(module));
|
|
95
|
+
const name = exportName ?? "createHandler";
|
|
96
|
+
const make = mod[name];
|
|
97
|
+
if (typeof make !== "function") {
|
|
98
|
+
throw new RuntimeFault("L1000", `${module} has no \`${name}\` export that is a function. A handler is not serialisable - it holds sockets, a client and a clock - so the request names a module and the thread builds it: that export takes the config and answers an EffectHandler.`);
|
|
99
|
+
}
|
|
100
|
+
return make(config);
|
|
101
|
+
}
|
|
102
|
+
async function run() {
|
|
103
|
+
const seam = await buildSeam();
|
|
104
|
+
const result = await runOnEngine(request.source, request.module, {
|
|
105
|
+
runId: request.runId,
|
|
106
|
+
handler: seam.handler,
|
|
107
|
+
evaluate: confined,
|
|
108
|
+
shouldStop,
|
|
109
|
+
...(request.file !== undefined ? { file: request.file } : {}),
|
|
110
|
+
...(request.pins !== undefined ? { pins: request.pins } : {}),
|
|
111
|
+
...(seam.journal !== undefined ? { journal: seam.journal } : {}),
|
|
112
|
+
...(request.seed !== undefined ? { seed: request.seed } : {}),
|
|
113
|
+
...(request.effectCeiling !== undefined ? { effectCeiling: request.effectCeiling } : {}),
|
|
114
|
+
...(request.stepBudget !== undefined ? { stepBudget: request.stepBudget } : {}),
|
|
115
|
+
onLog: (line) => port.postMessage({ kind: "log", line: { scope: line.scope, values: [...line.values] } }),
|
|
116
|
+
});
|
|
117
|
+
// THE RUN'S VALUE CROSSES A BOUNDARY, so it answers to the language's own crossing rule rather
|
|
118
|
+
// than to the structured-clone algorithm's. A function reaching this line would otherwise fail as
|
|
119
|
+
// a DataCloneError naming a host algorithm, when what happened is that a run tried to return
|
|
120
|
+
// something that cannot be recorded.
|
|
121
|
+
//
|
|
122
|
+
// ABSENCE IS NOT A VALUE and is not put through a rule about values: a program whose last line is
|
|
123
|
+
// a statement ends with no value at all, and the walker reports exactly that, so asserting here
|
|
124
|
+
// would refuse the ordinary case. Measured: the first worker cell written refused its own fixture
|
|
125
|
+
// with `undefined ... Use null when you mean "no value"` before this line said so.
|
|
126
|
+
if (result.value !== undefined)
|
|
127
|
+
assertCrossable(result.value, "the value this run returned");
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
value: result.value,
|
|
131
|
+
entries: result.journal.entries(),
|
|
132
|
+
pins: result.pins,
|
|
133
|
+
programHash: result.programHash,
|
|
134
|
+
steps: result.steps,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** The answer a thread owes when it cannot give the one it was asked for. */
|
|
138
|
+
const answerWith = (e) => {
|
|
139
|
+
const err = e;
|
|
140
|
+
port.postMessage({
|
|
141
|
+
kind: "result",
|
|
142
|
+
result: {
|
|
143
|
+
ok: false,
|
|
144
|
+
...(typeof err?.code === "string" ? { code: err.code } : {}),
|
|
145
|
+
name: typeof err?.name === "string" ? err.name : "Error",
|
|
146
|
+
message: typeof err?.message === "string" ? err.message : String(e),
|
|
147
|
+
// A release's reason is a field on the class (L5012), and an EffectError's kind and detail
|
|
148
|
+
// are its domain; each crosses as the field it is. See WorkerRunFailed.
|
|
149
|
+
...(typeof err?.reason === "string" ? { reason: err.reason } : {}),
|
|
150
|
+
...(typeof err?.kind === "string" ? { kind: err.kind } : {}),
|
|
151
|
+
...(err?.detail !== undefined && e instanceof EffectError ? { detail: err.detail } : {}),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
// `.catch` AFTER `.then(f, g)`, AND THAT IS THE WHOLE POINT: `g` is `f`'s SIBLING, not its handler,
|
|
156
|
+
// so a throw inside `f` was not caught by it. The result post is the throw that matters, because it
|
|
157
|
+
// structured-clones the whole journal, and when it threw, this thread died on an unhandled
|
|
158
|
+
// rejection and exited 0 without answering. The host could then say only "exited before the run
|
|
159
|
+
// answered", which is the wrong cause every time, and MEASURED: a handler binding a function
|
|
160
|
+
// produced exactly that, a DataCloneError naming a host algorithm reported as a silent exit.
|
|
161
|
+
//
|
|
162
|
+
// A BACKSTOP, NOT A DEAD BRANCH, and the difference is worth stating because this comment once
|
|
163
|
+
// claimed the stronger thing. Every value that crosses back is fenced at its own write site:
|
|
164
|
+
// `value` by `assertCrossable` above, `external` and a failure's `detail` by the guards in
|
|
165
|
+
// perform.ts, an effect's `result` at its settle, and a SCOPE's settled value at its own settle.
|
|
166
|
+
// `pins`, `programHash` and `steps` are primitives.
|
|
167
|
+
//
|
|
168
|
+
// THAT LIST IS AN ENUMERATION, so it is true of the fields it names and silent about any other. It
|
|
169
|
+
// was WRONG once, and not hypothetically: this comment claimed the route was unreachable while a
|
|
170
|
+
// scope's settled value crossed unfenced, so a legal program whose branch returned a closure
|
|
171
|
+
// completed on the walker and answered a DataCloneError through a thread. The fence for that landed
|
|
172
|
+
// with the cell that measures it. What survives is the honest reading: this line names no known
|
|
173
|
+
// route at this sha, and it is what makes the day a new field is added LOUD instead of silent.
|
|
174
|
+
run().then((result) => port.postMessage({ kind: "result", result }), answerWith).catch(answerWith);
|
|
175
|
+
//# sourceMappingURL=worker-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-entry.js","sourceRoot":"","sources":["../../src/engine/worker-entry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One worker thread per run, and the boundary a run's request and answer cross.
|
|
3
|
+
*
|
|
4
|
+
* WHY A THREAD AT ALL. `lockdown()` is irreversible and realm-wide: it hardens the intrinsics of the
|
|
5
|
+
* whole isolate, and a host process that also serves a mesh client, a journal store and a CLI cannot
|
|
6
|
+
* accept that on its own behalf. A worker gives the run its own realm to harden, and gives the host
|
|
7
|
+
* an isolate whose intrinsics it still owns.
|
|
8
|
+
*
|
|
9
|
+
* WHAT ACTUALLY CROSSES. The run happens INSIDE the thread - the seam, the journal and the effect
|
|
10
|
+
* host - and the request comes in, the result goes out, with log lines streamed between. The
|
|
11
|
+
* HANDLER has two routes, chosen by what the handler is. One that can be built from cloneable
|
|
12
|
+
* config is named BY MODULE and constructed in the thread, whole. One that cannot - a live object
|
|
13
|
+
* holding sockets, a mesh client, a lease-bound appender - STAYS IN THE HOST, and the thread
|
|
14
|
+
* forwards the effect seam over a MessagePort (`bridge.ts`): every `EffectHandler` member except
|
|
15
|
+
* `now()` is async, and the journal's durable half (`JournalStore.append`) is a Promise the
|
|
16
|
+
* journal awaits before any effect fires, so both survive a port hop with the awaits lining up
|
|
17
|
+
* exactly as they line up over a PubAck. What is genuinely synchronous is `now()` and the stop
|
|
18
|
+
* flag, and both go over shared memory. An earlier form of this header ruled the bridge out by
|
|
19
|
+
* claiming "every journal.* call in the effect path" is synchronous; the durable append never was,
|
|
20
|
+
* and the in-memory reads that are never leave the thread.
|
|
21
|
+
*
|
|
22
|
+
* CANCELLATION IS THE ONE EXCEPTION, and it is why there is a SharedArrayBuffer here. `shouldStop`
|
|
23
|
+
* is read synchronously, between effects, so it cannot be a message either. The host writes a reason
|
|
24
|
+
* into shared memory and the run reads it where it always read it. There is NO wall-clock timeout
|
|
25
|
+
* and no `terminate()` on a deadline: a thread killed mid-effect leaves the journal saying a step is
|
|
26
|
+
* pending forever, and the step budget plus this flag are the only two things that end a run.
|
|
27
|
+
*
|
|
28
|
+
* Measured on this floor (node v26.7.0, ses@2.3.0): lockdown 4.5ms, ONCE per thread; a Compartment
|
|
29
|
+
* after it 0.03ms, so the per-run cost is the thread, not the confinement; cold start to first
|
|
30
|
+
* message, lockdown included, 22.8-26.4ms over three spawns.
|
|
31
|
+
*/
|
|
32
|
+
import type { EffectHandler } from "../effects.js";
|
|
33
|
+
import type { JournalEntry, JournalStore } from "./../journal.js";
|
|
34
|
+
import type { RunPins } from "../pins.js";
|
|
35
|
+
export interface WorkerHandlerSpec {
|
|
36
|
+
/**
|
|
37
|
+
* The module to import INSIDE the thread. An absolute `file:` URL or a bare package specifier.
|
|
38
|
+
*
|
|
39
|
+
* A handler is not serialisable and must not be: it holds sockets, a mesh client and a clock. What
|
|
40
|
+
* crosses is its NAME.
|
|
41
|
+
*/
|
|
42
|
+
readonly module: string;
|
|
43
|
+
/** The export to call. It takes `config` and answers an `EffectHandler`. */
|
|
44
|
+
readonly export?: string;
|
|
45
|
+
/** Handed to that export, verbatim. Must be structured-cloneable, being all that crosses. */
|
|
46
|
+
readonly config?: unknown;
|
|
47
|
+
}
|
|
48
|
+
export interface WorkerRunRequest {
|
|
49
|
+
readonly source: string;
|
|
50
|
+
readonly module: string;
|
|
51
|
+
readonly runId: string;
|
|
52
|
+
/**
|
|
53
|
+
* `"bridged"` keeps the handler in the host: the thread forwards the effect seam over a
|
|
54
|
+
* MessagePort instead of constructing a handler, and the caller supplies the live handler and
|
|
55
|
+
* store via {@link WorkerRunOptions.bridge}. Exactly one of the two must be chosen — a request
|
|
56
|
+
* naming both routes, or neither, is a caller that has not decided where its effects run.
|
|
57
|
+
*/
|
|
58
|
+
readonly handler: WorkerHandlerSpec | "bridged";
|
|
59
|
+
readonly pins?: RunPins;
|
|
60
|
+
/** A resume: the recorded entries, rebuilt into the run's journal inside the thread. */
|
|
61
|
+
readonly entries?: readonly JournalEntry[];
|
|
62
|
+
readonly file?: string;
|
|
63
|
+
/**
|
|
64
|
+
* The caller's loose limits and seed, forwarded so the thread's `bindPins` performs the same
|
|
65
|
+
* agreement check (L5009) the in-process engines perform: with pins present these must agree or
|
|
66
|
+
* be absent, and dropping them at this boundary would silently skip that refusal.
|
|
67
|
+
*/
|
|
68
|
+
readonly seed?: string;
|
|
69
|
+
readonly effectCeiling?: number;
|
|
70
|
+
readonly stepBudget?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface WorkerRunOk {
|
|
73
|
+
readonly ok: true;
|
|
74
|
+
readonly value: unknown;
|
|
75
|
+
readonly entries: readonly JournalEntry[];
|
|
76
|
+
readonly pins: RunPins;
|
|
77
|
+
readonly programHash: string;
|
|
78
|
+
readonly steps: number;
|
|
79
|
+
}
|
|
80
|
+
export interface WorkerRunFailed {
|
|
81
|
+
readonly ok: false;
|
|
82
|
+
/** The language code where there is one (`L4013`, `L5011`), so a caller can branch as it always has. */
|
|
83
|
+
readonly code?: string;
|
|
84
|
+
readonly name: string;
|
|
85
|
+
readonly message: string;
|
|
86
|
+
/**
|
|
87
|
+
* `RunReleased.reason` (L5012), carried as the field it is so a host rebuilding the class does
|
|
88
|
+
* not have to parse its own sentence back out of the message.
|
|
89
|
+
*/
|
|
90
|
+
readonly reason?: string;
|
|
91
|
+
/**
|
|
92
|
+
* An `EffectError`'s domain fields, carried so a host can rebuild the class whole: `kind` is what
|
|
93
|
+
* failure handling branches on and `detail` is a recorded value, already fenced at its throw
|
|
94
|
+
* site. Present together with `code` exactly when the run failed as an effect failure.
|
|
95
|
+
*/
|
|
96
|
+
readonly kind?: string;
|
|
97
|
+
readonly detail?: Readonly<Record<string, unknown>>;
|
|
98
|
+
}
|
|
99
|
+
export type WorkerRunResult = WorkerRunOk | WorkerRunFailed;
|
|
100
|
+
export interface WorkerRun {
|
|
101
|
+
/** The run's answer. It rejects only for a worker that died without one. */
|
|
102
|
+
readonly done: Promise<WorkerRunResult>;
|
|
103
|
+
/**
|
|
104
|
+
* Ask the run to stop, with a reason.
|
|
105
|
+
*
|
|
106
|
+
* It is a request, not a kill: the run reads it between effects and at a fuel yield, finishes what
|
|
107
|
+
* is already in flight, and ends through its own cancellation path with the journal consistent.
|
|
108
|
+
*/
|
|
109
|
+
stop(reason: string): void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* THE WORKER ENTRY IS AN INPUT, not something this module derives.
|
|
113
|
+
*
|
|
114
|
+
* A thread's entry is a FILE ON DISK, and which file that is depends on how the caller was built and
|
|
115
|
+
* installed - not on anything this module can see about itself. From the compiled package it is
|
|
116
|
+
* `new URL("./worker-entry.js", import.meta.url)` resolved in the compiled module; a suite that
|
|
117
|
+
* wants to grade the SHIPPED artifact names that artifact; a bundler that rewrote the layout names
|
|
118
|
+
* what it produced. So the composition root passes it, and this module makes no guess.
|
|
119
|
+
*
|
|
120
|
+
* It was derived here, and the derivation was measurably wrong. Running from TypeScript sources,
|
|
121
|
+
* node 22 does not apply the parent's ESM loader hooks to a worker thread: a `.js` entry is
|
|
122
|
+
* ERR_MODULE_NOT_FOUND, and the `.ts` entry gets exactly one step further before dying on its own
|
|
123
|
+
* `../journal.js`. Inherited execArgv already carries tsx's `--import` into the thread and an
|
|
124
|
+
* explicit `execArgv: ["--import", "tsx"]` adds it again; neither changes the answer. No spelling
|
|
125
|
+
* this module could pick makes a `.ts` tree runnable in a thread on that node - the answer is to run
|
|
126
|
+
* the BUILD, which is a fact about the caller. Making the entry an argument is what lets the caller
|
|
127
|
+
* say so, and it is why nothing here is version-conditional.
|
|
128
|
+
*/
|
|
129
|
+
export interface WorkerRunOptions {
|
|
130
|
+
/**
|
|
131
|
+
* The module the thread starts at: `engine/worker-entry`, in whatever build the caller is running.
|
|
132
|
+
* A `file:` URL or an absolute path.
|
|
133
|
+
*/
|
|
134
|
+
readonly entry: URL | string;
|
|
135
|
+
/** Called for each `log` the run emits, as it emits it. */
|
|
136
|
+
readonly onLog?: (line: {
|
|
137
|
+
scope: string;
|
|
138
|
+
values: readonly unknown[];
|
|
139
|
+
}) => void;
|
|
140
|
+
/**
|
|
141
|
+
* The live seam a `"bridged"` request runs against: the host's handler and its durable store.
|
|
142
|
+
* Required exactly when the request says `"bridged"`; see {@link WorkerRunRequest.handler}.
|
|
143
|
+
*/
|
|
144
|
+
readonly bridge?: {
|
|
145
|
+
readonly handler: EffectHandler;
|
|
146
|
+
readonly store: JournalStore;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Run a transformed program in its own locked-down thread.
|
|
151
|
+
*
|
|
152
|
+
* The worker is terminated when the run answers, however it answers: one thread per run is the whole
|
|
153
|
+
* point, and a thread that outlives its run is a realm holding a journal nobody is reading.
|
|
154
|
+
*/
|
|
155
|
+
export declare function runInWorker(request: WorkerRunRequest, options: WorkerRunOptions): WorkerRun;
|
|
156
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/engine/worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One worker thread per run, and the boundary a run's request and answer cross.
|
|
3
|
+
*
|
|
4
|
+
* WHY A THREAD AT ALL. `lockdown()` is irreversible and realm-wide: it hardens the intrinsics of the
|
|
5
|
+
* whole isolate, and a host process that also serves a mesh client, a journal store and a CLI cannot
|
|
6
|
+
* accept that on its own behalf. A worker gives the run its own realm to harden, and gives the host
|
|
7
|
+
* an isolate whose intrinsics it still owns.
|
|
8
|
+
*
|
|
9
|
+
* WHAT ACTUALLY CROSSES. The run happens INSIDE the thread - the seam, the journal and the effect
|
|
10
|
+
* host - and the request comes in, the result goes out, with log lines streamed between. The
|
|
11
|
+
* HANDLER has two routes, chosen by what the handler is. One that can be built from cloneable
|
|
12
|
+
* config is named BY MODULE and constructed in the thread, whole. One that cannot - a live object
|
|
13
|
+
* holding sockets, a mesh client, a lease-bound appender - STAYS IN THE HOST, and the thread
|
|
14
|
+
* forwards the effect seam over a MessagePort (`bridge.ts`): every `EffectHandler` member except
|
|
15
|
+
* `now()` is async, and the journal's durable half (`JournalStore.append`) is a Promise the
|
|
16
|
+
* journal awaits before any effect fires, so both survive a port hop with the awaits lining up
|
|
17
|
+
* exactly as they line up over a PubAck. What is genuinely synchronous is `now()` and the stop
|
|
18
|
+
* flag, and both go over shared memory. An earlier form of this header ruled the bridge out by
|
|
19
|
+
* claiming "every journal.* call in the effect path" is synchronous; the durable append never was,
|
|
20
|
+
* and the in-memory reads that are never leave the thread.
|
|
21
|
+
*
|
|
22
|
+
* CANCELLATION IS THE ONE EXCEPTION, and it is why there is a SharedArrayBuffer here. `shouldStop`
|
|
23
|
+
* is read synchronously, between effects, so it cannot be a message either. The host writes a reason
|
|
24
|
+
* into shared memory and the run reads it where it always read it. There is NO wall-clock timeout
|
|
25
|
+
* and no `terminate()` on a deadline: a thread killed mid-effect leaves the journal saying a step is
|
|
26
|
+
* pending forever, and the step budget plus this flag are the only two things that end a run.
|
|
27
|
+
*
|
|
28
|
+
* Measured on this floor (node v26.7.0, ses@2.3.0): lockdown 4.5ms, ONCE per thread; a Compartment
|
|
29
|
+
* after it 0.03ms, so the per-run cost is the thread, not the confinement; cold start to first
|
|
30
|
+
* message, lockdown included, 22.8-26.4ms over three spawns.
|
|
31
|
+
*/
|
|
32
|
+
import { MessageChannel, Worker } from "node:worker_threads";
|
|
33
|
+
import { serviceBridge } from "./bridge.js";
|
|
34
|
+
/** Where the reason's byte length lives in the shared stop buffer; the bytes follow it. */
|
|
35
|
+
const STOP_HEADER = 4;
|
|
36
|
+
/** Room for the reason. A reason is a sentence, and one that does not fit is truncated, not dropped. */
|
|
37
|
+
const STOP_CAPACITY = 512;
|
|
38
|
+
/**
|
|
39
|
+
* What a stop with nothing to say arrives as.
|
|
40
|
+
*
|
|
41
|
+
* The length is the publish signal, so a zero-length reason would be no stop at all; the previous
|
|
42
|
+
* spelling floored it at 1 and the run read the one byte that happened to be there. MEASURED through
|
|
43
|
+
* the real thread: `stop("")` ended the run with a sentence whose last character was a NUL. A stop is
|
|
44
|
+
* an operator's act and it may not arrive as a control character, so a reason with no bytes is given
|
|
45
|
+
* this one - a sentence, and true.
|
|
46
|
+
*/
|
|
47
|
+
const NO_REASON = "the operator asked this run to stop and gave no reason";
|
|
48
|
+
/** Write a stop reason into shared memory: the bytes first, then the length that publishes them. */
|
|
49
|
+
function publishStop(buffer, reason) {
|
|
50
|
+
// `encodeInto`, NOT encode-then-slice: it fills the room with WHOLE code points and reports how
|
|
51
|
+
// many bytes that took, so a reason too long for the buffer is cut BETWEEN characters instead of
|
|
52
|
+
// through one. Measured through the real thread before the change: a three-byte character
|
|
53
|
+
// straddling the last byte of the buffer reached the run as U+FFFD, the replacement character, on
|
|
54
|
+
// the end of the operator's own sentence.
|
|
55
|
+
const room = new Uint8Array(buffer, STOP_HEADER, STOP_CAPACITY);
|
|
56
|
+
const { written } = new TextEncoder().encodeInto(reason === "" ? NO_REASON : reason, room);
|
|
57
|
+
// LENGTH LAST, and with Atomics: it is what makes the bytes visible, so a reader can never see a
|
|
58
|
+
// length that promises bytes the writer has not finished writing.
|
|
59
|
+
Atomics.store(new Int32Array(buffer, 0, 1), 0, written);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run a transformed program in its own locked-down thread.
|
|
63
|
+
*
|
|
64
|
+
* The worker is terminated when the run answers, however it answers: one thread per run is the whole
|
|
65
|
+
* point, and a thread that outlives its run is a realm holding a journal nobody is reading.
|
|
66
|
+
*/
|
|
67
|
+
export function runInWorker(request, options) {
|
|
68
|
+
// ONE ROUTE, DECIDED, before a thread exists to be wrong in. A bridged request with no seam has
|
|
69
|
+
// nowhere to run its effects; a module-named handler beside a live seam is two answers to where
|
|
70
|
+
// the effects live, and picking one silently would be this module deciding the caller's
|
|
71
|
+
// confinement posture for it.
|
|
72
|
+
if ((request.handler === "bridged") !== (options.bridge !== undefined)) {
|
|
73
|
+
throw new Error(request.handler === "bridged"
|
|
74
|
+
? `request ${request.runId} names the bridged handler route but no bridge seam was supplied; pass options.bridge with the live handler and store`
|
|
75
|
+
: `request ${request.runId} names a module handler and a bridge seam at once; a run's effects live in the thread or in the host, not both`);
|
|
76
|
+
}
|
|
77
|
+
const stop = new SharedArrayBuffer(STOP_HEADER + STOP_CAPACITY);
|
|
78
|
+
let host;
|
|
79
|
+
let bridge;
|
|
80
|
+
if (options.bridge !== undefined) {
|
|
81
|
+
const channel = new MessageChannel();
|
|
82
|
+
host = serviceBridge(channel.port1, options.bridge);
|
|
83
|
+
bridge = { port: channel.port2, clock: host.clock };
|
|
84
|
+
}
|
|
85
|
+
// Two literal spellings rather than one spread, so the crossing audit in the engine suite reads
|
|
86
|
+
// exactly what the thread is handed in each route off this source.
|
|
87
|
+
const worker = bridge !== undefined
|
|
88
|
+
? new Worker(options.entry, { workerData: { request, stop, bridge }, transferList: [bridge.port] })
|
|
89
|
+
: new Worker(options.entry, { workerData: { request, stop } });
|
|
90
|
+
const done = new Promise((resolve, reject) => {
|
|
91
|
+
let answered = false;
|
|
92
|
+
worker.on("message", (m) => {
|
|
93
|
+
if (m.kind === "log") {
|
|
94
|
+
options.onLog?.(m.line);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// AN UNKNOWN KIND IS A DISAGREEMENT ABOUT THE PROTOCOL, and it is refused rather than read as
|
|
98
|
+
// an answer. This branch used to be `else`, so a thread posting anything unexpected resolved
|
|
99
|
+
// the run with `undefined` and the real result arrived after nobody was listening - a run
|
|
100
|
+
// reporting success with no value, from a boundary that had already gone wrong.
|
|
101
|
+
if (m.kind !== "result") {
|
|
102
|
+
answered = true;
|
|
103
|
+
reject(new Error(`cotal-lang engine worker sent a message kind this host does not know (${String(m.kind)}); the thread and the host disagree about the boundary`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
answered = true;
|
|
107
|
+
resolve(m.result);
|
|
108
|
+
});
|
|
109
|
+
worker.on("error", (e) => reject(e));
|
|
110
|
+
worker.on("exit", (code) => {
|
|
111
|
+
// A worker that exits without answering has taken the run's outcome with it, and saying so is
|
|
112
|
+
// the only honest thing left: the journal may hold a pending step and only the store knows.
|
|
113
|
+
if (!answered)
|
|
114
|
+
reject(new Error(`cotal-lang engine worker exited with code ${code} before the run answered`));
|
|
115
|
+
});
|
|
116
|
+
}).finally(() => {
|
|
117
|
+
// The seam closes AFTER the thread is gone, so no in-flight effect answers into a closed port;
|
|
118
|
+
// terminate() returns a promise and the close rides its settlement.
|
|
119
|
+
void worker.terminate().finally(() => host?.close());
|
|
120
|
+
});
|
|
121
|
+
return { done, stop: (reason) => publishStop(stop, reason) };
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/engine/worker.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -81,6 +81,7 @@ export declare const CATALOG: {
|
|
|
81
81
|
readonly L4018: "No implicit conversion";
|
|
82
82
|
readonly L4019: "Array write past the end";
|
|
83
83
|
readonly L4020: "A method is not a value";
|
|
84
|
+
readonly L4021: "A callable `then` is not a record member";
|
|
84
85
|
readonly L5001: "Run divergence";
|
|
85
86
|
readonly L5002: "Program hash not available";
|
|
86
87
|
readonly L5003: "Orphaned `spawn` on migrate";
|
|
@@ -103,6 +104,8 @@ export declare const CATALOG: {
|
|
|
103
104
|
readonly L5019: "Fork cannot honour `onFork` on this host";
|
|
104
105
|
readonly L5021: "Resume over a journal without the run's pins";
|
|
105
106
|
readonly L5022: "A recorded branch is not in the migrated source";
|
|
107
|
+
readonly L5023: "No engine in this build serves this record's language version";
|
|
108
|
+
readonly L5024: "A recorded value has no canonical form";
|
|
106
109
|
readonly L6001: "Unscripted effect in simulation";
|
|
107
110
|
readonly L6002: "Simulation script entry unused";
|
|
108
111
|
};
|
|
@@ -184,4 +187,45 @@ export declare class RuntimeFault extends Error {
|
|
|
184
187
|
readonly code: string;
|
|
185
188
|
constructor(code: string, message: string);
|
|
186
189
|
}
|
|
190
|
+
/** The message off anything a foreign body throws, read defensively: other people's code may throw a primitive. */
|
|
191
|
+
export declare function messageOf(v: unknown): string;
|
|
192
|
+
/** A recorded step's inputs changed, so its recorded result may no longer be the truth. */
|
|
193
|
+
export declare class RunDivergence extends Error {
|
|
194
|
+
readonly stepKey: string;
|
|
195
|
+
readonly recordedHash: string;
|
|
196
|
+
readonly programHash: string;
|
|
197
|
+
constructor(stepKey: string, recordedHash: string, programHash: string);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* A migration's walk reached a settled scope it cannot enter.
|
|
201
|
+
*
|
|
202
|
+
* A `conclave` is the case that exists today: its channel handle is HANDLER-DERIVED, the mint
|
|
203
|
+
* returns it and nothing journals it, so a walk cannot re-enter the body without inventing a
|
|
204
|
+
* handle, and an invented one would re-hash every step inside that used the channel into a
|
|
205
|
+
* divergence the run never had. Refusing is the honest exit. Consuming the subtree instead would
|
|
206
|
+
* hide exactly the orphans a migration exists to find, which is a silent wrong answer in place of
|
|
207
|
+
* a loud refusal.
|
|
208
|
+
*/
|
|
209
|
+
export declare class UnwalkableScope extends Error {
|
|
210
|
+
readonly scopeKey: string;
|
|
211
|
+
readonly why: string;
|
|
212
|
+
constructor(scopeKey: string, why: string);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A migration's walk was sent into a recorded branch the new source does not have.
|
|
216
|
+
*
|
|
217
|
+
* Its own code rather than `RunDivergence`, for the reason the L5005/L5006/L5007 collision in the
|
|
218
|
+
* orphan table bought: `RunDivergence` is a HASH comparison and says so in both its fields and its
|
|
219
|
+
* message, and putting branch NAMES in fields called `recordedHash`/`programHash` would be a lie in
|
|
220
|
+
* the payload a repair loop reads. The author's repair differs too: this one is fixed by looking at
|
|
221
|
+
* an arm's NAME, not at its body.
|
|
222
|
+
*/
|
|
223
|
+
export declare class ScopeBranchMissing extends Error {
|
|
224
|
+
readonly scopeKey: string;
|
|
225
|
+
readonly scope: string;
|
|
226
|
+
readonly missing: readonly string[];
|
|
227
|
+
readonly recorded: readonly string[];
|
|
228
|
+
readonly source: readonly string[];
|
|
229
|
+
constructor(scopeKey: string, scope: string, missing: readonly string[], recorded: readonly string[], source: readonly string[]);
|
|
230
|
+
}
|
|
187
231
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,wFAAwF;AACxF,eAAO,MAAM,OAAO
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|