@polyengine/runtime 0.4.0 → 0.5.1
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/instantiate.js +43 -9
- package/esm/embedder/mod.js +21 -10
- package/esm/embedder/streams.js +96 -5
- package/esm/exec/boundary.js +210 -19
- package/esm/exec/executor.js +16 -1
- package/esm/exec/host_streams.js +376 -0
- package/esm/intrinsics/async_builtins.js +5 -2
- package/esm/jspi/suspending.js +7 -2
- 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/boundary.d.ts +13 -0
- package/types/exec/host_streams.d.ts +70 -0
- package/types/jspi/suspending.d.ts +1 -1
- package/types/task/streams.d.ts +83 -0
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();
|
|
@@ -414,8 +414,11 @@ export function createSubtaskCancel(decl, inst, mode = "plain") {
|
|
|
414
414
|
// rule (fact_calls.ts). A callee with a pending (undeliverable)
|
|
415
415
|
// cancel sits parked non-cancellably, which is determinate, so the
|
|
416
416
|
// genuine BLOCKED answer is still immediate. Host-import subtasks
|
|
417
|
-
// carry no callee task
|
|
418
|
-
//
|
|
417
|
+
// carry no callee task, and their state cannot be mid-hop: the
|
|
418
|
+
// default (A23) onCancel resolves them before this branch is ever
|
|
419
|
+
// reached, and a `deferCancel` import's no-op onCancel leaves them
|
|
420
|
+
// simply unresolved — either way the pre-jspi immediate answer
|
|
421
|
+
// stands.
|
|
419
422
|
//
|
|
420
423
|
// NAMED DIVERGENCE (docs/architecture.md §6, #92): this park makes
|
|
421
424
|
// the async built-in non-atomic — other ready threads may run while
|
package/esm/jspi/suspending.js
CHANGED
|
@@ -11,5 +11,10 @@
|
|
|
11
11
|
// Layering: this module was import-free on purpose (jspi/ stays standalone);
|
|
12
12
|
// A9 relaxes that to "imports `@polyengine/protocol` only" — the protocol package
|
|
13
13
|
// is itself dependency-free, so jspi/ still pulls in no runtime machinery.
|
|
14
|
-
//
|
|
15
|
-
export
|
|
14
|
+
// A23 (`deferCancel`/`isDeferCancel`) and A24 (`abortable`/`isAbortable`)
|
|
15
|
+
// ride the same re-export: they are the other per-declaration host-import
|
|
16
|
+
// marks, they live in the same dependency-free package, and
|
|
17
|
+
// `exec/executor.ts` reads all three through `jspi/mod.ts`.
|
|
18
|
+
// (Host modules import the marks from `@polyengine/protocol` directly —
|
|
19
|
+
// the embedder surface stopped re-exporting the vocabulary at A22.)
|
|
20
|
+
export { abortable, anySuspendingImport, deferCancel, isAbortable, isDeferCancel, isSuspending, suspending, } from "@polyengine/protocol";
|
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.1",
|
|
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.3"
|
|
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.1";
|
|
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";
|