@polyengine/runtime 0.5.0 → 0.6.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/esm/cabi/async_values.js +6 -5
- package/esm/cabi/bulk_lists.js +0 -5
- package/esm/cabi/context.js +13 -3
- package/esm/cabi/flatten.js +41 -9
- package/esm/cabi/handles.js +57 -54
- package/esm/cabi/layout.js +113 -52
- package/esm/cabi/load.js +31 -23
- package/esm/cabi/store.js +33 -26
- package/esm/cabi/trap.js +2 -2
- package/esm/cabi/types.js +138 -25
- package/esm/cabi/values.js +25 -7
- package/esm/cache/core.js +2 -11
- package/esm/digest/digest.js +10 -8
- package/esm/digest/mod.js +1 -1
- package/esm/digest/verify.js +6 -86
- package/esm/embedder/casing.js +24 -9
- package/esm/embedder/copy.js +6 -6
- package/esm/embedder/errors.js +2 -2
- package/esm/embedder/imports.js +3 -3
- package/esm/embedder/instantiate.js +170 -41
- package/esm/embedder/mod.js +9 -8
- package/esm/embedder/resources.js +39 -16
- package/esm/embedder/streams.js +36 -37
- package/esm/embedder/sync.js +242 -0
- package/esm/embedder/values.js +84 -22
- package/esm/embedder/version.js +9 -9
- package/esm/exec/boundary.js +318 -165
- package/esm/exec/executor.js +51 -24
- package/esm/exec/host_streams.js +31 -31
- package/esm/intrinsics/async_builtins.js +19 -8
- package/esm/intrinsics/context.js +1 -1
- package/esm/intrinsics/errors.js +9 -9
- package/esm/intrinsics/fact_calls.js +37 -49
- package/esm/intrinsics/mod.js +54 -117
- package/esm/intrinsics/stream_builtins.js +2 -2
- package/esm/intrinsics/transcode.js +1 -1
- package/esm/jspi/bridge.js +4 -3
- package/esm/jspi/suspending.js +10 -5
- package/esm/plan/loader.js +5 -5
- package/esm/shim/translator.js +2 -2
- package/esm/task/mod.js +45 -182
- package/esm/task/scheduler.js +154 -185
- package/esm/task/streams.js +39 -54
- package/esm/task/subtask.js +2 -2
- package/esm/task/thread.js +20 -41
- package/esm/task/waitable.js +0 -1
- package/package.json +2 -2
- package/types/cabi/async_values.d.ts +3 -2
- package/types/cabi/bulk_lists.d.ts +0 -2
- package/types/cabi/context.d.ts +15 -5
- package/types/cabi/flatten.d.ts +2 -2
- package/types/cabi/handles.d.ts +15 -26
- package/types/cabi/layout.d.ts +22 -1
- package/types/cabi/load.d.ts +10 -2
- package/types/cabi/store.d.ts +4 -2
- package/types/cabi/types.d.ts +22 -3
- package/types/digest/mod.d.ts +1 -1
- package/types/digest/verify.d.ts +3 -19
- package/types/embedder/casing.d.ts +9 -1
- package/types/embedder/copy.d.ts +4 -4
- package/types/embedder/instantiate.d.ts +4 -4
- package/types/embedder/mod.d.ts +3 -2
- package/types/embedder/resources.d.ts +20 -7
- package/types/embedder/streams.d.ts +5 -6
- package/types/embedder/sync.d.ts +81 -0
- package/types/embedder/values.d.ts +2 -2
- package/types/exec/boundary.d.ts +66 -42
- package/types/exec/executor.d.ts +3 -2
- package/types/exec/host_streams.d.ts +8 -8
- package/types/intrinsics/errors.d.ts +3 -3
- package/types/intrinsics/mod.d.ts +1 -1
- package/types/intrinsics/stream_builtins.d.ts +2 -2
- package/types/jspi/bridge.d.ts +6 -5
- package/types/jspi/suspending.d.ts +1 -1
- package/types/plan/format.d.ts +11 -10
- package/types/plan/loader.d.ts +2 -2
- package/types/shim/translator.d.ts +2 -2
- package/types/task/mod.d.ts +26 -97
- package/types/task/scheduler.d.ts +81 -62
- package/types/task/streams.d.ts +23 -38
- package/types/task/subtask.d.ts +2 -2
- package/types/task/waitable.d.ts +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Resources as classes on both sides of the boundary
|
|
2
|
-
// (contracts/embedder-api.md §"Resources"
|
|
2
|
+
// (contracts/embedder-api.md §"Resources").
|
|
3
3
|
//
|
|
4
4
|
// The raw boundary represents `own<R>` / `borrow<R>` as bare **reps**
|
|
5
5
|
// (cabi/handles.ts `liftOwn` returns `rh.rep`; the host never holds a table
|
|
6
|
-
// index).
|
|
6
|
+
// index). Embedders otherwise turn that into identity tables and
|
|
7
7
|
// hand-transcribed `[method]…` keys by hand. Both become runtime obligations
|
|
8
8
|
// here.
|
|
9
9
|
//
|
|
@@ -19,14 +19,15 @@ import { hostDtorCall } from "../exec/boundary.js";
|
|
|
19
19
|
import { COPY_URL, describeCrossCopy } from "./copy.js";
|
|
20
20
|
import { InvalidHandleError } from "./errors.js";
|
|
21
21
|
import { camelCase, pascalCase } from "./casing.js";
|
|
22
|
+
import { markSyncCallable, syncPayloadOf } from "./sync.js";
|
|
22
23
|
/**
|
|
23
24
|
* Internal state of a guest-resource wrapper.
|
|
24
25
|
*
|
|
25
|
-
* The KEY is the process-global `polyengine.resourceState/1` brand
|
|
26
|
-
*
|
|
26
|
+
* The KEY is the process-global `polyengine.resourceState/1` brand
|
|
27
|
+
* (contracts/embedder-api.md §"Module identity": it used to be a module-local `Symbol(...)`, on the now-repealed
|
|
27
28
|
* assumption that bundle and source runtimes are never mixed in one process —
|
|
28
29
|
* issue #83 showed they routinely are). The state SHAPE stays strictly
|
|
29
|
-
* runtime-internal, exactly as the
|
|
30
|
+
* runtime-internal, exactly as the module identity brand table notes: another copy may
|
|
30
31
|
* RECOGNIZE a wrapper, and must never read or write this object. `copyUrl` is
|
|
31
32
|
* what lets this copy tell its own wrappers from a foreign copy's.
|
|
32
33
|
*/
|
|
@@ -34,7 +35,7 @@ const STATE = RESOURCE_STATE;
|
|
|
34
35
|
/** Base of every runtime-built guest-resource class. */
|
|
35
36
|
export class GuestResource {
|
|
36
37
|
constructor() {
|
|
37
|
-
//
|
|
38
|
+
// realm boundary (contracts/embedder-api.md §"Realm boundaries and
|
|
38
39
|
// structured-clone-safe forms"; issue #131): guest-resource wrappers are
|
|
39
40
|
// realm-local by principle (their machinery lives in the minting
|
|
40
41
|
// copy's tables, issue #129's identity rule) — the pill makes a raw
|
|
@@ -130,7 +131,7 @@ export function initWrapper(w, state) {
|
|
|
130
131
|
*
|
|
131
132
|
* A wrapper minted by ANOTHER copy carries the same (process-global) brand key
|
|
132
133
|
* but its state belongs to that copy — reading it here would be reading a
|
|
133
|
-
* foreign copy's private shape
|
|
134
|
+
* foreign copy's private shape. So it is not a state: it is
|
|
134
135
|
* `undefined` here, and `requireLive` turns that into the named cross-copy
|
|
135
136
|
* error rather than a misleading "not a resource handle" / "not live".
|
|
136
137
|
*/
|
|
@@ -141,9 +142,9 @@ export function wrapperState(w) {
|
|
|
141
142
|
return s.copyUrl === COPY_URL ? s : undefined;
|
|
142
143
|
}
|
|
143
144
|
/**
|
|
144
|
-
* True iff `w` carries the
|
|
145
|
+
* True iff `w` carries the module identity resource-state key but is not one of ours.
|
|
145
146
|
*
|
|
146
|
-
* Note the resource brand is the odd one out in the
|
|
147
|
+
* Note the resource brand is the odd one out in the module identity table: its value is the
|
|
147
148
|
* state OBJECT, not `true`, so `hasBrand` does not apply — presence of the key
|
|
148
149
|
* is the recognition. Only meaningful once `wrapperState` has returned
|
|
149
150
|
* `undefined`, i.e. presence here means "another copy's wrapper".
|
|
@@ -265,7 +266,7 @@ export function takeRep(w, own, what) {
|
|
|
265
266
|
* synchronously is reported as such rather than silently returning a
|
|
266
267
|
* half-built object (see the report's contract-friction list).
|
|
267
268
|
*/
|
|
268
|
-
export function buildGuestResourceClass(spec, rt,
|
|
269
|
+
export function buildGuestResourceClass(spec, rt, wrapExport, lowerArgs) {
|
|
269
270
|
const className = pascalCase(spec.name);
|
|
270
271
|
const cls = class extends GuestResource {
|
|
271
272
|
constructor(...args) {
|
|
@@ -298,22 +299,44 @@ export function buildGuestResourceClass(spec, rt, adapt, lowerArgs) {
|
|
|
298
299
|
for (const m of spec.methods) {
|
|
299
300
|
const js = camelCase(m.member);
|
|
300
301
|
const where = `${className}.${js}`;
|
|
302
|
+
// Built ONCE at class-build time (sync(): "prototype methods and statics
|
|
303
|
+
// must carry the brand at class-build time, not per call") — every
|
|
304
|
+
// instance's method call goes through this same wrapper, receiver
|
|
305
|
+
// (`self`) prepended.
|
|
306
|
+
const wrapped = wrapExport(m.raw, m.params, m.results, m.async, where);
|
|
307
|
+
const methodFn = function (...args) {
|
|
308
|
+
// params[0] is the `borrow<R>`/`own<R>` self.
|
|
309
|
+
return wrapped(this, ...args);
|
|
310
|
+
};
|
|
311
|
+
const payload = syncPayloadOf(wrapped);
|
|
312
|
+
if (payload !== undefined) {
|
|
313
|
+
// A resource method's sync form takes `self` as its first argument —
|
|
314
|
+
// exactly `wrapped`'s own synchronous form (params[0] IS self), so the
|
|
315
|
+
// "method" brand's `fn` is `payload.fn` verbatim, just re-tagged so
|
|
316
|
+
// `sync()` knows this one needs `sync(instance)` rather than being
|
|
317
|
+
// callable bare.
|
|
318
|
+
markSyncCallable(methodFn, payload.kind === "free"
|
|
319
|
+
? { kind: "method", fn: payload.fn }
|
|
320
|
+
: payload);
|
|
321
|
+
}
|
|
301
322
|
Object.defineProperty(cls.prototype, js, {
|
|
302
323
|
configurable: true,
|
|
303
324
|
writable: true,
|
|
304
|
-
value:
|
|
305
|
-
// params[0] is the `borrow<R>`/`own<R>` self.
|
|
306
|
-
return adapt(m.raw, m.params, m.results, where, [this, ...args]);
|
|
307
|
-
},
|
|
325
|
+
value: methodFn,
|
|
308
326
|
});
|
|
309
327
|
}
|
|
310
328
|
for (const s of spec.statics) {
|
|
311
329
|
const js = camelCase(s.member);
|
|
312
330
|
const where = `${className}.${js} (static)`;
|
|
331
|
+
const wrapped = wrapExport(s.raw, s.params, s.results, s.async, where);
|
|
332
|
+
const staticFn = (...args) => wrapped(...args);
|
|
333
|
+
const payload = syncPayloadOf(wrapped);
|
|
334
|
+
if (payload !== undefined)
|
|
335
|
+
markSyncCallable(staticFn, payload);
|
|
313
336
|
Object.defineProperty(cls, js, {
|
|
314
337
|
configurable: true,
|
|
315
338
|
writable: true,
|
|
316
|
-
value:
|
|
339
|
+
value: staticFn,
|
|
317
340
|
});
|
|
318
341
|
}
|
|
319
342
|
return cls;
|
|
@@ -323,7 +346,7 @@ export function makeWrapper(
|
|
|
323
346
|
// deno-lint-ignore no-explicit-any
|
|
324
347
|
cls, rep, rt, owns) {
|
|
325
348
|
const w = Object.create(cls.prototype);
|
|
326
|
-
//
|
|
349
|
+
// realm boundary: `Object.create` bypasses `GuestResource`'s constructor, so the
|
|
327
350
|
// realm-local pill is installed explicitly here (see that constructor).
|
|
328
351
|
defineRealmLocal(w);
|
|
329
352
|
initWrapper(w, {
|
package/esm/embedder/streams.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Stream / future conventions (contracts/embedder-api.md §"Streams and
|
|
2
|
-
// futures"
|
|
2
|
+
// futures").
|
|
3
3
|
//
|
|
4
4
|
// The low-level seam is `exec/host_streams.ts` — `HostStream`/`HostFuture`
|
|
5
5
|
// over the shared rendezvous object. This file is the *handle* layer named by
|
|
@@ -14,7 +14,7 @@ import { defineBrand, defineRealmLocal, ERROR_CONTEXT, FUTURE, hasBrand, isStrea
|
|
|
14
14
|
import { describeCrossCopy } from "./copy.js";
|
|
15
15
|
import { DroppedError, PeerTrappedError } from "./errors.js";
|
|
16
16
|
// `StreamProducerError`'s canonical definition moved to `@polyengine/protocol`
|
|
17
|
-
// with
|
|
17
|
+
// with §"Module identity and @polyengine/protocol" (it is an embedder-contract value: recognition must
|
|
18
18
|
// survive multiple runtime copies, issue #83). Re-exported here so every
|
|
19
19
|
// existing import path is unchanged.
|
|
20
20
|
export { StreamProducerError } from "@polyengine/protocol";
|
|
@@ -45,7 +45,7 @@ const producerFailures = new WeakMap();
|
|
|
45
45
|
* shared object and raised on the next interaction with the handle.
|
|
46
46
|
*/
|
|
47
47
|
function reportProducerFailure(host, where, cause) {
|
|
48
|
-
// Brand, not class
|
|
48
|
+
// Brand, not class: a producer failure raised by another runtime copy
|
|
49
49
|
// must not be re-wrapped into a second layer of the same error.
|
|
50
50
|
const err = isStreamProducerError(cause)
|
|
51
51
|
? cause
|
|
@@ -68,8 +68,8 @@ function throwIfFailed(value, where = "stream") {
|
|
|
68
68
|
throwIfPeerTrapped(value, where);
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* @internal — raise the recorded poisoning failure, if any (#66,
|
|
72
|
-
*
|
|
71
|
+
* @internal — raise the recorded poisoning failure, if any (#66,
|
|
72
|
+
* contracts/embedder-api.md §"Streams and futures"). Pre-op: an operation started after the peer's instance trapped must
|
|
73
73
|
* reject rather than park forever. Post-await (with the op's outcome in
|
|
74
74
|
* hand): an operation the retirement walk settled DROPPED-shaped must reject
|
|
75
75
|
* rather than fake a clean end — but an op that genuinely COMPLETED before
|
|
@@ -86,12 +86,12 @@ export function isU8Element(element) {
|
|
|
86
86
|
return element !== null && despecialize(element).kind === "u8";
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* direct-access byte edge (#128): the direct-access byte edges are `stream<u8>` only. A
|
|
90
90
|
* zero-width element type (`t === null`) is not u8 either.
|
|
91
91
|
*/
|
|
92
92
|
function requireU8Direct(codec, who) {
|
|
93
93
|
if (codec === null || !isU8Element(codec.element)) {
|
|
94
|
-
throw new TypeError(`${who} is available on stream<u8> only (embedder-api
|
|
94
|
+
throw new TypeError(`${who} is available on stream<u8> only (embedder-api.md §"Streams and futures" ("Direct-access byte edges"), ` +
|
|
95
95
|
`polyengine#128); use write()/read() for other element types`);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -112,7 +112,7 @@ export class Stream {
|
|
|
112
112
|
constructor(host, codec) {
|
|
113
113
|
this.#host = host;
|
|
114
114
|
this.#codec = codec;
|
|
115
|
-
//
|
|
115
|
+
// realm boundary (contracts/embedder-api.md §"Realm boundaries and
|
|
116
116
|
// structured-clone-safe forms"; issue #131): the realm-local pill —
|
|
117
117
|
// stateful handles must fail loud (DataCloneError) at a raw
|
|
118
118
|
// structuredClone/postMessage instead of husking silently.
|
|
@@ -183,14 +183,14 @@ export class Stream {
|
|
|
183
183
|
"passed to a guest yet, so it has no element type; pass it first, " +
|
|
184
184
|
"or use the writer, which parks until then");
|
|
185
185
|
}
|
|
186
|
-
// Post-transfer refusal (#162, embedder-api
|
|
186
|
+
// Post-transfer refusal (#162, contracts/embedder-api.md §"Streams and futures"). Lifting
|
|
187
187
|
// removes the handle from the source table and lowering installs it in
|
|
188
188
|
// the destination's (definitions.py `lift_async_value` line 1530,
|
|
189
189
|
// `lower_stream` line 1828): once this handle's shared object has been
|
|
190
190
|
// passed to a guest, the guest owns the readable end and a host read here
|
|
191
191
|
// would operate a phantom duplicate of it. Refuse loudly instead.
|
|
192
192
|
// `StreamWriter` is deliberately unaffected — the host retains the
|
|
193
|
-
// writable end, and writing after the pass is the normal
|
|
193
|
+
// writable end, and writing after the pass is the normal stream/future round-trip pattern —
|
|
194
194
|
// and `drop()`/`cancelRead()` stay permissive.
|
|
195
195
|
if (this.#consumed) {
|
|
196
196
|
throw new TypeError("this Stream handle has already been passed to a guest; the guest " +
|
|
@@ -207,7 +207,7 @@ export class Stream {
|
|
|
207
207
|
const raw = await host.readable.read(max);
|
|
208
208
|
// An empty chunk normally means clean end-of-stream; when the peer's
|
|
209
209
|
// instance trapped it means the retirement walk settled us — reject
|
|
210
|
-
// instead of faking EOS (
|
|
210
|
+
// instead of faking EOS (§"Streams and futures"). A non-empty chunk was really
|
|
211
211
|
// copied before the trap and is delivered; the next read rejects.
|
|
212
212
|
if (raw.length === 0)
|
|
213
213
|
throwIfPeerTrapped(host.value, where);
|
|
@@ -215,7 +215,7 @@ export class Stream {
|
|
|
215
215
|
}
|
|
216
216
|
/**
|
|
217
217
|
* Consume the writer's bytes in place, without an intermediate chunk
|
|
218
|
-
* (`stream<u8>` only — contracts/embedder-api.md
|
|
218
|
+
* (`stream<u8>` only — contracts/embedder-api.md §"Streams and futures" ("Direct-access byte edges"),
|
|
219
219
|
* polyengine#128).
|
|
220
220
|
*
|
|
221
221
|
* At every rendezvous with a writer of nonzero capacity, `consume` runs
|
|
@@ -232,7 +232,7 @@ export class Stream {
|
|
|
232
232
|
* writer's parked operation survives and the stream stays alive.
|
|
233
233
|
*
|
|
234
234
|
* Refusals mirror `read`: an unbound `Stream.create()` handle and a handle
|
|
235
|
-
* already passed to a guest (the
|
|
235
|
+
* already passed to a guest (the deadlock-verdict suppression transfer guard) both throw, as does a
|
|
236
236
|
* non-`u8` element type.
|
|
237
237
|
*/
|
|
238
238
|
async readDirect(consume) {
|
|
@@ -242,7 +242,7 @@ export class Stream {
|
|
|
242
242
|
requireU8Direct(this.#codec, "readDirect");
|
|
243
243
|
const info = { endedByVerdict: false };
|
|
244
244
|
const n = await host.readable.readDirect(consume, info);
|
|
245
|
-
//
|
|
245
|
+
// loud component fault precision, `read`'s rule adapted: a session the CONSUMER itself
|
|
246
246
|
// ended with `"done"` genuinely completed and keeps its resolution. Any
|
|
247
247
|
// other way out (the writer dropped, the session was cancelled, the
|
|
248
248
|
// retirement walk settled us) is a settle-path this consumer did not
|
|
@@ -298,7 +298,7 @@ export class Stream {
|
|
|
298
298
|
* DROPPED event into the trapping instance's waitables, and a later
|
|
299
299
|
* driving loop asserted on the corpse).
|
|
300
300
|
*
|
|
301
|
-
* The arm is released on this path too (#162,
|
|
301
|
+
* The arm is released on this path too (#162, §"Streams and futures"): the wrapper's
|
|
302
302
|
* `HostActivity` now closes through the shared object's drop observers,
|
|
303
303
|
* which `dropSharedForTeardown` fires unconditionally — so a teardown with
|
|
304
304
|
* nothing parked no longer leaves the arm outliving the stream. (This
|
|
@@ -349,7 +349,7 @@ export class StreamWriter {
|
|
|
349
349
|
#stream;
|
|
350
350
|
constructor(stream) {
|
|
351
351
|
this.#stream = stream;
|
|
352
|
-
//
|
|
352
|
+
// realm boundary realm-local pill (see Stream's constructor above for rationale).
|
|
353
353
|
defineRealmLocal(this);
|
|
354
354
|
}
|
|
355
355
|
/**
|
|
@@ -370,7 +370,7 @@ export class StreamWriter {
|
|
|
370
370
|
const n = await host.writable.write(packChunk(values, this.#stream.codec));
|
|
371
371
|
// A short take normally means "re-offer later" / "reader done"; when the
|
|
372
372
|
// reader's instance trapped it means the retirement walk settled us —
|
|
373
|
-
// reject, carrying the delivered count (
|
|
373
|
+
// reject, carrying the delivered count (§"Streams and futures"). A full take
|
|
374
374
|
// genuinely completed before the trap and stays a success.
|
|
375
375
|
if (n < values.length)
|
|
376
376
|
throwIfPeerTrapped(host.value, where, n);
|
|
@@ -378,7 +378,7 @@ export class StreamWriter {
|
|
|
378
378
|
}
|
|
379
379
|
/**
|
|
380
380
|
* Fill the reader's landing zone in place, without an intermediate chunk
|
|
381
|
-
* (`stream<u8>` only — contracts/embedder-api.md
|
|
381
|
+
* (`stream<u8>` only — contracts/embedder-api.md §"Streams and futures" ("Direct-access byte edges"),
|
|
382
382
|
* polyengine#128).
|
|
383
383
|
*
|
|
384
384
|
* At every rendezvous with a reader of nonzero capacity, `produce` runs
|
|
@@ -406,7 +406,7 @@ export class StreamWriter {
|
|
|
406
406
|
requireU8Direct(this.#stream.codec, "writeDirect");
|
|
407
407
|
const info = { endedByVerdict: false };
|
|
408
408
|
const n = await host.writable.writeDirect(produce, info);
|
|
409
|
-
//
|
|
409
|
+
// loud component fault precision, `write`'s short-take rule adapted: a session the PRODUCER
|
|
410
410
|
// itself ended with `"done"` keeps its resolution; every other way out is
|
|
411
411
|
// a settle-path the producer did not cause, so a trapped peer rejects
|
|
412
412
|
// here carrying the delivered count.
|
|
@@ -470,7 +470,7 @@ export class Future {
|
|
|
470
470
|
this.#host = host;
|
|
471
471
|
this.#hostP = hostP;
|
|
472
472
|
this.#codec = codec;
|
|
473
|
-
//
|
|
473
|
+
// realm boundary realm-local pill (see Stream's constructor above for rationale).
|
|
474
474
|
defineRealmLocal(this);
|
|
475
475
|
}
|
|
476
476
|
static fromLifted(value, codec) {
|
|
@@ -494,7 +494,6 @@ export class Future {
|
|
|
494
494
|
* is itself PromiseLike, so `await` still works and still yields `T`), which
|
|
495
495
|
* keeps `drop()`/`cancel()` reachable for a caller that does not await. The
|
|
496
496
|
* alternative — resolving a Promise *to* the handle — is not expressible.
|
|
497
|
-
* Flagged in the C2 report.
|
|
498
497
|
*/
|
|
499
498
|
static deferred(pending, codec) {
|
|
500
499
|
const hostP = pending.then((v) => {
|
|
@@ -530,7 +529,7 @@ export class Future {
|
|
|
530
529
|
return this.#host.value;
|
|
531
530
|
}
|
|
532
531
|
#read() {
|
|
533
|
-
// Post-transfer refusal (#162,
|
|
532
|
+
// Post-transfer refusal (#162, §"Streams and futures"), the `Stream.read` mirror:
|
|
534
533
|
// once this handle was passed to a guest, the guest owns the readable end
|
|
535
534
|
// and a host read would operate a phantom duplicate. A read MEMOIZED
|
|
536
535
|
// before the transfer keeps resolving — it genuinely happened while the
|
|
@@ -546,7 +545,7 @@ export class Future {
|
|
|
546
545
|
const { value, result } = await host.readResult();
|
|
547
546
|
if (result !== CopyResult.COMPLETED) {
|
|
548
547
|
// A drop caused by the writer's instance trapping is a fault, not a
|
|
549
|
-
// "no value" outcome — brand it (#66,
|
|
548
|
+
// "no value" outcome — brand it (#66, §"Streams and futures").
|
|
550
549
|
throwIfPeerTrapped(host.value, this.#codec.where ?? "future read");
|
|
551
550
|
throw new DroppedError(result === CopyResult.CANCELLED
|
|
552
551
|
? "the future read was cancelled"
|
|
@@ -623,7 +622,7 @@ export class ErrorContext {
|
|
|
623
622
|
constructor(internal) {
|
|
624
623
|
this.internal = internal;
|
|
625
624
|
this.message = internal.debugMessage;
|
|
626
|
-
//
|
|
625
|
+
// realm boundary realm-local pill (see Stream's constructor above for rationale).
|
|
627
626
|
// Note: envelope-encodable brands take precedence over the pill at
|
|
628
627
|
// toCloneable time (ErrorContext carries both ERROR_CONTEXT and the
|
|
629
628
|
// pill; it encodes) — the pill here is only the backstop for raw
|
|
@@ -631,11 +630,11 @@ export class ErrorContext {
|
|
|
631
630
|
defineRealmLocal(this);
|
|
632
631
|
}
|
|
633
632
|
}
|
|
634
|
-
//
|
|
633
|
+
// module identity brands (contracts/embedder-api.md §"Module identity"): the STATEFUL
|
|
635
634
|
// embedder-facing handle classes. Their machinery lives in the copy that
|
|
636
635
|
// minted them, so the brand never makes a foreign handle usable — it makes
|
|
637
636
|
// it DIAGNOSABLE, at the lowering sites below. `StreamWriter` gains its
|
|
638
|
-
// brand with
|
|
637
|
+
// brand with §"The host-ABI surface and its version" (§"The host-ABI surface and its version"):
|
|
639
638
|
// writers carried none before because nothing needed to recognize one, and
|
|
640
639
|
// `isStreamWriter` now does.
|
|
641
640
|
defineBrand(Stream.prototype, STREAM);
|
|
@@ -651,13 +650,13 @@ defineBrand(ErrorContext.prototype, ERROR_CONTEXT);
|
|
|
651
650
|
* footgun.
|
|
652
651
|
*/
|
|
653
652
|
export function lowerStreamSource(src, codec) {
|
|
654
|
-
// Order matters (
|
|
653
|
+
// Order matters (§"Module identity and @polyengine/protocol"). Same-copy handle: the fast path, unchanged.
|
|
655
654
|
if (src instanceof Stream) {
|
|
656
655
|
return src.takeValue(codec);
|
|
657
656
|
}
|
|
658
657
|
// Branded but not ours: a `Stream` minted by ANOTHER runtime copy. Without
|
|
659
658
|
// this check it would fall through to producer adaptation below and be
|
|
660
|
-
// pumped by its async iterator — a silent downgrade that quietly voids
|
|
659
|
+
// pumped by its async iterator — a silent downgrade that quietly voids stream/future round-trip's
|
|
661
660
|
// identity guarantees. Refused, loudly, naming both copies (issue #83).
|
|
662
661
|
if (hasBrand(src, STREAM)) {
|
|
663
662
|
throw new TypeError(describeCrossCopy("this stream handle", "To pipe it by value, pass `src.readable()` instead."));
|
|
@@ -695,11 +694,11 @@ async function pump(src, host, codec) {
|
|
|
695
694
|
const where = codec.where ?? "stream producer";
|
|
696
695
|
let failure;
|
|
697
696
|
let produced = 0;
|
|
698
|
-
//
|
|
697
|
+
// resource stream cancellation companion: the pump learns of the reader dropping
|
|
699
698
|
// through short writes, but a producer PARKED on an external event (an
|
|
700
699
|
// accept-shaped source holding a live platform resource) offers no write
|
|
701
700
|
// to shorten — this notification is its only stop signal. It also fires
|
|
702
|
-
// on the
|
|
701
|
+
// on the loud component fault teardown walk and on our own end-of-pump drop (harmless: the
|
|
703
702
|
// loop has exited by then).
|
|
704
703
|
const gone = new Promise((resolve) => host.writable.onDropped(() => resolve(READER_GONE)));
|
|
705
704
|
try {
|
|
@@ -712,7 +711,7 @@ async function pump(src, host, codec) {
|
|
|
712
711
|
n = await host.writable.writeAll(lowered);
|
|
713
712
|
}
|
|
714
713
|
catch (e) {
|
|
715
|
-
//
|
|
714
|
+
// resource stream: elements past the fault's progress point were lowered but
|
|
716
715
|
// will never be taken — destroy them (an `own` element may hold a
|
|
717
716
|
// live platform resource). `PeerTrappedError.progress` reports
|
|
718
717
|
// delivered-before-the-fault; anything else delivered nothing.
|
|
@@ -722,7 +721,7 @@ async function pump(src, host, codec) {
|
|
|
722
721
|
produced += n;
|
|
723
722
|
if (n < lowered.length) {
|
|
724
723
|
// The reader went away: a clean end — but the un-taken tail of this
|
|
725
|
-
// chunk was already lowered and must be destroyed, not leaked
|
|
724
|
+
// chunk was already lowered and must be destroyed, not leaked.
|
|
726
725
|
releaseUntaken(lowered, n, codec);
|
|
727
726
|
break;
|
|
728
727
|
}
|
|
@@ -744,7 +743,7 @@ async function pump(src, host, codec) {
|
|
|
744
743
|
// been recorded on the store, so the call fails rather than resolving.
|
|
745
744
|
host.writable.drop();
|
|
746
745
|
}
|
|
747
|
-
/**
|
|
746
|
+
/** resource stream: destroy `lowered[taken..]` when a codec's elements hold resources. */
|
|
748
747
|
function releaseUntaken(lowered, taken, codec) {
|
|
749
748
|
const release = codec.release;
|
|
750
749
|
if (release === undefined || lowered instanceof Uint8Array)
|
|
@@ -754,13 +753,13 @@ function releaseUntaken(lowered, taken, codec) {
|
|
|
754
753
|
}
|
|
755
754
|
/**
|
|
756
755
|
* Normalize every accepted producer shape to an async iterator of batches,
|
|
757
|
-
* racing each pull against `gone` (
|
|
756
|
+
* racing each pull against `gone` (resource stream cancellation): when the stream dies
|
|
758
757
|
* with the producer parked, a `ReadableStream` source is `cancel()`ed
|
|
759
758
|
* through its reader, and an (async-)iterable source gets its optional
|
|
760
759
|
* `cancel()` method invoked — the documented producer-cancellation hook —
|
|
761
760
|
* then its pending pull is drained so a straggler element the producer
|
|
762
761
|
* already minted still reaches the caller's release path. A source with no
|
|
763
|
-
* cancel hook keeps the pre-
|
|
762
|
+
* cancel hook keeps the pre-resource stream behavior: the pump stays parked until the
|
|
764
763
|
* producer's next element (or forever — the documented embedder-negligence
|
|
765
764
|
* hang class).
|
|
766
765
|
*/
|
|
@@ -835,10 +834,10 @@ function isReadableStream(v) {
|
|
|
835
834
|
export function lowerFutureSource(src, codec) {
|
|
836
835
|
if (src instanceof Future)
|
|
837
836
|
return src.takeValue();
|
|
838
|
-
// Branded but not ours (
|
|
837
|
+
// Branded but not ours (§"Module identity and @polyengine/protocol"). This one is the sharpest edge in the
|
|
839
838
|
// family: `Future` is a `PromiseLike`, so a foreign future would otherwise
|
|
840
839
|
// be adopted as a plain thenable and appear to work — exactly the silent
|
|
841
|
-
// path
|
|
840
|
+
// path module identity bans, since the awaited value would ride the OTHER copy's
|
|
842
841
|
// machinery with no handle transfer at all.
|
|
843
842
|
if (hasBrand(src, FUTURE)) {
|
|
844
843
|
throw new TypeError(describeCrossCopy("this future handle", "To pipe it by value, pass `Promise.resolve(f)` instead."));
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// `sync()` — the explicit synchronous view of a WIT-sync export (contracts/
|
|
2
|
+
// embedder-api.md §"Functions and async", §"Functions and async", 2026-08-30).
|
|
3
|
+
//
|
|
4
|
+
// Placement: application machinery exported from
|
|
5
|
+
// `@polyengine/runtime/embedder`, like `createStream` — only an instantiating
|
|
6
|
+
// application holds export functions, so this is deliberately NOT host-module
|
|
7
|
+
// vocabulary and does not touch `@polyengine/protocol`.
|
|
8
|
+
//
|
|
9
|
+
// Recognition is by brand (`polyengine.syncCallable/1`, a registry symbol per
|
|
10
|
+
// module identity) so views work across mixed runtime copies. Unlike the boolean brands in
|
|
11
|
+
// `@polyengine/protocol`'s `brands.ts` (whose payload is always `true`), this
|
|
12
|
+
// brand carries a PAYLOAD describing the callable's synchronous form — the
|
|
13
|
+
// dispatch shapes below are what `instantiate.ts` / `resources.ts` attach at
|
|
14
|
+
// wrap time and what this module reads back.
|
|
15
|
+
/** The registry symbol. `Symbol.for` per module identity: N runtime copies agree on it
|
|
16
|
+
* without sharing modules. */
|
|
17
|
+
export const SYNC_CALLABLE = Symbol.for("polyengine.syncCallable/1");
|
|
18
|
+
/**
|
|
19
|
+
* Stamp `payload` on `target` under the brand: non-enumerable, non-writable,
|
|
20
|
+
* matching `@polyengine/protocol`'s `defineBrand` (protocol/src/brands.ts) —
|
|
21
|
+
* implemented locally since the runtime does not add application-tier
|
|
22
|
+
* vocabulary to the protocol package.
|
|
23
|
+
*
|
|
24
|
+
* @internal — written by `instantiate.ts` and `resources.ts` at wrap/
|
|
25
|
+
* class-build time; not part of the public `sync()` surface.
|
|
26
|
+
*/
|
|
27
|
+
export function markSyncCallable(target, payload) {
|
|
28
|
+
Object.defineProperty(target, SYNC_CALLABLE, {
|
|
29
|
+
value: payload,
|
|
30
|
+
enumerable: false,
|
|
31
|
+
writable: false,
|
|
32
|
+
configurable: false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read the brand payload off `target`, or `undefined` if unbranded.
|
|
37
|
+
* Structural, like `hasBrand`: accepts a payload minted by any copy.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export function syncPayloadOf(target) {
|
|
41
|
+
if (target === null)
|
|
42
|
+
return undefined;
|
|
43
|
+
const t = typeof target;
|
|
44
|
+
if (t !== "object" && t !== "function")
|
|
45
|
+
return undefined;
|
|
46
|
+
return target[SYNC_CALLABLE];
|
|
47
|
+
}
|
|
48
|
+
/** Own, function-valued, branded members of `proto`'s prototype chain
|
|
49
|
+
* (stopping at `Object.prototype`), nearest wins. Used to recognize a
|
|
50
|
+
* guest-resource INSTANCE: its class's prototype carries `"method"`-branded
|
|
51
|
+
* data properties (`resources.ts` `buildGuestResourceClass`). */
|
|
52
|
+
function protoBrandedMembers(proto) {
|
|
53
|
+
const out = new Map();
|
|
54
|
+
for (let o = proto; o !== null && o !== Object.prototype; o = Object.getPrototypeOf(o)) {
|
|
55
|
+
for (const key of Object.getOwnPropertyNames(o)) {
|
|
56
|
+
if (out.has(key) || key === "constructor")
|
|
57
|
+
continue;
|
|
58
|
+
const d = Object.getOwnPropertyDescriptor(o, key);
|
|
59
|
+
if (d === undefined || typeof d.value !== "function")
|
|
60
|
+
continue;
|
|
61
|
+
const p = syncPayloadOf(d.value);
|
|
62
|
+
if (p !== undefined)
|
|
63
|
+
out.set(key, p);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/** Own, function-valued, branded static members of a guest-resource class. */
|
|
69
|
+
function ownBrandedStatics(cls) {
|
|
70
|
+
const out = new Map();
|
|
71
|
+
for (const key of Object.getOwnPropertyNames(cls)) {
|
|
72
|
+
if (key === "prototype" || key === "name" || key === "length")
|
|
73
|
+
continue;
|
|
74
|
+
const d = Object.getOwnPropertyDescriptor(cls, key);
|
|
75
|
+
if (d === undefined || typeof d.value !== "function")
|
|
76
|
+
continue;
|
|
77
|
+
const p = syncPayloadOf(d.value);
|
|
78
|
+
if (p !== undefined)
|
|
79
|
+
out.set(key, p);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function isResourceInstance(v) {
|
|
84
|
+
if (typeof v === "function")
|
|
85
|
+
return false; // a class, not an instance
|
|
86
|
+
const proto = Object.getPrototypeOf(v);
|
|
87
|
+
if (proto === null || proto === Object.prototype)
|
|
88
|
+
return false;
|
|
89
|
+
return protoBrandedMembers(proto).size > 0;
|
|
90
|
+
}
|
|
91
|
+
// deno-lint-ignore ban-types
|
|
92
|
+
function isResourceClass(v) {
|
|
93
|
+
return ownBrandedStatics(v).size > 0;
|
|
94
|
+
}
|
|
95
|
+
function asyncMessage(name) {
|
|
96
|
+
return `sync(): '${name}' is an async-typed WIT export; async exports ` +
|
|
97
|
+
`have no synchronous form`;
|
|
98
|
+
}
|
|
99
|
+
function methodMessage(name) {
|
|
100
|
+
return `sync(): '${name}' is a resource method; call sync(instance) ` +
|
|
101
|
+
`instead of sync(fn) — a bare method function has no receiver to bind`;
|
|
102
|
+
}
|
|
103
|
+
/** Views are stable: `sync(x) === sync(x)` for the same target. */
|
|
104
|
+
const views = new WeakMap();
|
|
105
|
+
function memoView(key, build) {
|
|
106
|
+
const cached = views.get(key);
|
|
107
|
+
if (cached !== undefined)
|
|
108
|
+
return cached;
|
|
109
|
+
const view = build();
|
|
110
|
+
views.set(key, view);
|
|
111
|
+
return view;
|
|
112
|
+
}
|
|
113
|
+
/** A view member that reports its real reason (async) only when accessed —
|
|
114
|
+
* so an unrelated sync member of the same record/class/instance stays usable
|
|
115
|
+
* (see the CONTRACT note on record recursion below). */
|
|
116
|
+
function throwingMember(view, key, message) {
|
|
117
|
+
Object.defineProperty(view, key, {
|
|
118
|
+
enumerable: true,
|
|
119
|
+
configurable: true,
|
|
120
|
+
get() {
|
|
121
|
+
throw new TypeError(message);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function instanceView(instance) {
|
|
126
|
+
return memoView(instance, () => {
|
|
127
|
+
const proto = Object.getPrototypeOf(instance);
|
|
128
|
+
const members = protoBrandedMembers(proto);
|
|
129
|
+
const view = {};
|
|
130
|
+
for (const [key, p] of members) {
|
|
131
|
+
if (p.kind === "method") {
|
|
132
|
+
const fn = p.fn;
|
|
133
|
+
view[key] = (...a) => fn(instance, ...a);
|
|
134
|
+
}
|
|
135
|
+
else if (p.kind === "async") {
|
|
136
|
+
throwingMember(view, key, asyncMessage(key));
|
|
137
|
+
}
|
|
138
|
+
// A "free"-kind branded proto member should not occur (methods are
|
|
139
|
+
// always branded "method" by `buildGuestResourceClass`); nothing to do
|
|
140
|
+
// if it somehow did — the instance view only ever exposes methods
|
|
141
|
+
// (statics are not reachable from an instance; §"Functions and async").
|
|
142
|
+
}
|
|
143
|
+
return view;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function classView(cls) {
|
|
147
|
+
return memoView(cls, () => {
|
|
148
|
+
const statics = ownBrandedStatics(cls);
|
|
149
|
+
const view = {};
|
|
150
|
+
for (const [key, p] of statics) {
|
|
151
|
+
if (p.kind === "free") {
|
|
152
|
+
view[key] = p.fn;
|
|
153
|
+
}
|
|
154
|
+
else if (p.kind === "async") {
|
|
155
|
+
throwingMember(view, key, asyncMessage(key));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return view;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Map one record MEMBER by the `sync(record)` recursion rule: a branded
|
|
163
|
+
* function or a nested resource class/instance/record maps recursively;
|
|
164
|
+
* anything else (including an unbranded function) passes through unchanged.
|
|
165
|
+
*
|
|
166
|
+
* CONTRACT (contracts/embedder-api.md §"Functions and async" sync(), the
|
|
167
|
+
* `sync(record)` bullet): the bullet says a record's members are "mapped by
|
|
168
|
+
* these same rules, recursively" — read most literally, an async-typed
|
|
169
|
+
* member nested in a record should behave exactly as `sync(asyncFn)` does at
|
|
170
|
+
* top level, i.e. throw. But applying that EAGERLY while building the
|
|
171
|
+
* parent's view would make one unrelated async export in a real component's
|
|
172
|
+
* exports record (a normal mix — see contracts/embedder-api.md's own async +
|
|
173
|
+
* sync export examples) poison `sync(exports)` entirely, defeating the
|
|
174
|
+
* per-use adapter's whole purpose. The conservative reading kept here defers
|
|
175
|
+
* that failure to the point the caller actually reaches for the async
|
|
176
|
+
* member (`throwingMember`), never for members the caller never touches —
|
|
177
|
+
* every failure the contract mandates still happens, just lazily.
|
|
178
|
+
*/
|
|
179
|
+
function mapMember(v) {
|
|
180
|
+
if (typeof v === "function") {
|
|
181
|
+
const p = syncPayloadOf(v);
|
|
182
|
+
if (p !== undefined) {
|
|
183
|
+
if (p.kind === "free")
|
|
184
|
+
return p.fn;
|
|
185
|
+
if (p.kind === "method")
|
|
186
|
+
throw new TypeError(methodMessage(v.name));
|
|
187
|
+
throw new TypeError(asyncMessage(v.name));
|
|
188
|
+
}
|
|
189
|
+
if (isResourceClass(v))
|
|
190
|
+
return classView(v);
|
|
191
|
+
return v; // unbranded function: pass through unchanged
|
|
192
|
+
}
|
|
193
|
+
if (v !== null && typeof v === "object") {
|
|
194
|
+
if (isResourceInstance(v))
|
|
195
|
+
return instanceView(v);
|
|
196
|
+
return recordView(v); // a nested (interface) record
|
|
197
|
+
}
|
|
198
|
+
return v; // primitives, null: pass through unchanged
|
|
199
|
+
}
|
|
200
|
+
function recordView(rec) {
|
|
201
|
+
return memoView(rec, () => {
|
|
202
|
+
const view = {};
|
|
203
|
+
for (const key of Object.keys(rec)) {
|
|
204
|
+
const d = Object.getOwnPropertyDescriptor(rec, key);
|
|
205
|
+
if (d === undefined)
|
|
206
|
+
continue;
|
|
207
|
+
const value = d.value;
|
|
208
|
+
// Lazy: `mapMember` runs (and can throw, for an async member) only
|
|
209
|
+
// when the caller actually reads this key — see the CONTRACT note on
|
|
210
|
+
// `mapMember` above.
|
|
211
|
+
Object.defineProperty(view, key, {
|
|
212
|
+
enumerable: true,
|
|
213
|
+
configurable: true,
|
|
214
|
+
get: () => mapMember(value),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return view;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
export function sync(target) {
|
|
221
|
+
if (typeof target === "function") {
|
|
222
|
+
const p = syncPayloadOf(target);
|
|
223
|
+
if (p !== undefined) {
|
|
224
|
+
if (p.kind === "free")
|
|
225
|
+
return p.fn;
|
|
226
|
+
if (p.kind === "method") {
|
|
227
|
+
throw new TypeError(methodMessage(target.name || "<anonymous>"));
|
|
228
|
+
}
|
|
229
|
+
throw new TypeError(asyncMessage(target.name || "<anonymous>"));
|
|
230
|
+
}
|
|
231
|
+
if (isResourceClass(target))
|
|
232
|
+
return classView(target);
|
|
233
|
+
throw new TypeError(`sync(): '${target.name || "<anonymous>"}' is not a sync-callable export (unbranded function)`);
|
|
234
|
+
}
|
|
235
|
+
if (target === null || typeof target !== "object") {
|
|
236
|
+
throw new TypeError(`sync(): expected a lifted export function, guest-resource instance/` +
|
|
237
|
+
`class, or exports record; got ${target === null ? "null" : typeof target}`);
|
|
238
|
+
}
|
|
239
|
+
if (isResourceInstance(target))
|
|
240
|
+
return instanceView(target);
|
|
241
|
+
return recordView(target);
|
|
242
|
+
}
|