@polyengine/runtime 0.4.0 → 0.5.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/embedder/copy.js +1 -1
- package/esm/embedder/mod.js +21 -10
- package/esm/embedder/streams.js +96 -5
- package/esm/exec/host_streams.js +376 -0
- package/esm/task/streams.js +127 -2
- package/package.json +2 -2
- package/types/embedder/copy.d.ts +1 -1
- package/types/embedder/mod.d.ts +7 -4
- package/types/embedder/streams.d.ts +53 -6
- package/types/exec/host_streams.d.ts +70 -0
- package/types/task/streams.d.ts +83 -0
package/esm/embedder/copy.js
CHANGED
|
@@ -29,7 +29,7 @@ export const COPY_URL = import.meta.url;
|
|
|
29
29
|
* @internal — copy-identity constant for the A9 multi-copy diagnostics; not
|
|
30
30
|
* host-facing.
|
|
31
31
|
*/
|
|
32
|
-
export const RUNTIME_VERSION = "0.
|
|
32
|
+
export const RUNTIME_VERSION = "0.5.0";
|
|
33
33
|
/**
|
|
34
34
|
* Compose a cross-copy diagnostic: what was foreign, which copy is speaking,
|
|
35
35
|
* the census of every copy in the graph, and the by-value remediation.
|
package/esm/embedder/mod.js
CHANGED
|
@@ -22,19 +22,30 @@ registerRuntimeCopy({
|
|
|
22
22
|
protocolGeneration: PROTOCOL_GENERATION,
|
|
23
23
|
});
|
|
24
24
|
export { COPY_URL, RUNTIME_VERSION } from "./copy.js";
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
25
|
+
// Amendment A22 (contracts/embedder-api.md §"The host-ABI surface and its
|
|
26
|
+
// version"): the runtime's exported surface is application-only. The A9
|
|
27
|
+
// courtesy re-exports (error classes, predicates, brands, `suspending`,
|
|
28
|
+
// realm crossing, the copy registry) are removed — host modules import that
|
|
29
|
+
// vocabulary from `@polyengine/protocol` directly. The runtime still
|
|
30
|
+
// registers its own copy on the census above; it just no longer hands out
|
|
31
|
+
// the registry API to callers of this module.
|
|
29
32
|
export { artifactsFromEnvelope, instantiate, instantiateEmbedder, resolveArtifacts, } from "./instantiate.js";
|
|
30
33
|
export { requiredImports } from "./imports.js";
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
// `NameCollisionError` is the one error class that stays here: it's raised
|
|
35
|
+
// while building an instantiation facade, before any handle/value exists —
|
|
36
|
+
// application machinery, not host-ABI vocabulary (contracts/embedder-api.md
|
|
37
|
+
// §"The host-ABI surface and its version", amendment A22).
|
|
38
|
+
export { NameCollisionError } from "./errors.js";
|
|
39
|
+
// `createStream<T>()` — the A22 stream-pair factory (contracts/embedder-api.md
|
|
40
|
+
// §"The host-ABI surface and its version" / §"Streams and futures"): the
|
|
41
|
+
// `Stream.create()` static's application-surface spelling, since the
|
|
42
|
+
// concrete `Stream`/`StreamWriter` classes are no longer exported. Handle
|
|
43
|
+
// TYPES are spelled against `@polyengine/protocol`'s structural interfaces.
|
|
44
|
+
import { Stream as InternalStream } from "./streams.js";
|
|
45
|
+
export function createStream() {
|
|
46
|
+
return InternalStream.create();
|
|
47
|
+
}
|
|
33
48
|
export { GuestResource, HostResourceRegistry } from "./resources.js";
|
|
34
49
|
export { camelCase, parseLeafName, pascalCase } from "./casing.js";
|
|
35
|
-
// Per-declaration suspendability (contracts/embedder-api.md §"Functions and
|
|
36
|
-
// async", amendment A1): declares that a sync-typed host import may return a
|
|
37
|
-
// Promise, parking the calling wasm frame (JSPI engines only).
|
|
38
|
-
export { suspending } from "../jspi/suspending.js";
|
|
39
50
|
export { asTrackKeySpelling, compareSemver, ImportRegistrationError, ImportResolutionError, ImportResolver, parseInterfaceId, parseSemver, trackKey, } from "./version.js";
|
|
40
51
|
export { BorrowScope, fromHost, toHost, } from "./values.js";
|
package/esm/embedder/streams.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { despecialize } from "../cabi/types.js";
|
|
11
11
|
import { hostFuture, hostFutureFor, hostStream, hostStreamFor, } from "../exec/host_streams.js";
|
|
12
12
|
import { CopyResult, dropSharedForTeardown, poisonFailureOf, } from "../task/mod.js";
|
|
13
|
-
import { defineBrand, defineRealmLocal, ERROR_CONTEXT, FUTURE, hasBrand, isStreamProducerError, STREAM, StreamProducerError, } from "@polyengine/protocol";
|
|
13
|
+
import { defineBrand, defineRealmLocal, ERROR_CONTEXT, FUTURE, hasBrand, isStreamProducerError, STREAM, StreamProducerError, STREAM_WRITER, } from "@polyengine/protocol";
|
|
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`
|
|
@@ -85,6 +85,16 @@ function throwIfPeerTrapped(value, where, progress) {
|
|
|
85
85
|
export function isU8Element(element) {
|
|
86
86
|
return element !== null && despecialize(element).kind === "u8";
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* A21 (#128): the direct-access byte edges are `stream<u8>` only. A
|
|
90
|
+
* zero-width element type (`t === null`) is not u8 either.
|
|
91
|
+
*/
|
|
92
|
+
function requireU8Direct(codec, who) {
|
|
93
|
+
if (codec === null || !isU8Element(codec.element)) {
|
|
94
|
+
throw new TypeError(`${who} is available on stream<u8> only (embedder-api amendment A21, ` +
|
|
95
|
+
`polyengine#128); use write()/read() for other element types`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
88
98
|
/**
|
|
89
99
|
* A stream handle.
|
|
90
100
|
*
|
|
@@ -203,6 +213,45 @@ export class Stream {
|
|
|
203
213
|
throwIfPeerTrapped(host.value, where);
|
|
204
214
|
return this.#chunk(raw);
|
|
205
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Consume the writer's bytes in place, without an intermediate chunk
|
|
218
|
+
* (`stream<u8>` only — contracts/embedder-api.md amendment A21,
|
|
219
|
+
* polyengine#128).
|
|
220
|
+
*
|
|
221
|
+
* At every rendezvous with a writer of nonzero capacity, `consume` runs
|
|
222
|
+
* exactly once, synchronously, with a `DirectSource` over the writer's
|
|
223
|
+
* unread bytes — guest linear memory when the peer is a guest, so the
|
|
224
|
+
* consumer's own `set()`/`subarray` copy IS the canonical-ABI copy.
|
|
225
|
+
* `"more"` keeps the session parked for the next rendezvous; `"done"` ends
|
|
226
|
+
* it. Resolves with the session's total byte count. Marking a prefix is
|
|
227
|
+
* normal: the writer re-offers the rest on its own schedule.
|
|
228
|
+
*
|
|
229
|
+
* `"done"` with zero bytes marked *retracts*: the session ends and the
|
|
230
|
+
* writer's operation stays parked, with no event delivered. `"more"` with
|
|
231
|
+
* zero marked, and a throwing callback, reject — and in both cases the
|
|
232
|
+
* writer's parked operation survives and the stream stays alive.
|
|
233
|
+
*
|
|
234
|
+
* Refusals mirror `read`: an unbound `Stream.create()` handle and a handle
|
|
235
|
+
* already passed to a guest (the A15 transfer guard) both throw, as does a
|
|
236
|
+
* non-`u8` element type.
|
|
237
|
+
*/
|
|
238
|
+
async readDirect(consume) {
|
|
239
|
+
const host = this.#require();
|
|
240
|
+
const where = this.#codec?.where ?? "stream read";
|
|
241
|
+
throwIfFailed(host.value, where);
|
|
242
|
+
requireU8Direct(this.#codec, "readDirect");
|
|
243
|
+
const info = { endedByVerdict: false };
|
|
244
|
+
const n = await host.readable.readDirect(consume, info);
|
|
245
|
+
// A7 precision, `read`'s rule adapted: a session the CONSUMER itself
|
|
246
|
+
// ended with `"done"` genuinely completed and keeps its resolution. Any
|
|
247
|
+
// other way out (the writer dropped, the session was cancelled, the
|
|
248
|
+
// retirement walk settled us) is a settle-path this consumer did not
|
|
249
|
+
// cause — so if the peer's instance trapped, reject with the delivered
|
|
250
|
+
// count rather than fake a clean end.
|
|
251
|
+
if (!info.endedByVerdict)
|
|
252
|
+
throwIfPeerTrapped(host.value, where, n);
|
|
253
|
+
return n;
|
|
254
|
+
}
|
|
206
255
|
#chunk(raw) {
|
|
207
256
|
const codec = this.#codec;
|
|
208
257
|
if (isU8Element(codec.element)) {
|
|
@@ -327,6 +376,44 @@ export class StreamWriter {
|
|
|
327
376
|
throwIfPeerTrapped(host.value, where, n);
|
|
328
377
|
return n;
|
|
329
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Fill the reader's landing zone in place, without an intermediate chunk
|
|
381
|
+
* (`stream<u8>` only — contracts/embedder-api.md amendment A21,
|
|
382
|
+
* polyengine#128).
|
|
383
|
+
*
|
|
384
|
+
* At every rendezvous with a reader of nonzero capacity, `produce` runs
|
|
385
|
+
* exactly once, synchronously, with a `DirectDestination` over the reader's
|
|
386
|
+
* unfilled landing zone — guest linear memory when the peer is a guest, so
|
|
387
|
+
* the producer's own `set()` IS the canonical-ABI copy and an external byte
|
|
388
|
+
* mover (a websocket frame, a SAB ring segment, a transferred
|
|
389
|
+
* `ArrayBuffer`) never pays a second copy inside the runtime. `"more"`
|
|
390
|
+
* keeps the session parked for the next rendezvous; `"done"` ends it.
|
|
391
|
+
* Resolves with the session's total byte count.
|
|
392
|
+
*
|
|
393
|
+
* `"done"` with zero bytes marked *retracts* (the session ends, the
|
|
394
|
+
* reader's operation stays parked, no event — the speculative-park
|
|
395
|
+
* correction); `"more"` with zero marked, and a throwing callback, reject.
|
|
396
|
+
*
|
|
397
|
+
* Parks until the element type is known, exactly as `write` does — a
|
|
398
|
+
* `Stream.create()` writer has no element type until the lowering site
|
|
399
|
+
* binds one — and then requires `u8`.
|
|
400
|
+
*/
|
|
401
|
+
async writeDirect(produce) {
|
|
402
|
+
await this.#stream.whenBound();
|
|
403
|
+
const host = hostOf(this.#stream);
|
|
404
|
+
const where = this.#stream.codec?.where ?? "stream write";
|
|
405
|
+
throwIfFailed(host.value, where);
|
|
406
|
+
requireU8Direct(this.#stream.codec, "writeDirect");
|
|
407
|
+
const info = { endedByVerdict: false };
|
|
408
|
+
const n = await host.writable.writeDirect(produce, info);
|
|
409
|
+
// A7 precision, `write`'s short-take rule adapted: a session the PRODUCER
|
|
410
|
+
// itself ended with `"done"` keeps its resolution; every other way out is
|
|
411
|
+
// a settle-path the producer did not cause, so a trapped peer rejects
|
|
412
|
+
// here carrying the delivered count.
|
|
413
|
+
if (!info.endedByVerdict)
|
|
414
|
+
throwIfPeerTrapped(host.value, where, n);
|
|
415
|
+
return n;
|
|
416
|
+
}
|
|
330
417
|
/** Offer values until all are taken or the reader goes away. */
|
|
331
418
|
async writeAll(values) {
|
|
332
419
|
await this.#stream.whenBound();
|
|
@@ -544,11 +631,15 @@ export class ErrorContext {
|
|
|
544
631
|
defineRealmLocal(this);
|
|
545
632
|
}
|
|
546
633
|
}
|
|
547
|
-
// A9 brands (contracts/embedder-api.md §"Module identity"): the
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
634
|
+
// A9 brands (contracts/embedder-api.md §"Module identity"): the STATEFUL
|
|
635
|
+
// embedder-facing handle classes. Their machinery lives in the copy that
|
|
636
|
+
// minted them, so the brand never makes a foreign handle usable — it makes
|
|
637
|
+
// it DIAGNOSABLE, at the lowering sites below. `StreamWriter` gains its
|
|
638
|
+
// brand with amendment A22 (§"The host-ABI surface and its version"):
|
|
639
|
+
// writers carried none before because nothing needed to recognize one, and
|
|
640
|
+
// `isStreamWriter` now does.
|
|
551
641
|
defineBrand(Stream.prototype, STREAM);
|
|
642
|
+
defineBrand(StreamWriter.prototype, STREAM_WRITER);
|
|
552
643
|
defineBrand(Future.prototype, FUTURE);
|
|
553
644
|
defineBrand(ErrorContext.prototype, ERROR_CONTEXT);
|
|
554
645
|
/**
|
package/esm/exec/host_streams.js
CHANGED
|
@@ -186,6 +186,55 @@ export class HostBuffer {
|
|
|
186
186
|
}
|
|
187
187
|
return out;
|
|
188
188
|
}
|
|
189
|
+
// --- A21 `ByteWindow` (embedder-api amendment A21, polyengine#128) ---
|
|
190
|
+
//
|
|
191
|
+
// A host buffer can be the PEER of a direct session on the other end of a
|
|
192
|
+
// host↔host rendezvous. Which of the two shapes it takes follows from the
|
|
193
|
+
// direction it was built for, exactly as `read`/`write` above do:
|
|
194
|
+
//
|
|
195
|
+
// * SOURCE (`values !== null`, a parked `write`): the window is a view of
|
|
196
|
+
// the offered chunk itself — the A5 borrow, scoped to the callback. No
|
|
197
|
+
// extra copy at all.
|
|
198
|
+
// * DESTINATION (`values === null`, a parked/arriving `read(max)`): there
|
|
199
|
+
// is no landing zone to view, so the window is a fresh scratch; the
|
|
200
|
+
// marked prefix becomes the delivered chunk (ownership passes with it,
|
|
201
|
+
// and `taken()` hands a sole chunk through unsliced).
|
|
202
|
+
/** The synthesized destination window, live for one direct invocation. */
|
|
203
|
+
#scratch = null;
|
|
204
|
+
byteView(n) {
|
|
205
|
+
assert_(n <= this.remain(), "host direct window beyond remaining");
|
|
206
|
+
if (this.values === null) {
|
|
207
|
+
// Stable for the whole invocation: `remaining()` re-derives on every
|
|
208
|
+
// call and the producer's earlier `set()`s must survive that.
|
|
209
|
+
if (this.#scratch === null || this.#scratch.length !== n) {
|
|
210
|
+
this.#scratch = new Uint8Array(n);
|
|
211
|
+
}
|
|
212
|
+
return this.#scratch;
|
|
213
|
+
}
|
|
214
|
+
assert_(this.values instanceof Uint8Array, "host direct window on a non-u8 chunk");
|
|
215
|
+
return this.values.subarray(this.progress, this.progress + n);
|
|
216
|
+
}
|
|
217
|
+
advanceBytes(k) {
|
|
218
|
+
assert_(k >= 0 && k <= this.remain(), "host direct advance beyond remaining");
|
|
219
|
+
if (this.values === null) {
|
|
220
|
+
// A callback may mark bytes it never actually looked at the window to
|
|
221
|
+
// write (nonsense, but the runtime must stay total rather than trip an
|
|
222
|
+
// internal assertion). The acknowledged prefix is then whatever the
|
|
223
|
+
// synthesized landing zone held — zeroes — which is the faithful
|
|
224
|
+
// analogue of the guest-peer case, where it would be whatever the
|
|
225
|
+
// reader's memory already contained.
|
|
226
|
+
const scratch = this.#scratch ?? new Uint8Array(k);
|
|
227
|
+
// Delivered as an owned chunk; `write` is the same call the reference
|
|
228
|
+
// copy would have made, so `remain()`/`taken()` stay consistent.
|
|
229
|
+
this.write(scratch.subarray(0, k));
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
this.progress += k;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
endWindow() {
|
|
236
|
+
this.#scratch = null;
|
|
237
|
+
}
|
|
189
238
|
}
|
|
190
239
|
/**
|
|
191
240
|
* Every live `HostActivity` arm, by identity. These are the promises this
|
|
@@ -418,6 +467,209 @@ class HostActivity {
|
|
|
418
467
|
this.#arm();
|
|
419
468
|
}
|
|
420
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* The `DirectDestination`/`DirectSource` object itself. One per INVOCATION,
|
|
472
|
+
* not per session: "the object dies when the callback returns" is the
|
|
473
|
+
* contract's validity window, and every later method call throws a
|
|
474
|
+
* `TypeError` naming the rule.
|
|
475
|
+
*/
|
|
476
|
+
class DirectScope {
|
|
477
|
+
peer;
|
|
478
|
+
capacity;
|
|
479
|
+
marked = 0;
|
|
480
|
+
#live = true;
|
|
481
|
+
constructor(peer,
|
|
482
|
+
/** The peer's actual remaining capacity — never the parked sentinel. */
|
|
483
|
+
capacity) {
|
|
484
|
+
this.peer = peer;
|
|
485
|
+
this.capacity = capacity;
|
|
486
|
+
}
|
|
487
|
+
remaining() {
|
|
488
|
+
this.#check();
|
|
489
|
+
// Re-derived per call: `byteView` is grow-safe for a guest peer, and the
|
|
490
|
+
// `subarray` accounts for the marks made so far in this invocation.
|
|
491
|
+
return this.peer.byteView(this.capacity).subarray(this.marked);
|
|
492
|
+
}
|
|
493
|
+
markWritten(n) {
|
|
494
|
+
this.#mark(n, "markWritten");
|
|
495
|
+
}
|
|
496
|
+
markRead(n) {
|
|
497
|
+
this.#mark(n, "markRead");
|
|
498
|
+
}
|
|
499
|
+
#mark(n, who) {
|
|
500
|
+
this.#check();
|
|
501
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
502
|
+
throw new TypeError(`${who}(${n}): a direct-access mark must be a non-negative integer`);
|
|
503
|
+
}
|
|
504
|
+
if (this.marked + n > this.capacity) {
|
|
505
|
+
throw new TypeError(`${who}(${n}) would take the invocation's cumulative mark to ` +
|
|
506
|
+
`${this.marked + n}, past the ${this.capacity} byte(s) the view ` +
|
|
507
|
+
`held on entry (embedder-api amendment A21)`);
|
|
508
|
+
}
|
|
509
|
+
this.marked += n;
|
|
510
|
+
}
|
|
511
|
+
#check() {
|
|
512
|
+
if (!this.#live) {
|
|
513
|
+
throw new TypeError("this direct-access view is dead: a DirectDestination/DirectSource " +
|
|
514
|
+
"is scoped to the synchronous callback invocation it was passed " +
|
|
515
|
+
"to, and retaining one past its return is misuse (embedder-api " +
|
|
516
|
+
"amendment A21, polyengine#128)");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* End of the invocation: the object is dead, and every later method call
|
|
521
|
+
* throws. Releasing the peer's synthesized window is the caller's job
|
|
522
|
+
* (`DirectSession.runDirect`), because it must happen strictly after the
|
|
523
|
+
* acknowledged marks are applied.
|
|
524
|
+
*/
|
|
525
|
+
die() {
|
|
526
|
+
this.#live = false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* A parked direct session, as both halves see it: a `DirectBuffer` to the
|
|
531
|
+
* rendezvous (task/streams.ts) and a promise to the embedder.
|
|
532
|
+
*
|
|
533
|
+
* It presents the ordinary buffer surface so the reference control flow keeps
|
|
534
|
+
* working unchanged — `remain()` answers a positive SENTINEL while the session
|
|
535
|
+
* is live, which only ever feeds the rendezvous' `min()` and so resolves to
|
|
536
|
+
* the peer's real capacity — but `read`/`write` are unreachable: the seam
|
|
537
|
+
* routes a direct buffer through `runDirect` instead.
|
|
538
|
+
*/
|
|
539
|
+
class DirectSession {
|
|
540
|
+
t;
|
|
541
|
+
invoke;
|
|
542
|
+
direct = true;
|
|
543
|
+
/** Bytes acknowledged across the whole session. */
|
|
544
|
+
total = 0;
|
|
545
|
+
/** The callback said `"done"`, or the session failed / was settled. */
|
|
546
|
+
ended = false;
|
|
547
|
+
/** `ended` because the callback said so (A7 precision; see `DirectSessionInfo`). */
|
|
548
|
+
endedByVerdict = false;
|
|
549
|
+
/** Installed in the shared object's pending slot right now. */
|
|
550
|
+
pending = false;
|
|
551
|
+
/** `cancelWrite`/`cancelRead` arrived; stop at the next loop top. */
|
|
552
|
+
cancelled = false;
|
|
553
|
+
#settle = null;
|
|
554
|
+
#reject = null;
|
|
555
|
+
constructor(t, invoke) {
|
|
556
|
+
this.t = t;
|
|
557
|
+
this.invoke = invoke;
|
|
558
|
+
}
|
|
559
|
+
// --- buffer surface (definitions.py `Buffer`) ---
|
|
560
|
+
remain() {
|
|
561
|
+
// The sentinel is `Buffer.MAX_LENGTH`, the largest value the rendezvous
|
|
562
|
+
// can legally see; it never surfaces to the embedder because the scope is
|
|
563
|
+
// built from `min(peer.remain(), sentinel)`.
|
|
564
|
+
return this.ended ? 0 : BUFFER_MAX_LENGTH;
|
|
565
|
+
}
|
|
566
|
+
isZeroLength() {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
read(_n) {
|
|
570
|
+
throw new Error("internal: a direct session must go through the A21 seam");
|
|
571
|
+
}
|
|
572
|
+
write(_vs) {
|
|
573
|
+
throw new Error("internal: a direct session must go through the A21 seam");
|
|
574
|
+
}
|
|
575
|
+
// --- the direct protocol ---
|
|
576
|
+
runDirect(peer, n) {
|
|
577
|
+
const scope = new DirectScope(peer, n);
|
|
578
|
+
try {
|
|
579
|
+
return this.#runDirect(scope, peer);
|
|
580
|
+
}
|
|
581
|
+
finally {
|
|
582
|
+
// Release any window the peer SYNTHESIZED (a `HostBuffer` destination's
|
|
583
|
+
// scratch). Strictly after `advanceBytes`, which is what turns the
|
|
584
|
+
// marked prefix of that scratch into the delivered chunk.
|
|
585
|
+
peer.endWindow?.();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
#runDirect(scope, peer) {
|
|
589
|
+
let verdict;
|
|
590
|
+
try {
|
|
591
|
+
verdict = this.invoke(scope);
|
|
592
|
+
}
|
|
593
|
+
catch (e) {
|
|
594
|
+
// "A callback that throws rejects the session with that error, and the
|
|
595
|
+
// invocation's marks are discarded" — so nothing touches `peer`.
|
|
596
|
+
scope.die();
|
|
597
|
+
this.#fail(e);
|
|
598
|
+
return "failed";
|
|
599
|
+
}
|
|
600
|
+
scope.die();
|
|
601
|
+
if (verdict !== "more" && verdict !== "done") {
|
|
602
|
+
this.#fail(new TypeError(`a direct-access callback must return "more" or "done", got ` +
|
|
603
|
+
`${JSON.stringify(verdict)} (embedder-api amendment A21)`));
|
|
604
|
+
return "failed";
|
|
605
|
+
}
|
|
606
|
+
const k = scope.marked;
|
|
607
|
+
if (k === 0) {
|
|
608
|
+
if (verdict === "done") {
|
|
609
|
+
// Retraction: the speculative-park correction. The session ends with
|
|
610
|
+
// its running total and the peer's operation stays parked.
|
|
611
|
+
this.ended = true;
|
|
612
|
+
this.endedByVerdict = true;
|
|
613
|
+
return "retracted";
|
|
614
|
+
}
|
|
615
|
+
this.#fail(new TypeError('a direct-access callback returned "more" without marking any ' +
|
|
616
|
+
"bytes; a session that has nothing to offer retracts by " +
|
|
617
|
+
'returning "done" (embedder-api amendment A21, polyengine#128)'));
|
|
618
|
+
return "failed";
|
|
619
|
+
}
|
|
620
|
+
// Marks acknowledge ON CLEAN RETURN ONLY: this is the first and only
|
|
621
|
+
// place the peer's progress moves, and it completes the copy with `k`.
|
|
622
|
+
peer.advanceBytes(k);
|
|
623
|
+
this.total += k;
|
|
624
|
+
if (verdict === "done") {
|
|
625
|
+
this.ended = true;
|
|
626
|
+
this.endedByVerdict = true;
|
|
627
|
+
}
|
|
628
|
+
return "copied";
|
|
629
|
+
}
|
|
630
|
+
failDirect(error) {
|
|
631
|
+
this.#fail(error);
|
|
632
|
+
}
|
|
633
|
+
// --- promise plumbing ---
|
|
634
|
+
/** Arm the settle hooks for one issuance of this session. */
|
|
635
|
+
arm(settle, reject) {
|
|
636
|
+
this.#settle = settle;
|
|
637
|
+
this.#reject = reject;
|
|
638
|
+
}
|
|
639
|
+
#take() {
|
|
640
|
+
const s = this.#settle, r = this.#reject;
|
|
641
|
+
this.#settle = null;
|
|
642
|
+
this.#reject = null;
|
|
643
|
+
return [s, r];
|
|
644
|
+
}
|
|
645
|
+
#fail(e) {
|
|
646
|
+
this.ended = true;
|
|
647
|
+
this.pending = false;
|
|
648
|
+
const [, r] = this.#take();
|
|
649
|
+
r?.(e);
|
|
650
|
+
}
|
|
651
|
+
/** The session is over; the driving loop resolves with `total`. */
|
|
652
|
+
finish() {
|
|
653
|
+
this.ended = true;
|
|
654
|
+
this.pending = false;
|
|
655
|
+
const [s] = this.#take();
|
|
656
|
+
s?.("done");
|
|
657
|
+
}
|
|
658
|
+
/** This issuance rendezvoused but the session lives; re-issue it. */
|
|
659
|
+
reissue() {
|
|
660
|
+
this.pending = false;
|
|
661
|
+
const [s] = this.#take();
|
|
662
|
+
s?.("reissue");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/** A21 is `stream<u8>` only; `null` (zero-width) is not u8 either. */
|
|
666
|
+
function requireU8Element(t, who) {
|
|
667
|
+
if (t === null || despecialize(t).kind !== "u8") {
|
|
668
|
+
throw new TypeError(`${who} is available on stream<u8> only; this stream's element type ` +
|
|
669
|
+
`is ${t === null ? "the zero-width payload" : despecialize(t).kind} ` +
|
|
670
|
+
`(embedder-api amendment A21, polyengine#128)`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
421
673
|
/**
|
|
422
674
|
* Attach host-activity bookkeeping to a shared object at the CABI seam.
|
|
423
675
|
*
|
|
@@ -516,6 +768,92 @@ function mkStreamEnds(shared, activity) {
|
|
|
516
768
|
else
|
|
517
769
|
activity.notify();
|
|
518
770
|
};
|
|
771
|
+
/** The live direct session on each end, if any (A21, polyengine#128). */
|
|
772
|
+
const direct = {
|
|
773
|
+
read: null,
|
|
774
|
+
write: null,
|
|
775
|
+
};
|
|
776
|
+
/**
|
|
777
|
+
* Drive one direct session from park to end (A21).
|
|
778
|
+
*
|
|
779
|
+
* Two shapes reach us, and the difference is *which side arrived second*:
|
|
780
|
+
*
|
|
781
|
+
* * the session is the PENDING side — every rendezvous fires `onCopy`, and
|
|
782
|
+
* the `"more"` verdict simply declines to `reclaim()`, so the session
|
|
783
|
+
* stays in the pending slot for the next peer operation. This is
|
|
784
|
+
* `write()`'s "stay parked until the offer is exhausted" mechanism, with
|
|
785
|
+
* the callback's verdict in place of `buf.remain() > 0`.
|
|
786
|
+
* * the session ARRIVED second — the rendezvous completes it with
|
|
787
|
+
* `onCopyDone(COMPLETED)`, so a `"more"` verdict has to re-issue. The
|
|
788
|
+
* re-issue rides the loop below (one `await` apart), which is exactly
|
|
789
|
+
* `writeAll`'s re-offer shape and therefore inherits its ordering: the
|
|
790
|
+
* peer's pending event is delivered and its buffer reclaimed before we
|
|
791
|
+
* can rendezvous against it a second time.
|
|
792
|
+
*/
|
|
793
|
+
const runDirectSession = async (side, session) => {
|
|
794
|
+
parked[side] = true;
|
|
795
|
+
direct[side] = session;
|
|
796
|
+
try {
|
|
797
|
+
for (;;) {
|
|
798
|
+
if (session.cancelled)
|
|
799
|
+
break;
|
|
800
|
+
const step = await new Promise((res, rej) => {
|
|
801
|
+
session.arm(res, rej);
|
|
802
|
+
session.pending = true;
|
|
803
|
+
const onCopy = (reclaim) => {
|
|
804
|
+
if (!session.ended)
|
|
805
|
+
return; // "more": stay parked
|
|
806
|
+
reclaim();
|
|
807
|
+
activity.notify();
|
|
808
|
+
session.finish();
|
|
809
|
+
};
|
|
810
|
+
const onCopyDone = (result) => {
|
|
811
|
+
session.pending = false;
|
|
812
|
+
settle(result);
|
|
813
|
+
// COMPLETED with the session still live == the arriving-side
|
|
814
|
+
// rendezvous above; anything else (DROPPED, CANCELLED, or the
|
|
815
|
+
// retraction path through `reset_and_notify_pending`) ends it.
|
|
816
|
+
if (result === CopyResult.COMPLETED && !session.ended) {
|
|
817
|
+
session.reissue();
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
session.finish();
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
if (side === "write") {
|
|
824
|
+
shared.write(writeInst, session, onCopy, onCopyDone);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
shared.read(readInst, session, onCopy, onCopyDone);
|
|
828
|
+
}
|
|
829
|
+
activity.notify();
|
|
830
|
+
activity.pump();
|
|
831
|
+
});
|
|
832
|
+
if (step === "done")
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
finally {
|
|
837
|
+
parked[side] = false;
|
|
838
|
+
direct[side] = null;
|
|
839
|
+
}
|
|
840
|
+
return session.total;
|
|
841
|
+
};
|
|
842
|
+
/** Shared tail of `cancelWrite`/`cancelRead` for a parked direct session. */
|
|
843
|
+
const cancelDirect = (session) => {
|
|
844
|
+
// A21: cancelling RETRACTS the session — it resolves with its running
|
|
845
|
+
// total (A8's indistinguishability caveats unchanged). `shared.cancel()`
|
|
846
|
+
// only when the session actually holds the pending slot: a session caught
|
|
847
|
+
// between two issuances holds nothing, and `SharedBase.cancel` asserts
|
|
848
|
+
// that something is pending.
|
|
849
|
+
session.cancelled = true;
|
|
850
|
+
if (session.pending)
|
|
851
|
+
shared.cancel();
|
|
852
|
+
else
|
|
853
|
+
session.finish();
|
|
854
|
+
activity.notify();
|
|
855
|
+
activity.pump();
|
|
856
|
+
};
|
|
519
857
|
return {
|
|
520
858
|
writable: {
|
|
521
859
|
write(values) {
|
|
@@ -579,9 +917,29 @@ function mkStreamEnds(shared, activity) {
|
|
|
579
917
|
}
|
|
580
918
|
return sent;
|
|
581
919
|
},
|
|
920
|
+
writeDirect(produce, info) {
|
|
921
|
+
// Same one-in-flight-per-end rule, same wording shape as `write`:
|
|
922
|
+
// `writeDirect` participates in it exactly as `write` does (A21).
|
|
923
|
+
if (parked.write) {
|
|
924
|
+
throw new TypeError("a write is already in flight on this stream's writable end; " +
|
|
925
|
+
"await it or cancelWrite() first");
|
|
926
|
+
}
|
|
927
|
+
requireU8Element(shared.t, "writeDirect");
|
|
928
|
+
const session = new DirectSession(shared.t, (scope) => produce(scope));
|
|
929
|
+
const p = runDirectSession("write", session);
|
|
930
|
+
if (info === undefined)
|
|
931
|
+
return p;
|
|
932
|
+
return p.then((n) => {
|
|
933
|
+
info.endedByVerdict = session.endedByVerdict;
|
|
934
|
+
return n;
|
|
935
|
+
});
|
|
936
|
+
},
|
|
582
937
|
cancelWrite() {
|
|
583
938
|
if (!parked.write)
|
|
584
939
|
return;
|
|
940
|
+
const session = direct.write;
|
|
941
|
+
if (session !== null)
|
|
942
|
+
return cancelDirect(session);
|
|
585
943
|
parked.write = false;
|
|
586
944
|
shared.cancel();
|
|
587
945
|
activity.notify();
|
|
@@ -622,6 +980,21 @@ function mkStreamEnds(shared, activity) {
|
|
|
622
980
|
activity.pump();
|
|
623
981
|
});
|
|
624
982
|
},
|
|
983
|
+
readDirect(consume, info) {
|
|
984
|
+
if (parked.read) {
|
|
985
|
+
throw new TypeError("a read is already in flight on this stream's readable end; " +
|
|
986
|
+
"await it or cancelRead() first");
|
|
987
|
+
}
|
|
988
|
+
requireU8Element(shared.t, "readDirect");
|
|
989
|
+
const session = new DirectSession(shared.t, (scope) => consume(scope));
|
|
990
|
+
const p = runDirectSession("read", session);
|
|
991
|
+
if (info === undefined)
|
|
992
|
+
return p;
|
|
993
|
+
return p.then((n) => {
|
|
994
|
+
info.endedByVerdict = session.endedByVerdict;
|
|
995
|
+
return n;
|
|
996
|
+
});
|
|
997
|
+
},
|
|
625
998
|
cancelRead() {
|
|
626
999
|
// #97, DELIBERATE AND PINNED: cancelling resolves the in-flight
|
|
627
1000
|
// `read` promise with whatever the buffer took so far — for a read
|
|
@@ -635,6 +1008,9 @@ function mkStreamEnds(shared, activity) {
|
|
|
635
1008
|
// this state — a guest cannot cancel the host's read.
|
|
636
1009
|
if (!parked.read)
|
|
637
1010
|
return;
|
|
1011
|
+
const session = direct.read;
|
|
1012
|
+
if (session !== null)
|
|
1013
|
+
return cancelDirect(session);
|
|
638
1014
|
parked.read = false;
|
|
639
1015
|
shared.cancel();
|
|
640
1016
|
activity.notify();
|
package/esm/task/streams.js
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
// expressible.
|
|
46
46
|
import { defineBrand, ERROR_CONTEXT } from "@polyengine/protocol";
|
|
47
47
|
import { assert_, Trap, trapIf } from "../cabi/trap.js";
|
|
48
|
+
import { bytesOf } from "../cabi/memory.js";
|
|
48
49
|
import { loadListFromValidRange } from "../cabi/load.js";
|
|
49
50
|
import { storeListIntoValidRange } from "../cabi/store.js";
|
|
50
51
|
import { alignment, alignTo, elemSize } from "../cabi/layout.js";
|
|
@@ -141,6 +142,69 @@ export class GuestBuffer {
|
|
|
141
142
|
}
|
|
142
143
|
this.progress += vs.length;
|
|
143
144
|
}
|
|
145
|
+
// --- A21 direct-access byte edges (embedder-api amendment A21, #128) ---
|
|
146
|
+
//
|
|
147
|
+
// `ByteWindow`, implemented for the `stream<u8>` case only. The two methods
|
|
148
|
+
// together are the copy `read`/`write` would have done, split so that the
|
|
149
|
+
// *peer's* callback performs it: `byteView` hands out the range, and
|
|
150
|
+
// `advanceBytes` records the bytes that actually moved. They are role-blind
|
|
151
|
+
// (destination or source) because `this.ptr` already advances on BOTH
|
|
152
|
+
// `read` and `write` above, and `elemSize(u8) === 1`.
|
|
153
|
+
/**
|
|
154
|
+
* A fresh view over the next `n` bytes of this buffer's remaining range.
|
|
155
|
+
*
|
|
156
|
+
* Fresh on every call, via `bytesOf` (cabi/memory.ts:195) over the
|
|
157
|
+
* `LiveMemory` getters — so a `memory.grow` between two rendezvous of one
|
|
158
|
+
* parked direct session never yields a view onto the detached buffer.
|
|
159
|
+
*/
|
|
160
|
+
byteView(n) {
|
|
161
|
+
assert_(this.t !== null && despecialize(this.t).kind === "u8", "direct byte window on a non-u8 buffer");
|
|
162
|
+
assert_(n <= this.remain(), "direct byte window beyond remaining");
|
|
163
|
+
const mem = this.cx.opts.memory;
|
|
164
|
+
assert_(mem !== null, "direct byte window requires a memory");
|
|
165
|
+
return bytesOf(mem, this.ptr, n);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Advance by `k` WITHOUT copying: the bytes already moved through the view
|
|
169
|
+
* `byteView` handed out. Called by the seam only after the direct callback
|
|
170
|
+
* returned cleanly, which is what makes marks acknowledge-on-clean-return.
|
|
171
|
+
*/
|
|
172
|
+
advanceBytes(k) {
|
|
173
|
+
assert_(k >= 0 && k <= this.remain(), "direct advance beyond remaining");
|
|
174
|
+
this.ptr += k; // elemSize(u8) === 1
|
|
175
|
+
this.progress += k;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function isDirectBuffer(b) {
|
|
179
|
+
return b.direct === true;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The one copy site, shared by `SharedStreamImpl.read` and `.write`.
|
|
183
|
+
*
|
|
184
|
+
* Collapses to definitions.py's `dst_buffer.write(src_buffer.read(n))`
|
|
185
|
+
* whenever neither side is a direct session — which is every guest↔guest,
|
|
186
|
+
* guest↔host-chunk and host-chunk↔host-chunk rendezvous, i.e. everything
|
|
187
|
+
* that existed before A21.
|
|
188
|
+
*/
|
|
189
|
+
function rendezvousCopy(src, dst, n) {
|
|
190
|
+
const srcDirect = isDirectBuffer(src);
|
|
191
|
+
const dstDirect = isDirectBuffer(dst);
|
|
192
|
+
if (!srcDirect && !dstDirect) {
|
|
193
|
+
dst.write(src.read(n));
|
|
194
|
+
return "chunk";
|
|
195
|
+
}
|
|
196
|
+
if (srcDirect && dstDirect)
|
|
197
|
+
return "both-direct";
|
|
198
|
+
return srcDirect
|
|
199
|
+
? src.runDirect(dst, n)
|
|
200
|
+
: dst.runDirect(src, n);
|
|
201
|
+
}
|
|
202
|
+
/** The A21 rejection for a rendezvous of two direct sessions. */
|
|
203
|
+
function bothDirectError() {
|
|
204
|
+
return new TypeError("at least one side of a host-to-host rendezvous must use the chunk " +
|
|
205
|
+
"forms: two direct-access sessions cannot rendezvous with each other " +
|
|
206
|
+
"because neither side owns the memory the other would write into " +
|
|
207
|
+
"(embedder-api amendment A21, polyengine#128)");
|
|
144
208
|
}
|
|
145
209
|
/**
|
|
146
210
|
* definitions.py `none_or_number_type` (line 1070). Guards the "temporary"
|
|
@@ -272,7 +336,19 @@ export class SharedStreamImpl {
|
|
|
272
336
|
if (this.pendingBuffer.remain() > 0) {
|
|
273
337
|
if (dstBuffer.remain() > 0) {
|
|
274
338
|
const n = Math.min(dstBuffer.remain(), this.pendingBuffer.remain());
|
|
275
|
-
|
|
339
|
+
// A21 seam (#128). `"chunk"` is the reference line verbatim.
|
|
340
|
+
const pendingIsDirect = isDirectBuffer(this.pendingBuffer);
|
|
341
|
+
const out = rendezvousCopy(this.pendingBuffer, dstBuffer, n);
|
|
342
|
+
if (out === "both-direct") {
|
|
343
|
+
// The ARRIVING side (here the reader) is the one refused; the
|
|
344
|
+
// parked session keeps the pending slot, undisturbed.
|
|
345
|
+
dstBuffer.failDirect(bothDirectError());
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (out === "retracted" || out === "failed") {
|
|
349
|
+
this.#routeDirectNoCopy(out, pendingIsDirect, inst, dstBuffer, onCopy, onCopyDone);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
276
352
|
this.pendingOnCopy(() => this.resetPending());
|
|
277
353
|
}
|
|
278
354
|
onCopyDone(CopyResult.COMPLETED);
|
|
@@ -298,7 +374,19 @@ export class SharedStreamImpl {
|
|
|
298
374
|
if (this.pendingBuffer.remain() > 0) {
|
|
299
375
|
if (srcBuffer.remain() > 0) {
|
|
300
376
|
const n = Math.min(srcBuffer.remain(), this.pendingBuffer.remain());
|
|
301
|
-
|
|
377
|
+
// A21 seam (#128). `"chunk"` is the reference line verbatim.
|
|
378
|
+
const pendingIsDirect = isDirectBuffer(this.pendingBuffer);
|
|
379
|
+
const out = rendezvousCopy(srcBuffer, this.pendingBuffer, n);
|
|
380
|
+
if (out === "both-direct") {
|
|
381
|
+
// The ARRIVING side (here the writer) is refused; the parked
|
|
382
|
+
// session keeps the pending slot.
|
|
383
|
+
srcBuffer.failDirect(bothDirectError());
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (out === "retracted" || out === "failed") {
|
|
387
|
+
this.#routeDirectNoCopy(out, pendingIsDirect, inst, srcBuffer, onCopy, onCopyDone);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
302
390
|
this.pendingOnCopy(() => this.resetPending());
|
|
303
391
|
}
|
|
304
392
|
onCopyDone(CopyResult.COMPLETED);
|
|
@@ -315,6 +403,43 @@ export class SharedStreamImpl {
|
|
|
315
403
|
}
|
|
316
404
|
}
|
|
317
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* A21 (#128): route a rendezvous whose direct session did NOT copy.
|
|
408
|
+
*
|
|
409
|
+
* Two outcomes land here, and both share one invariant: the peer's parked
|
|
410
|
+
* operation survives, no event is delivered, and the stream is not dropped
|
|
411
|
+
* — a runtime never emits a zero-progress COMPLETED copy, which is
|
|
412
|
+
* unreachable in definitions.py for a nonzero-capacity operation.
|
|
413
|
+
*
|
|
414
|
+
* * `"retracted"` — `"done"` with zero marked. The session ends and
|
|
415
|
+
* resolves with its running total, through the ordinary
|
|
416
|
+
* `on_copy_done(COMPLETED)` channel.
|
|
417
|
+
* * `"failed"` — misuse or a throwing callback. The session has ALREADY
|
|
418
|
+
* rejected (`DirectSession.#fail`), so it must be retired silently:
|
|
419
|
+
* its rejection is its notification.
|
|
420
|
+
*
|
|
421
|
+
* Which side was the session decides where each goes, and both shapes are
|
|
422
|
+
* states definitions.py already produces:
|
|
423
|
+
*
|
|
424
|
+
* * PARKED session ⇒ the "the parked side had nothing left" branch
|
|
425
|
+
* (definitions.py:1043/1063): retire it and park the arriving
|
|
426
|
+
* operation, which gets no event either way.
|
|
427
|
+
* * ARRIVING session ⇒ the "arriving buffer of zero capacity" state
|
|
428
|
+
* (definitions.py:1041/1057): the pending side is left untouched with
|
|
429
|
+
* its `on_copy` unfired, and the arriving side completes.
|
|
430
|
+
*/
|
|
431
|
+
#routeDirectNoCopy(out, pendingIsDirect, inst, arriving, onCopy, onCopyDone) {
|
|
432
|
+
if (pendingIsDirect) {
|
|
433
|
+
if (out === "retracted")
|
|
434
|
+
this.resetAndNotifyPending(CopyResult.COMPLETED);
|
|
435
|
+
else
|
|
436
|
+
this.resetPending();
|
|
437
|
+
this.setPending(inst, arriving, onCopy, onCopyDone);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (out === "retracted")
|
|
441
|
+
onCopyDone(CopyResult.COMPLETED);
|
|
442
|
+
}
|
|
318
443
|
#assertSameElemType(b) {
|
|
319
444
|
// Structural, not identity: definitions.py compares dataclass types with
|
|
320
445
|
// `==`, and our `ValType`s are fresh objects per table (the plan's type
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polyengine/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A WebAssembly Component Model host for JavaScript engines: plan executor, canonical ABI, 0.3 task scheduler, JSPI bridge, and embedder API.",
|
|
5
5
|
"homepage": "https://github.com/polymorph-components/polyengine#readme",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@polyengine/protocol": "^0.2.
|
|
58
|
+
"@polyengine/protocol": "^0.2.2"
|
|
59
59
|
},
|
|
60
60
|
"_generatedBy": "dnt@0.43.2"
|
|
61
61
|
}
|
package/types/embedder/copy.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare const COPY_URL: string;
|
|
|
17
17
|
* @internal — copy-identity constant for the A9 multi-copy diagnostics; not
|
|
18
18
|
* host-facing.
|
|
19
19
|
*/
|
|
20
|
-
export declare const RUNTIME_VERSION = "0.
|
|
20
|
+
export declare const RUNTIME_VERSION = "0.5.0";
|
|
21
21
|
/**
|
|
22
22
|
* Compose a cross-copy diagnostic: what was foreign, which copy is speaking,
|
|
23
23
|
* the census of every copy in the graph, and the by-value remediation.
|
package/types/embedder/mod.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export { COPY_URL, RUNTIME_VERSION } from "./copy.js";
|
|
2
|
-
export { copyCensus, defineRealmLocal, DROPPED, ERROR_CONTEXT, fromCloneable, FUTURE, hasBrand, INVALID_HANDLE, isDroppedError, isInvalidHandleError, isPeerTrappedError, isRealmLocal, isStreamProducerError, isSuspending, isTrap, isComponentException, PEER_TRAPPED, PROTOCOL_GENERATION, REALM_LOCAL, registerRuntimeCopy, RESOURCE_STATE, type RuntimeCopy, runtimeCopies, STREAM, STREAM_PRODUCER, SUSPENDING, toCloneable, TRAP, COMPONENT_EXCEPTION, } from "@polyengine/protocol";
|
|
3
2
|
export { artifactsFromEnvelope, type ComponentArtifacts, type EmbedderInstance, type EmbedderOptions, type InstantiateSource, type UntranslatedArtifacts, instantiate, instantiateEmbedder, resolveArtifacts, } from "./instantiate.js";
|
|
4
3
|
export { type FuncSummary, type ImportLeaf, type PlanLike, requiredImports } from "./imports.js";
|
|
5
|
-
export {
|
|
6
|
-
export { type
|
|
4
|
+
export { NameCollisionError } from "./errors.js";
|
|
5
|
+
export { type ElemCodec } from "./streams.js";
|
|
6
|
+
import type { Stream as ProtocolStream, StreamWriter as ProtocolStreamWriter } from "@polyengine/protocol";
|
|
7
|
+
export declare function createStream<T>(): {
|
|
8
|
+
stream: ProtocolStream<T>;
|
|
9
|
+
writer: ProtocolStreamWriter<T>;
|
|
10
|
+
};
|
|
7
11
|
export { GuestResource, HostResourceRegistry } from "./resources.js";
|
|
8
12
|
export { camelCase, type LeafName, parseLeafName, pascalCase } from "./casing.js";
|
|
9
|
-
export { suspending } from "../jspi/suspending.js";
|
|
10
13
|
export { asTrackKeySpelling, compareSemver, ImportRegistrationError, ImportResolutionError, ImportResolver, type ParsedId, parseInterfaceId, parseSemver, type Semver, trackKey, } from "./version.js";
|
|
11
14
|
export { type AdapterOptions, BorrowScope, fromHost, toHost, type ValueBridge, } from "./values.js";
|
|
@@ -2,8 +2,8 @@ import type { ValType } from "../cabi/types.js";
|
|
|
2
2
|
import type { ComponentValue } from "../cabi/types.js";
|
|
3
3
|
import { type HostFuture, type HostStream } from "../exec/host_streams.js";
|
|
4
4
|
import { ErrorContext as InternalErrorContext } from "../task/mod.js";
|
|
5
|
-
|
|
6
|
-
export type Chunk
|
|
5
|
+
import { type Chunk, type DirectDestination, type DirectSource, type DirectVerdict, type ErrorContext as ProtocolErrorContext, type Future as ProtocolFuture, type Stream as ProtocolStream, type StreamWriter as ProtocolStreamWriter } from "@polyengine/protocol";
|
|
6
|
+
export type { Chunk } from "@polyengine/protocol";
|
|
7
7
|
/**
|
|
8
8
|
* Per-element adaptation, supplied by the value adapter.
|
|
9
9
|
* @internal — supplied by the value adapter, never by a host.
|
|
@@ -26,13 +26,14 @@ export interface ElemCodec<T> {
|
|
|
26
26
|
export { StreamProducerError } from "@polyengine/protocol";
|
|
27
27
|
/** True for `stream<u8>` / `future<u8>`, whose chunks are `Uint8Array`. */
|
|
28
28
|
export declare function isU8Element(element: ValType | null): boolean;
|
|
29
|
+
export type { DirectDestination, DirectSource, DirectVerdict } from "@polyengine/protocol";
|
|
29
30
|
/**
|
|
30
31
|
* A stream handle.
|
|
31
32
|
*
|
|
32
33
|
* `read` returning an empty chunk is end-of-stream, exactly as the contract
|
|
33
34
|
* spells it; `readable()` and the async iterator are built on it.
|
|
34
35
|
*/
|
|
35
|
-
export declare class Stream<T> {
|
|
36
|
+
export declare class Stream<T> implements ProtocolStream<T> {
|
|
36
37
|
#private;
|
|
37
38
|
private constructor();
|
|
38
39
|
/** Wrap a stream value that was lifted out of a guest. */
|
|
@@ -66,6 +67,29 @@ export declare class Stream<T> {
|
|
|
66
67
|
get codec(): ElemCodec<T> | null;
|
|
67
68
|
/** Low-level read: up to `max` elements; an empty chunk means end-of-stream. */
|
|
68
69
|
read(max: number): Promise<Chunk<T>>;
|
|
70
|
+
/**
|
|
71
|
+
* Consume the writer's bytes in place, without an intermediate chunk
|
|
72
|
+
* (`stream<u8>` only — contracts/embedder-api.md amendment A21,
|
|
73
|
+
* polyengine#128).
|
|
74
|
+
*
|
|
75
|
+
* At every rendezvous with a writer of nonzero capacity, `consume` runs
|
|
76
|
+
* exactly once, synchronously, with a `DirectSource` over the writer's
|
|
77
|
+
* unread bytes — guest linear memory when the peer is a guest, so the
|
|
78
|
+
* consumer's own `set()`/`subarray` copy IS the canonical-ABI copy.
|
|
79
|
+
* `"more"` keeps the session parked for the next rendezvous; `"done"` ends
|
|
80
|
+
* it. Resolves with the session's total byte count. Marking a prefix is
|
|
81
|
+
* normal: the writer re-offers the rest on its own schedule.
|
|
82
|
+
*
|
|
83
|
+
* `"done"` with zero bytes marked *retracts*: the session ends and the
|
|
84
|
+
* writer's operation stays parked, with no event delivered. `"more"` with
|
|
85
|
+
* zero marked, and a throwing callback, reject — and in both cases the
|
|
86
|
+
* writer's parked operation survives and the stream stays alive.
|
|
87
|
+
*
|
|
88
|
+
* Refusals mirror `read`: an unbound `Stream.create()` handle and a handle
|
|
89
|
+
* already passed to a guest (the A15 transfer guard) both throw, as does a
|
|
90
|
+
* non-`u8` element type.
|
|
91
|
+
*/
|
|
92
|
+
readDirect(consume: (src: DirectSource) => DirectVerdict): Promise<number>;
|
|
69
93
|
/**
|
|
70
94
|
* Cancel an in-flight `read` (R-fix review advisory 1).
|
|
71
95
|
*
|
|
@@ -103,7 +127,7 @@ export declare class Stream<T> {
|
|
|
103
127
|
[Symbol.asyncIterator](): AsyncIterator<Chunk<T>>;
|
|
104
128
|
}
|
|
105
129
|
/** Writer half of `Stream.create()`. */
|
|
106
|
-
export declare class StreamWriter<T> {
|
|
130
|
+
export declare class StreamWriter<T> implements ProtocolStreamWriter<T> {
|
|
107
131
|
#private;
|
|
108
132
|
constructor(stream: Stream<T>);
|
|
109
133
|
/**
|
|
@@ -117,6 +141,29 @@ export declare class StreamWriter<T> {
|
|
|
117
141
|
* window is misuse. Plain-array chunks are lowered (copied) up front.
|
|
118
142
|
*/
|
|
119
143
|
write(values: Chunk<T>): Promise<number>;
|
|
144
|
+
/**
|
|
145
|
+
* Fill the reader's landing zone in place, without an intermediate chunk
|
|
146
|
+
* (`stream<u8>` only — contracts/embedder-api.md amendment A21,
|
|
147
|
+
* polyengine#128).
|
|
148
|
+
*
|
|
149
|
+
* At every rendezvous with a reader of nonzero capacity, `produce` runs
|
|
150
|
+
* exactly once, synchronously, with a `DirectDestination` over the reader's
|
|
151
|
+
* unfilled landing zone — guest linear memory when the peer is a guest, so
|
|
152
|
+
* the producer's own `set()` IS the canonical-ABI copy and an external byte
|
|
153
|
+
* mover (a websocket frame, a SAB ring segment, a transferred
|
|
154
|
+
* `ArrayBuffer`) never pays a second copy inside the runtime. `"more"`
|
|
155
|
+
* keeps the session parked for the next rendezvous; `"done"` ends it.
|
|
156
|
+
* Resolves with the session's total byte count.
|
|
157
|
+
*
|
|
158
|
+
* `"done"` with zero bytes marked *retracts* (the session ends, the
|
|
159
|
+
* reader's operation stays parked, no event — the speculative-park
|
|
160
|
+
* correction); `"more"` with zero marked, and a throwing callback, reject.
|
|
161
|
+
*
|
|
162
|
+
* Parks until the element type is known, exactly as `write` does — a
|
|
163
|
+
* `Stream.create()` writer has no element type until the lowering site
|
|
164
|
+
* binds one — and then requires `u8`.
|
|
165
|
+
*/
|
|
166
|
+
writeDirect(produce: (dest: DirectDestination) => DirectVerdict): Promise<number>;
|
|
120
167
|
/** Offer values until all are taken or the reader goes away. */
|
|
121
168
|
writeAll(values: Chunk<T>): Promise<number>;
|
|
122
169
|
cancelWrite(): void;
|
|
@@ -131,7 +178,7 @@ export declare function publishHostStream<T>(s: Stream<T>, h: HostStream<T>): vo
|
|
|
131
178
|
* A future whose write end dropped without ever writing rejects with
|
|
132
179
|
* `DroppedError` — not `undefined`, which `future<void>` legitimately yields.
|
|
133
180
|
*/
|
|
134
|
-
export declare class Future<T> implements
|
|
181
|
+
export declare class Future<T> implements ProtocolFuture<T> {
|
|
135
182
|
#private;
|
|
136
183
|
private constructor();
|
|
137
184
|
static fromLifted<T>(value: ComponentValue, codec: ElemCodec<T>): Future<T>;
|
|
@@ -182,7 +229,7 @@ export declare class Future<T> implements PromiseLike<T> {
|
|
|
182
229
|
* The internal value is `task/streams.ts`'s `ErrorContext` (debug message
|
|
183
230
|
* only, per definitions.py).
|
|
184
231
|
*/
|
|
185
|
-
export declare class ErrorContext {
|
|
232
|
+
export declare class ErrorContext implements ProtocolErrorContext {
|
|
186
233
|
readonly message: string;
|
|
187
234
|
/** @internal — the internal value, preserved so it can be lowered back. */
|
|
188
235
|
readonly internal: InternalErrorContext;
|
|
@@ -37,6 +37,49 @@ export declare class HostBuffer {
|
|
|
37
37
|
* writer used.
|
|
38
38
|
*/
|
|
39
39
|
taken(): PayloadChunk;
|
|
40
|
+
byteView(n: number): Uint8Array;
|
|
41
|
+
advanceBytes(k: number): void;
|
|
42
|
+
endWindow(): void;
|
|
43
|
+
}
|
|
44
|
+
/** The scoped landing zone handed to a `writeDirect` producer (A21, #128). */
|
|
45
|
+
export interface DirectDestination {
|
|
46
|
+
/**
|
|
47
|
+
* The reader's still-unfilled bytes. Re-derived on every call (a
|
|
48
|
+
* `memory.grow` between two rendezvous of one session never yields a stale
|
|
49
|
+
* view) and shrinking by whatever has been marked so far in THIS
|
|
50
|
+
* invocation. DEAD once the callback returns.
|
|
51
|
+
*/
|
|
52
|
+
remaining(): Uint8Array;
|
|
53
|
+
/**
|
|
54
|
+
* Acknowledge bytes written into the view. Cumulative within the
|
|
55
|
+
* invocation; acknowledged only if the callback then returns cleanly.
|
|
56
|
+
*/
|
|
57
|
+
markWritten(n: number): void;
|
|
58
|
+
}
|
|
59
|
+
/** The scoped view handed to a `readDirect` consumer (A21, #128). */
|
|
60
|
+
export interface DirectSource {
|
|
61
|
+
/**
|
|
62
|
+
* The writer's unread bytes; read-only by contract. Same scoping and
|
|
63
|
+
* re-derivation rules as `DirectDestination.remaining`.
|
|
64
|
+
*/
|
|
65
|
+
remaining(): Uint8Array;
|
|
66
|
+
/** Acknowledge bytes consumed from the view. See `markWritten`. */
|
|
67
|
+
markRead(n: number): void;
|
|
68
|
+
}
|
|
69
|
+
/** The callback's poll cadence, spelled event-style (A21). */
|
|
70
|
+
export type DirectVerdict = "more" | "done";
|
|
71
|
+
/**
|
|
72
|
+
* Out-parameter of the low-level direct forms: `true` iff the session ended
|
|
73
|
+
* because the callback itself returned `"done"`, rather than because the peer
|
|
74
|
+
* dropped / the operation was cancelled / the peer's instance trapped.
|
|
75
|
+
*
|
|
76
|
+
* The conventions layer needs the distinction for A7 precision — a session
|
|
77
|
+
* the producer already completed keeps its resolution even if the peer then
|
|
78
|
+
* trapped — and `Promise<number>` is the contract's return shape, so it rides
|
|
79
|
+
* here rather than in the resolved value.
|
|
80
|
+
*/
|
|
81
|
+
export interface DirectSessionInfo {
|
|
82
|
+
endedByVerdict: boolean;
|
|
40
83
|
}
|
|
41
84
|
/** Host end the embedder WRITES; the guest reads. */
|
|
42
85
|
export interface HostWritableEnd<T> {
|
|
@@ -65,6 +108,24 @@ export interface HostWritableEnd<T> {
|
|
|
65
108
|
* if the reader dropped.
|
|
66
109
|
*/
|
|
67
110
|
writeAll(values: T[]): Promise<number>;
|
|
111
|
+
/**
|
|
112
|
+
* Park a **direct session** on this end (`stream<u8>` only — embedder-api
|
|
113
|
+
* amendment A21, polyengine#128).
|
|
114
|
+
*
|
|
115
|
+
* At every rendezvous with a reader of nonzero capacity, `produce` runs
|
|
116
|
+
* exactly once, synchronously, inside the rendezvous, with a
|
|
117
|
+
* `DirectDestination` over the reader's unfilled landing zone — guest linear
|
|
118
|
+
* memory when the peer is a guest, so the producer's own `set()` is the
|
|
119
|
+
* canonical-ABI copy. `"more"` keeps the session parked for the next
|
|
120
|
+
* rendezvous; `"done"` ends it. Resolves with the session's total.
|
|
121
|
+
*
|
|
122
|
+
* Marks acknowledge on clean return only. `"done"` with zero marked is
|
|
123
|
+
* *retraction* (the session ends, the reader's operation stays parked, no
|
|
124
|
+
* event); `"more"` with zero marked, and a throwing callback, reject.
|
|
125
|
+
*
|
|
126
|
+
* Participates in the one-in-flight-per-end rule exactly as `write` does.
|
|
127
|
+
*/
|
|
128
|
+
writeDirect(produce: (dest: DirectDestination) => DirectVerdict, info?: DirectSessionInfo): Promise<number>;
|
|
68
129
|
/**
|
|
69
130
|
* Cancel an in-flight `write`/`writeAll` (definitions.py
|
|
70
131
|
* `SharedStreamImpl.cancel` -> `CopyResult.CANCELLED`). No-op when nothing
|
|
@@ -92,6 +153,15 @@ export interface HostReadableEnd<T> {
|
|
|
92
153
|
* array.
|
|
93
154
|
*/
|
|
94
155
|
read(max: number): Promise<T[]>;
|
|
156
|
+
/**
|
|
157
|
+
* Park a **direct session** on this end (`stream<u8>` only — embedder-api
|
|
158
|
+
* amendment A21, polyengine#128). The mirror of
|
|
159
|
+
* `HostWritableEnd.writeDirect`: `consume` receives a `DirectSource` over
|
|
160
|
+
* the writer's unread bytes (a view of guest memory, or of the offered
|
|
161
|
+
* host chunk itself) and may take a prefix — a partial take is normal, and
|
|
162
|
+
* the writer re-offers on its own schedule.
|
|
163
|
+
*/
|
|
164
|
+
readDirect(consume: (src: DirectSource) => DirectVerdict, info?: DirectSessionInfo): Promise<number>;
|
|
95
165
|
/** Cancel an in-flight `read`; see `HostWritableEnd.cancelWrite`. */
|
|
96
166
|
cancelRead(): void;
|
|
97
167
|
drop(): void;
|
package/types/task/streams.d.ts
CHANGED
|
@@ -51,7 +51,90 @@ export declare class GuestBuffer {
|
|
|
51
51
|
read(n: number): PayloadChunk;
|
|
52
52
|
/** definitions.py `WritableBufferGuestImpl.write`. */
|
|
53
53
|
write(vs: PayloadChunk): void;
|
|
54
|
+
/**
|
|
55
|
+
* A fresh view over the next `n` bytes of this buffer's remaining range.
|
|
56
|
+
*
|
|
57
|
+
* Fresh on every call, via `bytesOf` (cabi/memory.ts:195) over the
|
|
58
|
+
* `LiveMemory` getters — so a `memory.grow` between two rendezvous of one
|
|
59
|
+
* parked direct session never yields a view onto the detached buffer.
|
|
60
|
+
*/
|
|
61
|
+
byteView(n: number): Uint8Array;
|
|
62
|
+
/**
|
|
63
|
+
* Advance by `k` WITHOUT copying: the bytes already moved through the view
|
|
64
|
+
* `byteView` handed out. Called by the seam only after the direct callback
|
|
65
|
+
* returned cleanly, which is what makes marks acknowledge-on-clean-return.
|
|
66
|
+
*/
|
|
67
|
+
advanceBytes(k: number): void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The buffer surface the rendezvous actually uses (definitions.py `Buffer`,
|
|
71
|
+
* line 918). `GuestBuffer` and the host layer's `HostBuffer` both satisfy it.
|
|
72
|
+
*/
|
|
73
|
+
export interface RendezvousBuffer {
|
|
74
|
+
remain(): number;
|
|
75
|
+
isZeroLength(): boolean;
|
|
76
|
+
read(n: number): PayloadChunk;
|
|
77
|
+
write(vs: PayloadChunk): void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A21: the peer half of a direct rendezvous — a buffer that can expose its
|
|
81
|
+
* remaining range as bytes and be advanced without a copy.
|
|
82
|
+
*
|
|
83
|
+
* Implemented by `GuestBuffer` (a view into guest linear memory: the
|
|
84
|
+
* embedder's own `set()` becomes the one ABI copy) and by `HostBuffer` (a
|
|
85
|
+
* view of the offered chunk when it is the source; a synthesized scratch that
|
|
86
|
+
* becomes the delivered chunk when it is the destination).
|
|
87
|
+
*/
|
|
88
|
+
export interface ByteWindow {
|
|
89
|
+
/**
|
|
90
|
+
* A view over the next `n` bytes. May be called several times within one
|
|
91
|
+
* direct invocation (`remaining()` re-derives on every call); an
|
|
92
|
+
* implementation that *synthesizes* the window must return the same
|
|
93
|
+
* storage for the whole invocation and release it in `endWindow`.
|
|
94
|
+
*/
|
|
95
|
+
byteView(n: number): Uint8Array;
|
|
96
|
+
/** Record `k` bytes as moved. Called only after a clean callback return. */
|
|
97
|
+
advanceBytes(k: number): void;
|
|
98
|
+
/** End of one direct invocation; drop any synthesized window. */
|
|
99
|
+
endWindow?(): void;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A21: the parked direct session, as the rendezvous sees it. It presents the
|
|
103
|
+
* ordinary buffer surface (so `remain()`/`isZeroLength()` keep the reference
|
|
104
|
+
* control flow working) but its `read`/`write` are never called — the seam
|
|
105
|
+
* routes it through `runDirect` instead.
|
|
106
|
+
*/
|
|
107
|
+
export interface DirectBuffer extends RendezvousBuffer {
|
|
108
|
+
readonly direct: true;
|
|
109
|
+
/**
|
|
110
|
+
* Run this session's callback exactly once against the peer's window,
|
|
111
|
+
* with `n` bytes of capacity. Applies the acknowledged marks to `peer`
|
|
112
|
+
* itself, and settles the session on failure — the seam only routes the
|
|
113
|
+
* rendezvous state that follows.
|
|
114
|
+
*/
|
|
115
|
+
runDirect(peer: ByteWindow, n: number): DirectOutcome;
|
|
116
|
+
/**
|
|
117
|
+
* Reject this session out-of-band (the two-direct-sessions rendezvous,
|
|
118
|
+
* where neither side owns memory).
|
|
119
|
+
*/
|
|
120
|
+
failDirect(error: Error): void;
|
|
54
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* What the seam did, and hence how the rendezvous must continue.
|
|
124
|
+
*
|
|
125
|
+
* * `"chunk"` — no direct session was involved: the reference copy ran.
|
|
126
|
+
* * `"copied"` — the callback acknowledged ≥ 1 byte; continue exactly as
|
|
127
|
+
* after a reference copy (fire the pending side's `on_copy`).
|
|
128
|
+
* * `"retracted"` — `"done"` with zero marked. Continue as if the direct
|
|
129
|
+
* side's buffer had had `remain() == 0` all along, which is a state
|
|
130
|
+
* definitions.py already routes.
|
|
131
|
+
* * `"failed"` — misuse or a throwing callback; the session has already
|
|
132
|
+
* rejected. No copy, no event, the peer's parked operation survives.
|
|
133
|
+
* * `"both-direct"` — neither side owns memory; the ARRIVING side is
|
|
134
|
+
* rejected by the caller and the parked side is left undisturbed.
|
|
135
|
+
*/
|
|
136
|
+
export type DirectOutcome = "copied" | "retracted" | "failed";
|
|
137
|
+
export type RendezvousOutcome = DirectOutcome | "chunk" | "both-direct";
|
|
55
138
|
/** Common shape of the object a `stream`/`future` *value* refers to. */
|
|
56
139
|
export interface SharedBase {
|
|
57
140
|
readonly t: ValType | null;
|