@polyengine/runtime 0.1.0-pre.g633468a

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.
Files changed (126) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +23 -0
  3. package/esm/cabi/async_values.js +162 -0
  4. package/esm/cabi/bulk_lists.js +198 -0
  5. package/esm/cabi/context.js +42 -0
  6. package/esm/cabi/flatten.js +145 -0
  7. package/esm/cabi/float.js +67 -0
  8. package/esm/cabi/handles.js +253 -0
  9. package/esm/cabi/layout.js +149 -0
  10. package/esm/cabi/lift.js +196 -0
  11. package/esm/cabi/load.js +146 -0
  12. package/esm/cabi/lower.js +141 -0
  13. package/esm/cabi/memory.js +182 -0
  14. package/esm/cabi/mod.js +22 -0
  15. package/esm/cabi/store.js +186 -0
  16. package/esm/cabi/strings.js +336 -0
  17. package/esm/cabi/trap.js +38 -0
  18. package/esm/cabi/types.js +264 -0
  19. package/esm/cabi/values.js +64 -0
  20. package/esm/cache/core.js +156 -0
  21. package/esm/cache/dir.js +170 -0
  22. package/esm/cache/mod.js +4 -0
  23. package/esm/cache/web.js +136 -0
  24. package/esm/digest/digest.js +332 -0
  25. package/esm/digest/mod.js +3 -0
  26. package/esm/digest/verify.js +129 -0
  27. package/esm/embedder/casing.js +56 -0
  28. package/esm/embedder/copy.js +42 -0
  29. package/esm/embedder/errors.js +26 -0
  30. package/esm/embedder/imports.js +63 -0
  31. package/esm/embedder/instantiate.js +978 -0
  32. package/esm/embedder/mod.js +40 -0
  33. package/esm/embedder/resources.js +406 -0
  34. package/esm/embedder/streams.js +770 -0
  35. package/esm/embedder/values.js +450 -0
  36. package/esm/embedder/version.js +273 -0
  37. package/esm/exec/boundary.js +1902 -0
  38. package/esm/exec/executor.js +1004 -0
  39. package/esm/exec/host_streams.js +818 -0
  40. package/esm/exec/mod.js +4 -0
  41. package/esm/intrinsics/async_builtins.js +510 -0
  42. package/esm/intrinsics/context.js +90 -0
  43. package/esm/intrinsics/errors.js +13 -0
  44. package/esm/intrinsics/fact_calls.js +865 -0
  45. package/esm/intrinsics/mod.js +564 -0
  46. package/esm/intrinsics/stream_builtins.js +578 -0
  47. package/esm/intrinsics/transcode.js +443 -0
  48. package/esm/jspi/bridge.js +579 -0
  49. package/esm/jspi/mechanics.js +89 -0
  50. package/esm/jspi/mod.js +5 -0
  51. package/esm/jspi/suspending.js +15 -0
  52. package/esm/jspi/types.js +29 -0
  53. package/esm/package.json +3 -0
  54. package/esm/plan/format.js +5 -0
  55. package/esm/plan/loader.js +657 -0
  56. package/esm/plan/mod.js +3 -0
  57. package/esm/shim/mod.js +2 -0
  58. package/esm/shim/translator.js +94 -0
  59. package/esm/task/mod.js +483 -0
  60. package/esm/task/scheduler.js +1028 -0
  61. package/esm/task/streams.js +786 -0
  62. package/esm/task/subtask.js +135 -0
  63. package/esm/task/thread.js +255 -0
  64. package/esm/task/waitable.js +144 -0
  65. package/package.json +91 -0
  66. package/types/cabi/async_values.d.ts +35 -0
  67. package/types/cabi/bulk_lists.d.ts +18 -0
  68. package/types/cabi/context.d.ts +59 -0
  69. package/types/cabi/flatten.d.ts +14 -0
  70. package/types/cabi/float.d.ts +14 -0
  71. package/types/cabi/handles.d.ts +70 -0
  72. package/types/cabi/layout.d.ts +13 -0
  73. package/types/cabi/lift.d.ts +25 -0
  74. package/types/cabi/load.d.ts +14 -0
  75. package/types/cabi/lower.d.ts +10 -0
  76. package/types/cabi/memory.d.ts +58 -0
  77. package/types/cabi/mod.d.ts +15 -0
  78. package/types/cabi/store.d.ts +12 -0
  79. package/types/cabi/strings.d.ts +23 -0
  80. package/types/cabi/trap.d.ts +11 -0
  81. package/types/cabi/types.d.ts +206 -0
  82. package/types/cabi/values.d.ts +5 -0
  83. package/types/cache/core.d.ts +97 -0
  84. package/types/cache/dir.d.ts +6 -0
  85. package/types/cache/mod.d.ts +3 -0
  86. package/types/cache/web.d.ts +10 -0
  87. package/types/digest/digest.d.ts +17 -0
  88. package/types/digest/mod.d.ts +2 -0
  89. package/types/digest/verify.d.ts +48 -0
  90. package/types/embedder/casing.d.ts +40 -0
  91. package/types/embedder/copy.d.ts +24 -0
  92. package/types/embedder/errors.d.ts +11 -0
  93. package/types/embedder/imports.d.ts +47 -0
  94. package/types/embedder/instantiate.d.ts +88 -0
  95. package/types/embedder/mod.d.ts +11 -0
  96. package/types/embedder/resources.d.ts +158 -0
  97. package/types/embedder/streams.d.ts +202 -0
  98. package/types/embedder/values.d.ts +70 -0
  99. package/types/embedder/version.d.ts +85 -0
  100. package/types/exec/boundary.d.ts +360 -0
  101. package/types/exec/executor.d.ts +125 -0
  102. package/types/exec/host_streams.d.ts +165 -0
  103. package/types/exec/mod.d.ts +3 -0
  104. package/types/intrinsics/async_builtins.d.ts +69 -0
  105. package/types/intrinsics/context.d.ts +28 -0
  106. package/types/intrinsics/errors.d.ts +5 -0
  107. package/types/intrinsics/fact_calls.d.ts +120 -0
  108. package/types/intrinsics/mod.d.ts +187 -0
  109. package/types/intrinsics/stream_builtins.d.ts +113 -0
  110. package/types/intrinsics/transcode.d.ts +21 -0
  111. package/types/jspi/bridge.d.ts +227 -0
  112. package/types/jspi/mechanics.d.ts +50 -0
  113. package/types/jspi/mod.d.ts +3 -0
  114. package/types/jspi/suspending.d.ts +1 -0
  115. package/types/jspi/types.d.ts +26 -0
  116. package/types/plan/format.d.ts +369 -0
  117. package/types/plan/loader.d.ts +113 -0
  118. package/types/plan/mod.d.ts +2 -0
  119. package/types/shim/mod.d.ts +1 -0
  120. package/types/shim/translator.d.ts +55 -0
  121. package/types/task/mod.d.ts +257 -0
  122. package/types/task/scheduler.d.ts +421 -0
  123. package/types/task/streams.d.ts +370 -0
  124. package/types/task/subtask.d.ts +96 -0
  125. package/types/task/thread.d.ts +73 -0
  126. package/types/task/waitable.d.ts +67 -0
@@ -0,0 +1,818 @@
1
+ // Host-side stream and future ends: the minimal embedder surface for the
2
+ // async value types.
3
+ //
4
+ // ===========================================================================
5
+ // WHY THIS IS SMALL
6
+ // ===========================================================================
7
+ //
8
+ // The rendezvous in `task/streams.ts` never touches linear memory. It only
9
+ // ever calls four methods on whatever buffer it is handed — `read`, `write`,
10
+ // `remain`, `isZeroLength` — and it passes the *shared* stream object around
11
+ // by identity. So a host end needs exactly two new things:
12
+ //
13
+ // * `HostBuffer`, a sibling of `GuestBuffer` implementing that same
14
+ // four-method surface over a plain JS array instead of guest memory; and
15
+ // * a way to park a host read/write until the guest shows up.
16
+ //
17
+ // Everything else is existing machinery. In particular the *value* that
18
+ // crosses the component boundary is the `SharedStreamImpl` itself, so passing
19
+ // a host stream to a guest goes through the ordinary `lowerStream` path
20
+ // (definitions.py `lower_stream`, line 1828 — wrap the shared object in a
21
+ // fresh `ReadableStreamEnd` in the callee's table) and a guest-returned stream
22
+ // arrives as the same kind of object from `liftStream`. No lift/lower code was
23
+ // added for this file.
24
+ //
25
+ // ===========================================================================
26
+ // SCHEDULING
27
+ // ===========================================================================
28
+ //
29
+ // A host read/write that cannot rendezvous immediately parks, exactly as a
30
+ // guest one does, and hands back a Promise. Two cases:
31
+ //
32
+ // * The guest is still running (it is what will complete the rendezvous).
33
+ // The host's `onCopyDone` fires synchronously inside the guest's
34
+ // `stream.read`/`stream.write` trampoline and the Promise resolves.
35
+ // * The *guest* is the parked side and only the embedder can make progress.
36
+ // Then `drive()` would otherwise see no ready thread and no outstanding
37
+ // host call and declare deadlock — correctly, for a component that really
38
+ // is stuck, but wrongly here. `HostActivity` below registers a
39
+ // re-arming promise in `store.pendingHostCalls` for as long as the host
40
+ // RETAINS a way to act, which is precisely the signal `driveAsync`
41
+ // already understands: "progress is possible, but only after a turn of
42
+ // the event loop".
43
+ //
44
+ // Retention, stated as the rule the arm implements (#162, embedder-api
45
+ // amendment A15): the arm is live iff the host holds a retained end, a parked
46
+ // host operation, or an unfinished producer pump. Which ends the host holds
47
+ // follows from where the wrapper came from — a host-CREATED stream keeps its
48
+ // writable end across every lower (only readable ends transfer,
49
+ // definitions.py `lower_stream` line 1828), while a LIFTED one holds just the
50
+ // readable end the guest passed out, so lowering that same object back into a
51
+ // guest (the `identity: async func(s: stream<u8>) -> stream<u8>` round trip)
52
+ // hands the host's last end away and the arm disarms. A later re-lift
53
+ // re-arms. See `bindOnLower` and `HostActivity` for the mechanism.
54
+ //
55
+ // The consequence, stated plainly: an embedder that lowers a host stream into
56
+ // a guest and then never writes to it or drops it will *hang* rather than
57
+ // trap. That is the honest outcome — the component is not deadlocked, the
58
+ // embedder simply has not done its half — and it matches how any other
59
+ // unresolved Promise behaves in JS. That policy is unchanged by A15; what
60
+ // changed is that the claim now EXPIRES with retention, so a store that once
61
+ // round-tripped a stream through the host no longer misreports every later
62
+ // genuine deadlock as this hang.
63
+ //
64
+ // The inverse case is NOT a hang (#66, embedder-api amendment A7): when the
65
+ // GUEST side dies — a trap poisons the instance holding the peer end — the
66
+ // poisoned table's ends are retired (task/streams.ts
67
+ // `retireInstanceAsyncEnds`), so a parked host operation settles DROPPED-
68
+ // shaped here and the conventions layer rejects it with `PeerTrappedError`.
69
+ // Only embedder negligence hangs; a component fault is always loud.
70
+ import { assert_ } from "../cabi/trap.js";
71
+ import { despecialize } from "../cabi/types.js";
72
+ import { driveStoreAsync, storeDriverDepth, whenStoreDriverIdle, } from "./boundary.js";
73
+ import { abandonSharedFuture, BUFFER_MAX_LENGTH, CopyResult, markHostActivityArm, sameElemType, SharedFutureImpl, SharedStreamImpl, storeQuiescent as quiescent, } from "../task/mod.js";
74
+ /**
75
+ * The `inst` a host end presents to the rendezvous. definitions.py compares it
76
+ * against `pending_inst` for the "same instance" restriction — a guard against
77
+ * interleaving two *lifts in one component instance's linear memory*, which is
78
+ * why it exempts number types (definitions.py `none_or_number_type`). A host
79
+ * end has no linear memory, so that restriction can never apply to it: each
80
+ * end gets its OWN sentinel (never equal to a real `ComponentInstanceState`,
81
+ * and never equal to the peer end's), so a host writer and a host reader may
82
+ * rendezvous directly for every element type. One shared sentinel used to
83
+ * stand for "the host" here, which made a post-pass-through host↔host copy of
84
+ * a non-number element type trap as "intra-component" (found by the #54
85
+ * pass-through investigation).
86
+ */
87
+ function hostEndInstance(role) {
88
+ return Object.freeze({ hostEnd: role });
89
+ }
90
+ /**
91
+ * A buffer over JS values. Sibling of `GuestBuffer`, same four methods, no
92
+ * memory access. Used in one of two directions:
93
+ *
94
+ * * as a *readable* buffer (host supplies `values`, the guest reads them);
95
+ * * as a *writable* buffer (host supplies capacity, the guest fills it and
96
+ * `taken()` is what arrived).
97
+ *
98
+ * u8 payloads stay `Uint8Array` through both directions (issue #54): `read`
99
+ * slices the typed array (bulk, and the ONE semantically required copy — the
100
+ * chunk is only borrowed by the stream until the write settles, so the reader
101
+ * must receive owned bytes), and `write` keeps arriving chunks whole instead
102
+ * of exploding them element-by-element into a plain array.
103
+ */
104
+ export class HostBuffer {
105
+ t;
106
+ values;
107
+ length;
108
+ progress = 0;
109
+ #chunks = [];
110
+ constructor(t, values, length) {
111
+ this.t = t;
112
+ this.values = values;
113
+ this.length = length;
114
+ // definitions.py `Buffer.MAX_LENGTH` (:919) is asserted on every buffer
115
+ // the spec builds (`BufferGuestImpl.__init__`, :938); `GuestBuffer` traps
116
+ // on it. A host buffer is not guest-visible, so a violation is embedder
117
+ // misuse rather than a component fault — hence a loud typed JS error and
118
+ // not a `Trap`. Caught at construction: an over-long host offer would
119
+ // otherwise silently exceed the spec bound (#97).
120
+ if (!Number.isInteger(length) || length < 0) {
121
+ throw new RangeError(`host buffer length must be a non-negative integer, got ${length}`);
122
+ }
123
+ if (length > BUFFER_MAX_LENGTH) {
124
+ throw new RangeError(`host buffer length ${length} exceeds the Component Model's ` +
125
+ `Buffer.MAX_LENGTH (${BUFFER_MAX_LENGTH})`);
126
+ }
127
+ }
128
+ remain() {
129
+ return this.length - this.progress;
130
+ }
131
+ isZeroLength() {
132
+ return this.length === 0;
133
+ }
134
+ /** Guest side is reading from us. */
135
+ read(n) {
136
+ assert_(n <= this.remain(), "host buffer read beyond remaining");
137
+ const out = this.values === null
138
+ ? new Array(n).fill(null)
139
+ : this.values.slice(this.progress, this.progress + n);
140
+ this.progress += n;
141
+ return out;
142
+ }
143
+ /** Guest side is writing into us. */
144
+ write(vs) {
145
+ assert_(vs.length <= this.remain(), "host buffer write beyond remaining");
146
+ this.#chunks.push(vs);
147
+ this.progress += vs.length;
148
+ }
149
+ /**
150
+ * Everything written into this buffer, in arrival order.
151
+ *
152
+ * For a u8 element type the result is a `Uint8Array`; in the common case —
153
+ * one rendezvous before the read resolves — the writer's chunk is returned
154
+ * as-is, so the whole host-side read costs exactly the one rendezvous copy.
155
+ * Every other element type yields a plain array regardless of the shape the
156
+ * writer used.
157
+ */
158
+ taken() {
159
+ const u8 = this.t !== null && despecialize(this.t).kind === "u8";
160
+ if (u8) {
161
+ if (this.#chunks.length === 1 && this.#chunks[0] instanceof Uint8Array) {
162
+ return this.#chunks[0];
163
+ }
164
+ // Multiple chunks, or a raw-layer plain-array writer: pack. Element
165
+ // coercion matches Uint8Array.from, which is what the conventions
166
+ // layer applied to these values before chunks stayed whole.
167
+ const out = new Uint8Array(this.progress);
168
+ let o = 0;
169
+ for (const c of this.#chunks) {
170
+ if (c instanceof Uint8Array)
171
+ out.set(c, o);
172
+ else
173
+ for (let i = 0; i < c.length; i++)
174
+ out[o + i] = c[i];
175
+ o += c.length;
176
+ }
177
+ return out;
178
+ }
179
+ if (this.#chunks.length === 1 && Array.isArray(this.#chunks[0])) {
180
+ return this.#chunks[0];
181
+ }
182
+ const out = [];
183
+ for (const c of this.#chunks) {
184
+ for (let i = 0; i < c.length; i++)
185
+ out.push(c[i]);
186
+ }
187
+ return out;
188
+ }
189
+ }
190
+ /**
191
+ * Every live `HostActivity` arm, by identity. These are the promises this
192
+ * module parks in `store.pendingHostCalls` purely to say "the embedder may
193
+ * still act"; they are NOT outstanding work, so the host pump must not treat
194
+ * their presence as a reason to keep looping (that is the "activity keeps
195
+ * pendingHostCalls non-empty forever" hazard: a pump whose exit condition is
196
+ * `pendingHostCalls.size === 0` would never exit).
197
+ *
198
+ * The registry and the two predicates over it (`hasRealHostCall`,
199
+ * `storeQuiescent`, imported above as `quiescent`) moved to
200
+ * task/scheduler.ts so that boundary.ts's settlement pump — the OTHER
201
+ * between-calls driver — shares the same classification without an import
202
+ * cycle. Arms are minted here and marked via `markHostActivityArm`.
203
+ */
204
+ /**
205
+ * Keeps `store.pendingHostCalls` non-empty while a host end is live, so the
206
+ * driving loop treats "waiting for the embedder" as progress-is-possible
207
+ * rather than deadlock. Re-arms after every notification.
208
+ *
209
+ * RETENTION IS THE LIVENESS RULE (#162, embedder-api amendment A15). The arm
210
+ * is live iff the host retains a way to act on this shared object: a retained
211
+ * end, a parked host operation, or an unfinished producer pump. The claim it
212
+ * makes to the deadlock verdicts — "the embedder may still act" — therefore
213
+ * *expires*. Three state transitions implement it:
214
+ *
215
+ * * `close()` — terminal: DROPPED, an explicit drop, or the shared object's
216
+ * drop observers (either end, the A7 teardown walk). Nothing can revive
217
+ * the wrapper.
218
+ * * `disarm()` — NON-terminal: the host handed its last end back to a guest
219
+ * (a lifted stream/future lowered back in — the identity round trip). The
220
+ * object is still alive; the host merely holds nothing.
221
+ * * `rearm()` — the inverse: a re-lift handed the readable end back.
222
+ *
223
+ * The embedder-negligence policy of the module header is unchanged — an
224
+ * embedder that lowers a host-CREATED stream and never writes still hangs
225
+ * rather than traps, because it genuinely retains the writable end.
226
+ */
227
+ class HostActivity {
228
+ #store = null;
229
+ #promise = null;
230
+ #resolve = null;
231
+ #closed = false;
232
+ /** Retention is momentarily zero; revivable via `rearm()` (#162). */
233
+ #disarmed = false;
234
+ #pumping = false;
235
+ bind(store) {
236
+ if (this.#store !== null || this.#closed)
237
+ return;
238
+ this.#store = store;
239
+ this.#arm();
240
+ }
241
+ #arm() {
242
+ if (this.#store === null || this.#promise !== null)
243
+ return;
244
+ if (this.#closed || this.#disarmed)
245
+ return;
246
+ this.#promise = new Promise((r) => (this.#resolve = r));
247
+ markHostActivityArm(this.#promise);
248
+ this.#store.pendingHostCalls.add(this.#promise);
249
+ }
250
+ /** The embedder did something; let the driving loop re-pump. */
251
+ notify() {
252
+ const p = this.#promise, r = this.#resolve;
253
+ this.#promise = null;
254
+ this.#resolve = null;
255
+ if (p !== null && this.#store !== null) {
256
+ this.#store.pendingHostCalls.delete(p);
257
+ }
258
+ r?.();
259
+ this.#arm();
260
+ }
261
+ /**
262
+ * Drive the guest until it can make no more progress.
263
+ *
264
+ * A host operation that lands *between* export calls has no driving loop
265
+ * running — `drive()` returned when the last export call resolved. So after
266
+ * initiating a host read/write (or a drop) we pump the store ourselves.
267
+ * Synchronously first (the common case: the guest is merely waiting on a
268
+ * scheduler condition our rendezvous just satisfied, and the host op's
269
+ * promise resolves before we return), then — if anything is still
270
+ * outstanding — by handing the store to the *same* loop an export call
271
+ * would have used, `driveStoreAsync`. Without the asynchronous half a guest
272
+ * parked in a background forwarding task would never be resumed to consume
273
+ * what we just offered, and the host read would await forever (C0 finding
274
+ * R-1: the previous local drain only serviced `store.awaiting` and never
275
+ * awaited `store.pendingHostCalls`, so a writer parked on a
276
+ * Promise-returning host import stalled the reader).
277
+ *
278
+ * Traps from the synchronous half propagate to the caller of the host
279
+ * operation, which is the only place that can report them.
280
+ */
281
+ pump() {
282
+ const store = this.#store;
283
+ if (store === null)
284
+ return;
285
+ // Settled activation tails gate `tick` (Store.settled); a driver that
286
+ // never services them wedges the store — this loop runs BETWEEN export
287
+ // calls, when no driveAsync exists to do it.
288
+ for (;;) {
289
+ const serviced = store.serviceSettled();
290
+ const ticked = store.tick();
291
+ if (!serviced && !ticked)
292
+ break;
293
+ }
294
+ if (this.#pumping)
295
+ return;
296
+ // Nothing is outstanding that only an event-loop turn could advance ⇒ no
297
+ // asynchronous pump needed. In particular an embedder that lowered a host
298
+ // end into a guest and then never did its half lands here: we return, no
299
+ // spin and no deadlock trap, and the operation's promise simply stays
300
+ // pending — the documented "hangs rather than traps" behaviour (see the
301
+ // module header).
302
+ if (quiescent(store))
303
+ return;
304
+ this.#pumping = true;
305
+ void this.#pumpAsync(store);
306
+ }
307
+ async #pumpAsync(store) {
308
+ try {
309
+ // This pump is the FALLBACK driver — the one for host operations that
310
+ // land BETWEEN export calls — so it stands down whenever an export
311
+ // call's loop is live: that loop already races `pendingHostCalls` and
312
+ // `store.awaiting` and so pumps host activity on our behalf. When it
313
+ // exits, we take over. `whenStoreDriverIdle` is edge-triggered, not
314
+ // polled, so waiting costs no turns.
315
+ //
316
+ // The stand-down is COOPERATIVE, not exclusion: an export call can
317
+ // start while we are parked mid-`await`, and we only notice at the next
318
+ // `done()` evaluation, so a bounded overlap window remains by
319
+ // construction (concurrent export calls have always overlapped too).
320
+ // That is safe for the resume-once invariant — `resumeWith` deletes
321
+ // from `store.awaiting` synchronously and every resumption site
322
+ // re-checks membership *and* promise identity first; see the invariant
323
+ // write-up on `storeDriverDepth` in boundary.ts. Standing down is about
324
+ // not interleaving two loops' `serviceSettled`/`tick` phases, which is
325
+ // what tripped `Trap: table entry empty` out of `runCallbackLoop` when
326
+ // this pump first drove unconditionally alongside an export call.
327
+ while (!quiescent(store)) {
328
+ if (storeDriverDepth(store) > 0) {
329
+ await whenStoreDriverIdle(store);
330
+ continue;
331
+ }
332
+ await driveStoreAsync(store,
333
+ // Quiescence, not completion: this pump exists to keep the guest
334
+ // moving; the host operation's own promise is what the caller
335
+ // awaits. Three exit clauses:
336
+ //
337
+ // * nothing left that a turn of the event loop could advance
338
+ // (`quiescent`);
339
+ // * `pendingHostCalls` empty, which is the precondition of BOTH
340
+ // of `driveAsync`'s deadlock traps. Returning true there keeps
341
+ // this between-calls pump from converting the documented
342
+ // embedder-never-acts hang (module header) into a trap that
343
+ // would surface, misattributed, on some later export call.
344
+ // Deadlock detection for genuine component deadlock stays where
345
+ // it belongs: in the driving loop of the export call the guest
346
+ // is blocked in;
347
+ // * another driver appeared (an export call started while we were
348
+ // parked) — hand the store back to it, per the single-driver
349
+ // rule. Our depth is 1 while we are inside, hence `> 1`.
350
+ () => store.pendingHostCalls.size === 0 ||
351
+ quiescent(store) ||
352
+ storeDriverDepth(store) > 1, "host stream/future activity");
353
+ }
354
+ }
355
+ catch (e) {
356
+ // Nothing is awaiting this pump, so park the failure where the next
357
+ // driving loop will surface it (same channel as a host-import
358
+ // rejection).
359
+ store.hostFailure ??= e;
360
+ }
361
+ finally {
362
+ this.#pumping = false;
363
+ }
364
+ // The pump advanced the guest OUTSIDE any export call's driving loop. A
365
+ // `driveAsync` parked on `Promise.race([...pendingHostCalls])` re-evaluates
366
+ // its `done` predicate only when something it raced settles — and
367
+ // everything the pump just did (resume the callback task, deliver the
368
+ // event, watch the guest `task.return`) may have settled nothing that race
369
+ // can see. Re-arm through `notify()` so a parked driver wakes and
370
+ // re-checks; without this the lifted call's Promise never resolves even
371
+ // though the task resolved (observed: future-user's `double-future` under
372
+ // jspi auto-detection — the guest finished, the embedder's await hung
373
+ // forever).
374
+ this.notify();
375
+ }
376
+ /** No further host activity is possible on this stream. */
377
+ close() {
378
+ const p = this.#promise, r = this.#resolve;
379
+ this.#closed = true;
380
+ this.#promise = null;
381
+ this.#resolve = null;
382
+ if (p !== null && this.#store !== null) {
383
+ this.#store.pendingHostCalls.delete(p);
384
+ }
385
+ r?.();
386
+ }
387
+ /**
388
+ * The host retains no way to act: its lifted end was lowered back into a
389
+ * guest, which now owns it (#162, amendment A15). NON-terminal — a re-lift
390
+ * of the same shared object restores retention via `rearm()`.
391
+ *
392
+ * Resolving the stale arm is required, not tidiness: a `driveAsync` parked
393
+ * on `Promise.race([...pendingHostCalls])` re-evaluates its `done` predicate
394
+ * and its deadlock preconditions only when something it raced settles. An
395
+ * arm merely deleted from the set would leave that driver asleep on a
396
+ * promise nobody will ever settle.
397
+ */
398
+ disarm() {
399
+ const p = this.#promise, r = this.#resolve;
400
+ this.#disarmed = true;
401
+ this.#promise = null;
402
+ this.#resolve = null;
403
+ if (p !== null && this.#store !== null) {
404
+ this.#store.pendingHostCalls.delete(p);
405
+ }
406
+ r?.();
407
+ }
408
+ /**
409
+ * A lift handed the host the readable end again — the A5 cache-hit wrapper
410
+ * for a shared object that round-tripped back out of the guest (#162).
411
+ * A no-op for a closed activity (the object is gone for good) and for one
412
+ * that was never disarmed.
413
+ */
414
+ rearm() {
415
+ if (this.#closed)
416
+ return;
417
+ this.#disarmed = false;
418
+ this.#arm();
419
+ }
420
+ }
421
+ /**
422
+ * Attach host-activity bookkeeping to a shared object at the CABI seam.
423
+ *
424
+ * `kind` is the retention model (#162, amendment A15) — WHICH ends the host
425
+ * holds, which is decided entirely by where the wrapper came from:
426
+ *
427
+ * * `"created"` — `hostStream()`/`hostFuture()`. Only READABLE ends
428
+ * transfer across the boundary (definitions.py `lower_stream`, line 1828,
429
+ * wraps the shared object in a fresh `ReadableStreamEnd` in the callee's
430
+ * table), so lowering hands the guest the readable end and the host keeps
431
+ * the WRITABLE one. Retention survives every lower; the arm ends only at
432
+ * drop/end-of-pump.
433
+ * * `"lifted"` — `hostStreamFor()`/`hostFutureFor()`. The host holds exactly
434
+ * the readable end the guest passed out (`lift_async_value`, line 1530).
435
+ * Lowering that same object back into a guest transfers it away, so
436
+ * retention hits zero and the activity disarms; a later re-lift restores
437
+ * it through the `onLifted` hook.
438
+ *
439
+ * The hooks live here rather than in the conventions layer's `takeValue` so
440
+ * that BOTH the conventions layer and the raw boundary are covered, with no
441
+ * window between "the embedder said transfer" and "the transfer happened".
442
+ */
443
+ function bindOnLower(shared, activity, kind, alsoOnLowered) {
444
+ const holder = shared;
445
+ // INTERNAL INVARIANT (not the embedder-facing policy): two live wrappers
446
+ // on one shared object would mean two HostActivities pumping it, and the
447
+ // second `onLowered` hook would silently orphan the first wrapper's
448
+ // activity binding for future lowers (review advisory, host-streams
449
+ // round). The public entry points cannot get here with a wrapped object —
450
+ // `hostStreamFor`/`hostFutureFor` return the cached wrapper instead
451
+ // (amendment A5) — so a trip here is a bug in this module. The class field
452
+ // initializes to null; == null covers both sentinels.
453
+ assert_(holder.onLowered == null, "internal: a second host wrapper was built for an already-wrapped " +
454
+ "stream/future (the wrapper cache should have returned the first)");
455
+ assert_(holder.onLifted == null, "internal: a second host wrapper installed a lift hook on an " +
456
+ "already-wrapped stream/future (the wrapper cache should have " +
457
+ "returned the first)");
458
+ // `lowerStream`/`lowerFuture` (cabi/async_values.ts :177/:204) fire this on
459
+ // EVERY lower, not just the first — the hook persists, and the asserts
460
+ // above only forbid installing a SECOND one.
461
+ holder.onLowered = (inst) => {
462
+ alsoOnLowered?.();
463
+ if (kind === "lifted") {
464
+ // The wrapper was bound at construction off `boundStore` (the branch
465
+ // below); lowering this object back into a guest hands away the only
466
+ // end the host held.
467
+ activity.disarm();
468
+ }
469
+ else {
470
+ activity.bind(inst.store);
471
+ }
472
+ };
473
+ // Fired by `liftAsyncValue` (cabi/async_values.ts :126) whenever this
474
+ // object is lifted out of a guest table. For a "created"-kind wrapper
475
+ // `rearm()` is a harmless no-op (it is never disarmed), so the hook is
476
+ // installed uniformly.
477
+ holder.onLifted = () => activity.rearm();
478
+ // Release the arm when the shared object dies, whatever kills it. This is
479
+ // the single point that covers three otherwise-separate leaks of one class:
480
+ // the `dropForTeardown` asymmetry (embedder/streams.ts — a teardown with
481
+ // nothing parked never reached `close()`), a guest dropping its end with no
482
+ // host operation parked (the `settle(DROPPED)` -> `close()` path only runs
483
+ // for a parked op), and `HostFuture.readResult`'s already-dropped fast path
484
+ // (which answers synchronously without touching the activity).
485
+ //
486
+ // Note on the guest-to-guest composed hop: a value lifted from the caller
487
+ // and immediately lowered into the callee, both synchronously inside one
488
+ // call's lower phase, fires rearm-then-disarm on any host wrapper that
489
+ // happens to exist for it. The pair nets out to the correct final state.
490
+ shared.whenDropped(() => activity.close());
491
+ // A stream that came *out* of a guest was lifted, never lowered, so the
492
+ // `onLowered` hook above will not fire first; `boundStore` was recorded at
493
+ // lift time instead.
494
+ const bound = shared.boundStore;
495
+ if (bound)
496
+ activity.bind(bound);
497
+ }
498
+ function mkStreamEnds(shared, activity) {
499
+ // Distinct rendezvous identities per end — see `hostEndInstance`.
500
+ const writeInst = hostEndInstance("write");
501
+ const readInst = hostEndInstance("read");
502
+ // Which of OUR operations is currently the shared object's pending side.
503
+ // `SharedBase.cancel` retires whatever is parked, so cancelling is only
504
+ // legal (and only meaningful) while the parked side is ours.
505
+ const parked = { read: false, write: false };
506
+ /**
507
+ * Settle bookkeeping for a completed copy. `DROPPED` means the peer end is
508
+ * gone: no further host activity on this end is possible, so the activity
509
+ * arm is *closed* rather than re-armed (R-fix review advisory 2 — a live arm
510
+ * after end-of-stream keeps `pendingHostCalls` non-empty forever and masks
511
+ * a genuine deadlock as "the embedder might still act").
512
+ */
513
+ const settle = (result) => {
514
+ if (result === CopyResult.DROPPED)
515
+ activity.close();
516
+ else
517
+ activity.notify();
518
+ };
519
+ return {
520
+ writable: {
521
+ write(values) {
522
+ // One in-flight operation per end — the host-side spelling of the
523
+ // `CopyEnd` busy trap guests get from the table. Without it a second
524
+ // write would find the FIRST write's buffer in the shared object's
525
+ // pending slot and "rendezvous" write-against-write, silently
526
+ // copying into the parked buffer's accumulation (observed as a
527
+ // write resolving `1` against a peer that no longer exists — the
528
+ // #66 repro). Reading while a write is parked stays legal: that is
529
+ // the pass-through data plane (two different ends).
530
+ if (parked.write) {
531
+ throw new TypeError("a write is already in flight on this stream's writable end; " +
532
+ "await it or cancelWrite() first");
533
+ }
534
+ const buf = new HostBuffer(shared.t, values, values.length);
535
+ return new Promise((resolve) => {
536
+ parked.write = true;
537
+ shared.write(writeInst, buf,
538
+ // `on_copy`: a partial rendezvous happened. A guest end would be
539
+ // handed a COMPLETED event here and decide for itself whether to
540
+ // re-offer; a host end has no event loop, so we make the useful
541
+ // choice and **stay parked** until the offer is exhausted. That is
542
+ // exactly the shape wit-bindgen's `wit_stream::new()` produces —
543
+ // a background write that the reader drains a few elements at a
544
+ // time — and it is why `reclaim` is deliberately not called while
545
+ // values remain: reclaiming retires the pending buffer and the
546
+ // next guest read would find nothing.
547
+ (reclaim) => {
548
+ if (buf.remain() > 0)
549
+ return; // still parked; more to give
550
+ reclaim();
551
+ parked.write = false;
552
+ activity.notify();
553
+ resolve(buf.progress);
554
+ }, (result) => {
555
+ parked.write = false;
556
+ settle(result);
557
+ resolve(buf.progress);
558
+ });
559
+ activity.notify();
560
+ activity.pump();
561
+ });
562
+ },
563
+ async writeAll(values) {
564
+ let sent = 0;
565
+ while (sent < values.length && !shared.dropped) {
566
+ // Re-offers keep `write`'s borrow semantics: the first round is the
567
+ // chunk itself and later rounds a `subarray` VIEW for typed chunks
568
+ // (review F1: a `slice` here cost a second full copy on the very
569
+ // path the one-copy contract names), a `slice` for plain arrays.
570
+ const rest = sent === 0
571
+ ? values
572
+ : values instanceof Uint8Array
573
+ ? values.subarray(sent)
574
+ : values.slice(sent);
575
+ const n = await this.write(rest);
576
+ if (n === 0)
577
+ break; // reader gone; nothing more will be taken
578
+ sent += n;
579
+ }
580
+ return sent;
581
+ },
582
+ cancelWrite() {
583
+ if (!parked.write)
584
+ return;
585
+ parked.write = false;
586
+ shared.cancel();
587
+ activity.notify();
588
+ activity.pump();
589
+ },
590
+ drop() {
591
+ shared.drop();
592
+ activity.close();
593
+ activity.pump();
594
+ },
595
+ onDropped(fn) {
596
+ shared.whenDropped(fn);
597
+ },
598
+ },
599
+ readable: {
600
+ read(max) {
601
+ // One in-flight operation per end — see the write() guard: a second
602
+ // read would rendezvous read-against-read with our own parked
603
+ // buffer.
604
+ if (parked.read) {
605
+ throw new TypeError("a read is already in flight on this stream's readable end; " +
606
+ "await it or cancelRead() first");
607
+ }
608
+ const buf = new HostBuffer(shared.t, null, max);
609
+ return new Promise((resolve) => {
610
+ parked.read = true;
611
+ shared.read(readInst, buf, (reclaim) => {
612
+ reclaim();
613
+ parked.read = false;
614
+ activity.notify();
615
+ resolve(buf.taken());
616
+ }, (result) => {
617
+ parked.read = false;
618
+ settle(result);
619
+ resolve(buf.taken());
620
+ });
621
+ activity.notify();
622
+ activity.pump();
623
+ });
624
+ },
625
+ cancelRead() {
626
+ // #97, DELIBERATE AND PINNED: cancelling resolves the in-flight
627
+ // `read` promise with whatever the buffer took so far — for a read
628
+ // that had not yet rendezvoused, the empty chunk. An empty chunk is
629
+ // also this layer's end-of-stream signal (see `HostReadableEnd.read`
630
+ // and embedder/streams.ts `Stream.read`), so **a host-cancelled read
631
+ // is indistinguishable from EOS at the conventions layer**. That is
632
+ // accepted rather than papered over: the code that calls
633
+ // `cancelRead()` is the same code that observes the result, so it
634
+ // already knows which of the two happened. Nothing else can reach
635
+ // this state — a guest cannot cancel the host's read.
636
+ if (!parked.read)
637
+ return;
638
+ parked.read = false;
639
+ shared.cancel();
640
+ activity.notify();
641
+ activity.pump();
642
+ },
643
+ drop() {
644
+ shared.drop();
645
+ activity.close();
646
+ activity.pump();
647
+ },
648
+ },
649
+ };
650
+ }
651
+ /**
652
+ * One host wrapper per shared object, by identity (embedder-api amendment
653
+ * A5). A stream/future value that round-trips host → guest → host lifts back
654
+ * as the SAME wrapper the host already holds, so wrapping is idempotent —
655
+ * there is never a second `HostActivity` competing to pump one shared object
656
+ * (the hazard the old double-wrap assert guarded against), and the readable
657
+ * end stays transferable across as many boundary hops as the spec allows.
658
+ */
659
+ const streamWrappers = new WeakMap();
660
+ const futureWrappers = new WeakMap();
661
+ /** Create a host-owned stream of `element` (`null` = zero-width payload). */
662
+ export function hostStream(element) {
663
+ const shared = new SharedStreamImpl(element);
664
+ const activity = new HostActivity();
665
+ bindOnLower(shared, activity, "created");
666
+ const ends = mkStreamEnds(shared, activity);
667
+ const wrapper = { ...ends, value: shared };
668
+ streamWrappers.set(shared, wrapper);
669
+ return wrapper;
670
+ }
671
+ /**
672
+ * Wrap a stream that came *out* of a guest (from `liftStream`). Idempotent:
673
+ * a shared object that already has a host wrapper (it was created by
674
+ * `hostStream`, or lifted before) yields that same wrapper.
675
+ */
676
+ export function hostStreamFor(value) {
677
+ const shared = value;
678
+ assert_(shared instanceof SharedStreamImpl, "hostStreamFor expects a lifted stream value");
679
+ const cached = streamWrappers.get(shared);
680
+ if (cached !== undefined)
681
+ return cached;
682
+ const activity = new HostActivity();
683
+ bindOnLower(shared, activity, "lifted");
684
+ const ends = mkStreamEnds(shared, activity);
685
+ const wrapper = { ...ends, value };
686
+ streamWrappers.set(shared, wrapper);
687
+ return wrapper;
688
+ }
689
+ /** Create a host-owned future of `element`. */
690
+ export function hostFuture(element) {
691
+ const shared = new SharedFutureImpl(element);
692
+ const activity = new HostActivity();
693
+ const lowering = { lowered: false };
694
+ bindOnLower(shared, activity, "created", () => lowering.lowered = true);
695
+ const wrapper = mkFuture(shared, activity, shared, lowering);
696
+ futureWrappers.set(shared, wrapper);
697
+ return wrapper;
698
+ }
699
+ /**
700
+ * Wrap a future that came *out* of a guest (from `liftFuture`). Idempotent —
701
+ * see `hostStreamFor`.
702
+ */
703
+ export function hostFutureFor(value) {
704
+ const shared = value;
705
+ assert_(shared instanceof SharedFutureImpl, "hostFutureFor expects a lifted future value");
706
+ const cached = futureWrappers.get(shared);
707
+ if (cached !== undefined)
708
+ return cached;
709
+ const activity = new HostActivity();
710
+ const lowering = { lowered: false };
711
+ bindOnLower(shared, activity, "lifted", () => lowering.lowered = true);
712
+ const wrapper = mkFuture(shared, activity, value, lowering);
713
+ futureWrappers.set(shared, wrapper);
714
+ return wrapper;
715
+ }
716
+ function mkFuture(shared, activity, value,
717
+ /**
718
+ * Flipped by `bindOnLower` the first time this future is lowered into a
719
+ * guest — i.e. the first time a guest receives its READABLE end and this
720
+ * wrapper takes on the spec's writable role. `drop()` needs it (#90).
721
+ */
722
+ lowering) {
723
+ // Distinct rendezvous identities per end — see `hostEndInstance`.
724
+ const writeInst = hostEndInstance("write");
725
+ const readInst = hostEndInstance("read");
726
+ const parked = { any: false };
727
+ /** Set once the future's one value has actually crossed (#90). */
728
+ let delivered = false;
729
+ const settle = (result) => {
730
+ parked.any = false;
731
+ if (result === CopyResult.COMPLETED)
732
+ delivered = true;
733
+ if (result === CopyResult.DROPPED)
734
+ activity.close();
735
+ else
736
+ activity.notify();
737
+ };
738
+ const self = {
739
+ write(v) {
740
+ // One in-flight operation per wrapper — see mkStreamEnds' guards: a
741
+ // second op would rendezvous against our own parked buffer.
742
+ if (parked.any) {
743
+ throw new TypeError("an operation is already in flight on this future; " +
744
+ "await it or cancel() first");
745
+ }
746
+ // definitions.py `SharedFutureImpl.write` asserts `remain() == 1`: a
747
+ // future carries exactly one element.
748
+ const buf = new HostBuffer(shared.t, [v], 1);
749
+ return new Promise((resolve) => {
750
+ parked.any = true;
751
+ shared.write(writeInst, buf, (result) => {
752
+ settle(result);
753
+ resolve();
754
+ });
755
+ activity.notify();
756
+ activity.pump();
757
+ });
758
+ },
759
+ readResult() {
760
+ // One in-flight operation per wrapper — see write().
761
+ if (parked.any) {
762
+ throw new TypeError("an operation is already in flight on this future; " +
763
+ "await it or cancel() first");
764
+ }
765
+ // definitions.py `SharedFutureImpl.read` asserts `not self.dropped`, so
766
+ // a read after the write end went away must be answered here rather
767
+ // than by tripping an internal assertion.
768
+ if (shared.dropped) {
769
+ return Promise.resolve({
770
+ value: undefined,
771
+ result: CopyResult.DROPPED,
772
+ });
773
+ }
774
+ const buf = new HostBuffer(shared.t, null, 1);
775
+ return new Promise((resolve) => {
776
+ parked.any = true;
777
+ shared.read(readInst, buf, (result) => {
778
+ settle(result);
779
+ resolve({
780
+ value: buf.taken()[0],
781
+ result,
782
+ });
783
+ });
784
+ activity.notify();
785
+ activity.pump();
786
+ });
787
+ },
788
+ async read() {
789
+ return (await self.readResult()).value;
790
+ },
791
+ cancel() {
792
+ if (!parked.any)
793
+ return;
794
+ parked.any = false;
795
+ shared.cancel();
796
+ activity.notify();
797
+ activity.pump();
798
+ },
799
+ drop() {
800
+ // #90. Never throws, idempotent: `SharedFutureImpl.drop` and
801
+ // `abandonSharedFuture` both no-op on an already-dropped future, and
802
+ // neither can raise. See the `HostFuture.drop` doc for the three cases.
803
+ if (!delivered && lowering.lowered && !shared.dropped) {
804
+ abandonSharedFuture(shared, new Error("the host dropped the writable end of this future without " +
805
+ "writing a value"));
806
+ }
807
+ else {
808
+ shared.drop();
809
+ }
810
+ activity.close();
811
+ activity.pump();
812
+ },
813
+ value,
814
+ };
815
+ return self;
816
+ }
817
+ /** Re-exported so embedders can build element types without importing cabi. */
818
+ export { sameElemType };