@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.
- package/LICENSE +202 -0
- package/README.md +23 -0
- package/esm/cabi/async_values.js +162 -0
- package/esm/cabi/bulk_lists.js +198 -0
- package/esm/cabi/context.js +42 -0
- package/esm/cabi/flatten.js +145 -0
- package/esm/cabi/float.js +67 -0
- package/esm/cabi/handles.js +253 -0
- package/esm/cabi/layout.js +149 -0
- package/esm/cabi/lift.js +196 -0
- package/esm/cabi/load.js +146 -0
- package/esm/cabi/lower.js +141 -0
- package/esm/cabi/memory.js +182 -0
- package/esm/cabi/mod.js +22 -0
- package/esm/cabi/store.js +186 -0
- package/esm/cabi/strings.js +336 -0
- package/esm/cabi/trap.js +38 -0
- package/esm/cabi/types.js +264 -0
- package/esm/cabi/values.js +64 -0
- package/esm/cache/core.js +156 -0
- package/esm/cache/dir.js +170 -0
- package/esm/cache/mod.js +4 -0
- package/esm/cache/web.js +136 -0
- package/esm/digest/digest.js +332 -0
- package/esm/digest/mod.js +3 -0
- package/esm/digest/verify.js +129 -0
- package/esm/embedder/casing.js +56 -0
- package/esm/embedder/copy.js +42 -0
- package/esm/embedder/errors.js +26 -0
- package/esm/embedder/imports.js +63 -0
- package/esm/embedder/instantiate.js +978 -0
- package/esm/embedder/mod.js +40 -0
- package/esm/embedder/resources.js +406 -0
- package/esm/embedder/streams.js +770 -0
- package/esm/embedder/values.js +450 -0
- package/esm/embedder/version.js +273 -0
- package/esm/exec/boundary.js +1902 -0
- package/esm/exec/executor.js +1004 -0
- package/esm/exec/host_streams.js +818 -0
- package/esm/exec/mod.js +4 -0
- package/esm/intrinsics/async_builtins.js +510 -0
- package/esm/intrinsics/context.js +90 -0
- package/esm/intrinsics/errors.js +13 -0
- package/esm/intrinsics/fact_calls.js +865 -0
- package/esm/intrinsics/mod.js +564 -0
- package/esm/intrinsics/stream_builtins.js +578 -0
- package/esm/intrinsics/transcode.js +443 -0
- package/esm/jspi/bridge.js +579 -0
- package/esm/jspi/mechanics.js +89 -0
- package/esm/jspi/mod.js +5 -0
- package/esm/jspi/suspending.js +15 -0
- package/esm/jspi/types.js +29 -0
- package/esm/package.json +3 -0
- package/esm/plan/format.js +5 -0
- package/esm/plan/loader.js +657 -0
- package/esm/plan/mod.js +3 -0
- package/esm/shim/mod.js +2 -0
- package/esm/shim/translator.js +94 -0
- package/esm/task/mod.js +483 -0
- package/esm/task/scheduler.js +1028 -0
- package/esm/task/streams.js +786 -0
- package/esm/task/subtask.js +135 -0
- package/esm/task/thread.js +255 -0
- package/esm/task/waitable.js +144 -0
- package/package.json +91 -0
- package/types/cabi/async_values.d.ts +35 -0
- package/types/cabi/bulk_lists.d.ts +18 -0
- package/types/cabi/context.d.ts +59 -0
- package/types/cabi/flatten.d.ts +14 -0
- package/types/cabi/float.d.ts +14 -0
- package/types/cabi/handles.d.ts +70 -0
- package/types/cabi/layout.d.ts +13 -0
- package/types/cabi/lift.d.ts +25 -0
- package/types/cabi/load.d.ts +14 -0
- package/types/cabi/lower.d.ts +10 -0
- package/types/cabi/memory.d.ts +58 -0
- package/types/cabi/mod.d.ts +15 -0
- package/types/cabi/store.d.ts +12 -0
- package/types/cabi/strings.d.ts +23 -0
- package/types/cabi/trap.d.ts +11 -0
- package/types/cabi/types.d.ts +206 -0
- package/types/cabi/values.d.ts +5 -0
- package/types/cache/core.d.ts +97 -0
- package/types/cache/dir.d.ts +6 -0
- package/types/cache/mod.d.ts +3 -0
- package/types/cache/web.d.ts +10 -0
- package/types/digest/digest.d.ts +17 -0
- package/types/digest/mod.d.ts +2 -0
- package/types/digest/verify.d.ts +48 -0
- package/types/embedder/casing.d.ts +40 -0
- package/types/embedder/copy.d.ts +24 -0
- package/types/embedder/errors.d.ts +11 -0
- package/types/embedder/imports.d.ts +47 -0
- package/types/embedder/instantiate.d.ts +88 -0
- package/types/embedder/mod.d.ts +11 -0
- package/types/embedder/resources.d.ts +158 -0
- package/types/embedder/streams.d.ts +202 -0
- package/types/embedder/values.d.ts +70 -0
- package/types/embedder/version.d.ts +85 -0
- package/types/exec/boundary.d.ts +360 -0
- package/types/exec/executor.d.ts +125 -0
- package/types/exec/host_streams.d.ts +165 -0
- package/types/exec/mod.d.ts +3 -0
- package/types/intrinsics/async_builtins.d.ts +69 -0
- package/types/intrinsics/context.d.ts +28 -0
- package/types/intrinsics/errors.d.ts +5 -0
- package/types/intrinsics/fact_calls.d.ts +120 -0
- package/types/intrinsics/mod.d.ts +187 -0
- package/types/intrinsics/stream_builtins.d.ts +113 -0
- package/types/intrinsics/transcode.d.ts +21 -0
- package/types/jspi/bridge.d.ts +227 -0
- package/types/jspi/mechanics.d.ts +50 -0
- package/types/jspi/mod.d.ts +3 -0
- package/types/jspi/suspending.d.ts +1 -0
- package/types/jspi/types.d.ts +26 -0
- package/types/plan/format.d.ts +369 -0
- package/types/plan/loader.d.ts +113 -0
- package/types/plan/mod.d.ts +2 -0
- package/types/shim/mod.d.ts +1 -0
- package/types/shim/translator.d.ts +55 -0
- package/types/task/mod.d.ts +257 -0
- package/types/task/scheduler.d.ts +421 -0
- package/types/task/streams.d.ts +370 -0
- package/types/task/subtask.d.ts +96 -0
- package/types/task/thread.d.ts +73 -0
- 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 };
|