@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,770 @@
|
|
|
1
|
+
// Stream / future conventions (contracts/embedder-api.md §"Streams and
|
|
2
|
+
// futures"; C2 checklist item 4).
|
|
3
|
+
//
|
|
4
|
+
// The low-level seam is `exec/host_streams.ts` — `HostStream`/`HostFuture`
|
|
5
|
+
// over the shared rendezvous object. This file is the *handle* layer named by
|
|
6
|
+
// the contract: `SharedStreamImpl` identity stays internal, embedders see
|
|
7
|
+
// `Stream<T>` / `Future<T>` / `ErrorContext`, and lowering accepts the natural
|
|
8
|
+
// JS producers (`ReadableStream`, `AsyncIterable`, arrays, `Promise`) with the
|
|
9
|
+
// layer owning the pumping.
|
|
10
|
+
import { despecialize } from "../cabi/types.js";
|
|
11
|
+
import { hostFuture, hostFutureFor, hostStream, hostStreamFor, } from "../exec/host_streams.js";
|
|
12
|
+
import { CopyResult, dropSharedForTeardown, poisonFailureOf, } from "../task/mod.js";
|
|
13
|
+
import { defineBrand, ERROR_CONTEXT, FUTURE, hasBrand, isStreamProducerError, STREAM, StreamProducerError, } from "@polyengine/protocol";
|
|
14
|
+
import { describeCrossCopy } from "./copy.js";
|
|
15
|
+
import { DroppedError, PeerTrappedError } from "./errors.js";
|
|
16
|
+
// `StreamProducerError`'s canonical definition moved to `@polyengine/protocol`
|
|
17
|
+
// with amendment A9 (it is an embedder-contract value: recognition must
|
|
18
|
+
// survive multiple runtime copies, issue #83). Re-exported here so every
|
|
19
|
+
// existing import path is unchanged.
|
|
20
|
+
export { StreamProducerError } from "@polyengine/protocol";
|
|
21
|
+
/**
|
|
22
|
+
* Failures recorded against a shared stream object whose driving store could
|
|
23
|
+
* not be reached (the stream was never lowered, or the store already carries a
|
|
24
|
+
* failure). Surfaced on the next interaction with the handle.
|
|
25
|
+
*/
|
|
26
|
+
const producerFailures = new WeakMap();
|
|
27
|
+
/**
|
|
28
|
+
* Report a producer failure on the channel that can actually attribute it.
|
|
29
|
+
*
|
|
30
|
+
* PRIMARY channel: `store.hostFailure`. This is the runtime's existing
|
|
31
|
+
* host-side failure slot — `driveAsync` checks it after every tick and throws
|
|
32
|
+
* it out of the driving loop (exec/boundary.ts:468/647/679/692), which is the
|
|
33
|
+
* driving loop of *the export call that is consuming this stream*. So the call
|
|
34
|
+
* that would otherwise have resolved with truncated data rejects with this
|
|
35
|
+
* error instead. It is the same channel `HostActivity.#pumpAsync` already uses
|
|
36
|
+
* for a trap raised while pumping between export calls
|
|
37
|
+
* (exec/host_streams.ts:284), so the two host-side stream failure paths agree.
|
|
38
|
+
*
|
|
39
|
+
* The report happens BEFORE the write end is dropped: the drop is what lets
|
|
40
|
+
* the guest observe end-of-stream and resolve, and `driveAsync` checks
|
|
41
|
+
* `hostFailure` before it checks `done()`.
|
|
42
|
+
*
|
|
43
|
+
* FALLBACK: no store bound (the stream was never lowered), or the store
|
|
44
|
+
* already carries an earlier failure. Then the cause is recorded against the
|
|
45
|
+
* shared object and raised on the next interaction with the handle.
|
|
46
|
+
*/
|
|
47
|
+
function reportProducerFailure(host, where, cause) {
|
|
48
|
+
// Brand, not class (A9): a producer failure raised by another runtime copy
|
|
49
|
+
// must not be re-wrapped into a second layer of the same error.
|
|
50
|
+
const err = isStreamProducerError(cause)
|
|
51
|
+
? cause
|
|
52
|
+
: new StreamProducerError(where, cause);
|
|
53
|
+
const shared = host.value;
|
|
54
|
+
producerFailures.set(host.value, err);
|
|
55
|
+
const store = shared.boundStore;
|
|
56
|
+
if (store != null && typeof store === "object") {
|
|
57
|
+
if (store.hostFailure === undefined)
|
|
58
|
+
store.hostFailure = err;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
/** @internal — raise a recorded producer failure, if any. */
|
|
64
|
+
function throwIfFailed(value, where = "stream") {
|
|
65
|
+
const e = producerFailures.get(value);
|
|
66
|
+
if (e !== undefined)
|
|
67
|
+
throw e;
|
|
68
|
+
throwIfPeerTrapped(value, where);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @internal — raise the recorded poisoning failure, if any (#66, amendment
|
|
72
|
+
* A7). Pre-op: an operation started after the peer's instance trapped must
|
|
73
|
+
* reject rather than park forever. Post-await (with the op's outcome in
|
|
74
|
+
* hand): an operation the retirement walk settled DROPPED-shaped must reject
|
|
75
|
+
* rather than fake a clean end — but an op that genuinely COMPLETED before
|
|
76
|
+
* the trap keeps its result (the fault still surfaces on the export call,
|
|
77
|
+
* and on this handle's next operation).
|
|
78
|
+
*/
|
|
79
|
+
function throwIfPeerTrapped(value, where, progress) {
|
|
80
|
+
const p = poisonFailureOf(value);
|
|
81
|
+
if (p !== undefined)
|
|
82
|
+
throw new PeerTrappedError(where, p, progress);
|
|
83
|
+
}
|
|
84
|
+
/** True for `stream<u8>` / `future<u8>`, whose chunks are `Uint8Array`. */
|
|
85
|
+
export function isU8Element(element) {
|
|
86
|
+
return element !== null && despecialize(element).kind === "u8";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A stream handle.
|
|
90
|
+
*
|
|
91
|
+
* `read` returning an empty chunk is end-of-stream, exactly as the contract
|
|
92
|
+
* spells it; `readable()` and the async iterator are built on it.
|
|
93
|
+
*/
|
|
94
|
+
export class Stream {
|
|
95
|
+
#host;
|
|
96
|
+
#codec;
|
|
97
|
+
/** Set once the handle's shared object has been handed to a guest. */
|
|
98
|
+
#consumed = false;
|
|
99
|
+
#dropped = false;
|
|
100
|
+
/** Waiters parked in `Stream.create()` until an element type is known. */
|
|
101
|
+
#binders = [];
|
|
102
|
+
constructor(host, codec) {
|
|
103
|
+
this.#host = host;
|
|
104
|
+
this.#codec = codec;
|
|
105
|
+
}
|
|
106
|
+
/** Wrap a stream value that was lifted out of a guest. */
|
|
107
|
+
static fromLifted(value, codec) {
|
|
108
|
+
return new Stream(hostStreamFor(value), codec);
|
|
109
|
+
}
|
|
110
|
+
/** Wrap a freshly created host-owned stream of a known element type. */
|
|
111
|
+
static fromHostStream(host, codec) {
|
|
112
|
+
return new Stream(host, codec);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `Stream.create<T>(): { stream, writer }` — the writer-side host end the
|
|
116
|
+
* contract names.
|
|
117
|
+
*
|
|
118
|
+
* The element type is deliberately NOT a parameter: the embedder does not
|
|
119
|
+
* have one (a `ValType` is a runtime-internal shape) and the *lowering site*
|
|
120
|
+
* always does. So the shared object is created lazily, at the moment the
|
|
121
|
+
* stream is passed to a guest, and writer operations issued before that park
|
|
122
|
+
* until then. A stream created and written but never passed anywhere simply
|
|
123
|
+
* never completes — the same honest hang the low-level layer documents.
|
|
124
|
+
*/
|
|
125
|
+
static create() {
|
|
126
|
+
const stream = new Stream(null, null);
|
|
127
|
+
return { stream, writer: new StreamWriter(stream) };
|
|
128
|
+
}
|
|
129
|
+
/** @internal — bind a lazily created stream to the lowering site's type. */
|
|
130
|
+
bindElement(codec) {
|
|
131
|
+
if (this.#host !== null)
|
|
132
|
+
return;
|
|
133
|
+
this.#codec = codec;
|
|
134
|
+
this.#host = hostStream(codec.element);
|
|
135
|
+
publishHostStream(this, this.#host);
|
|
136
|
+
const waiters = this.#binders;
|
|
137
|
+
this.#binders = [];
|
|
138
|
+
for (const w of waiters)
|
|
139
|
+
w();
|
|
140
|
+
}
|
|
141
|
+
/** @internal — resolve once this handle has a shared object. */
|
|
142
|
+
whenBound() {
|
|
143
|
+
if (this.#host !== null)
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
return new Promise((r) => this.#binders.push(r));
|
|
146
|
+
}
|
|
147
|
+
/** @internal */
|
|
148
|
+
get bound() {
|
|
149
|
+
return this.#host !== null;
|
|
150
|
+
}
|
|
151
|
+
/** @internal — the shared value to hand to a lowering site. */
|
|
152
|
+
takeValue(codec) {
|
|
153
|
+
this.bindElement(codec);
|
|
154
|
+
if (this.#consumed) {
|
|
155
|
+
throw new TypeError("this Stream handle has already been passed to a guest; a stream " +
|
|
156
|
+
"value may only be transferred once");
|
|
157
|
+
}
|
|
158
|
+
this.#consumed = true;
|
|
159
|
+
return this.#host.value;
|
|
160
|
+
}
|
|
161
|
+
/** @internal */
|
|
162
|
+
get codec() {
|
|
163
|
+
return this.#codec;
|
|
164
|
+
}
|
|
165
|
+
#require() {
|
|
166
|
+
if (this.#host === null) {
|
|
167
|
+
throw new TypeError("this Stream was created with Stream.create() and has not been " +
|
|
168
|
+
"passed to a guest yet, so it has no element type; pass it first, " +
|
|
169
|
+
"or use the writer, which parks until then");
|
|
170
|
+
}
|
|
171
|
+
// Post-transfer refusal (#162, embedder-api amendment A15). Lifting
|
|
172
|
+
// removes the handle from the source table and lowering installs it in
|
|
173
|
+
// the destination's (definitions.py `lift_async_value` line 1530,
|
|
174
|
+
// `lower_stream` line 1828): once this handle's shared object has been
|
|
175
|
+
// passed to a guest, the guest owns the readable end and a host read here
|
|
176
|
+
// would operate a phantom duplicate of it. Refuse loudly instead.
|
|
177
|
+
// `StreamWriter` is deliberately unaffected — the host retains the
|
|
178
|
+
// writable end, and writing after the pass is the normal A5 pattern —
|
|
179
|
+
// and `drop()`/`cancelRead()` stay permissive.
|
|
180
|
+
if (this.#consumed) {
|
|
181
|
+
throw new TypeError("this Stream handle has already been passed to a guest; the guest " +
|
|
182
|
+
"owns its readable end, so it can no longer be read from the host " +
|
|
183
|
+
"(issue #162)");
|
|
184
|
+
}
|
|
185
|
+
return this.#host;
|
|
186
|
+
}
|
|
187
|
+
/** Low-level read: up to `max` elements; an empty chunk means end-of-stream. */
|
|
188
|
+
async read(max) {
|
|
189
|
+
const host = this.#require();
|
|
190
|
+
const where = this.#codec?.where ?? "stream read";
|
|
191
|
+
throwIfFailed(host.value, where);
|
|
192
|
+
const raw = await host.readable.read(max);
|
|
193
|
+
// An empty chunk normally means clean end-of-stream; when the peer's
|
|
194
|
+
// instance trapped it means the retirement walk settled us — reject
|
|
195
|
+
// instead of faking EOS (amendment A7). A non-empty chunk was really
|
|
196
|
+
// copied before the trap and is delivered; the next read rejects.
|
|
197
|
+
if (raw.length === 0)
|
|
198
|
+
throwIfPeerTrapped(host.value, where);
|
|
199
|
+
return this.#chunk(raw);
|
|
200
|
+
}
|
|
201
|
+
#chunk(raw) {
|
|
202
|
+
const codec = this.#codec;
|
|
203
|
+
if (isU8Element(codec.element)) {
|
|
204
|
+
// The exec layer already resolves u8 reads as a Uint8Array (the
|
|
205
|
+
// rendezvous copy itself — issue #54); pass it through untouched so a
|
|
206
|
+
// host read costs exactly that one copy. Uint8Array.from covers
|
|
207
|
+
// raw-layer writers that fed plain arrays.
|
|
208
|
+
return (raw instanceof Uint8Array
|
|
209
|
+
? raw
|
|
210
|
+
: Uint8Array.from(raw));
|
|
211
|
+
}
|
|
212
|
+
const vs = raw instanceof Uint8Array ? Array.from(raw) : raw;
|
|
213
|
+
return vs.map((v) => codec.toHost(v));
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Cancel an in-flight `read` (R-fix review advisory 1).
|
|
217
|
+
*
|
|
218
|
+
* #97, DELIBERATE AND PINNED: the cancelled `read` resolves with whatever
|
|
219
|
+
* had already arrived — typically the empty chunk, which this layer also
|
|
220
|
+
* uses as end-of-stream (`read`'s contract, and hence `readable()` and the
|
|
221
|
+
* async iterator, which close on it). **A cancelled read is therefore
|
|
222
|
+
* indistinguishable from EOS at this layer.** Kept as-is rather than given
|
|
223
|
+
* a distinct signal: the caller of `cancelRead()` is the same code that
|
|
224
|
+
* observes the read's result, so it already knows which happened, and only
|
|
225
|
+
* that caller can reach the state. See exec/host_streams.ts
|
|
226
|
+
* `HostReadableEnd.cancelRead` for the mechanism.
|
|
227
|
+
*/
|
|
228
|
+
cancelRead() {
|
|
229
|
+
this.#host?.readable.cancelRead();
|
|
230
|
+
}
|
|
231
|
+
drop() {
|
|
232
|
+
if (this.#dropped)
|
|
233
|
+
return;
|
|
234
|
+
this.#dropped = true;
|
|
235
|
+
// Both ends of a host wrapper name the same shared object; dropping once
|
|
236
|
+
// is enough (`SharedStreamImpl.drop` is idempotent).
|
|
237
|
+
this.#host?.readable.drop();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* @internal — teardown after a trapping import abandoned this handle
|
|
241
|
+
* (#66, instantiate.ts `releaseAsyncArgs`). Unlike `drop()`, this goes
|
|
242
|
+
* through `dropSharedForTeardown`, whose parked-side discipline never
|
|
243
|
+
* wakes the about-to-be-poisoned caller (review B2: a plain drop queued a
|
|
244
|
+
* DROPPED event into the trapping instance's waitables, and a later
|
|
245
|
+
* driving loop asserted on the corpse).
|
|
246
|
+
*
|
|
247
|
+
* The arm is released on this path too (#162, amendment A15): the wrapper's
|
|
248
|
+
* `HostActivity` now closes through the shared object's drop observers,
|
|
249
|
+
* which `dropSharedForTeardown` fires unconditionally — so a teardown with
|
|
250
|
+
* nothing parked no longer leaves the arm outliving the stream. (This
|
|
251
|
+
* paragraph previously recorded that asymmetry as a known, non-blocking
|
|
252
|
+
* review advisory.)
|
|
253
|
+
*/
|
|
254
|
+
dropForTeardown() {
|
|
255
|
+
if (this.#dropped)
|
|
256
|
+
return;
|
|
257
|
+
this.#dropped = true;
|
|
258
|
+
if (this.#host !== null) {
|
|
259
|
+
dropSharedForTeardown(this.#host.value);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
[Symbol.dispose]() {
|
|
263
|
+
this.drop();
|
|
264
|
+
}
|
|
265
|
+
/** Web-native view: `ReadableStream<Chunk<T>>`. */
|
|
266
|
+
readable() {
|
|
267
|
+
const self = this;
|
|
268
|
+
return new ReadableStream({
|
|
269
|
+
async pull(controller) {
|
|
270
|
+
const chunk = await self.read(READ_CHUNK);
|
|
271
|
+
if (chunk.length === 0) {
|
|
272
|
+
controller.close();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
controller.enqueue(chunk);
|
|
276
|
+
},
|
|
277
|
+
cancel() {
|
|
278
|
+
self.drop();
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async *[Symbol.asyncIterator]() {
|
|
283
|
+
for (;;) {
|
|
284
|
+
const chunk = await this.read(READ_CHUNK);
|
|
285
|
+
if (chunk.length === 0)
|
|
286
|
+
return;
|
|
287
|
+
yield chunk;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** How many elements a convenience read asks for at a time. */
|
|
292
|
+
const READ_CHUNK = 4096;
|
|
293
|
+
/** Writer half of `Stream.create()`. */
|
|
294
|
+
export class StreamWriter {
|
|
295
|
+
#stream;
|
|
296
|
+
constructor(stream) {
|
|
297
|
+
this.#stream = stream;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Offer values; resolves with how many the reader took.
|
|
301
|
+
*
|
|
302
|
+
* `Chunk<T>` mirrors the read side: a u8 stream accepts a `Uint8Array`
|
|
303
|
+
* (taken as already-lowered bytes), and a plain array of any element type
|
|
304
|
+
* is lowered per element. u8 chunks travel as `Uint8Array` all the way to
|
|
305
|
+
* the CABI store's bulk path (issue #54) — which makes a `Uint8Array`
|
|
306
|
+
* chunk a BORROW until the returned promise settles; mutating it in that
|
|
307
|
+
* window is misuse. Plain-array chunks are lowered (copied) up front.
|
|
308
|
+
*/
|
|
309
|
+
async write(values) {
|
|
310
|
+
await this.#stream.whenBound();
|
|
311
|
+
const host = hostOf(this.#stream);
|
|
312
|
+
const where = this.#stream.codec?.where ?? "stream write";
|
|
313
|
+
throwIfFailed(host.value, where);
|
|
314
|
+
const n = await host.writable.write(packChunk(values, this.#stream.codec));
|
|
315
|
+
// A short take normally means "re-offer later" / "reader done"; when the
|
|
316
|
+
// reader's instance trapped it means the retirement walk settled us —
|
|
317
|
+
// reject, carrying the delivered count (amendment A7). A full take
|
|
318
|
+
// genuinely completed before the trap and stays a success.
|
|
319
|
+
if (n < values.length)
|
|
320
|
+
throwIfPeerTrapped(host.value, where, n);
|
|
321
|
+
return n;
|
|
322
|
+
}
|
|
323
|
+
/** Offer values until all are taken or the reader goes away. */
|
|
324
|
+
async writeAll(values) {
|
|
325
|
+
await this.#stream.whenBound();
|
|
326
|
+
const host = hostOf(this.#stream);
|
|
327
|
+
const where = this.#stream.codec?.where ?? "stream write";
|
|
328
|
+
throwIfFailed(host.value, where);
|
|
329
|
+
const n = await host.writable.writeAll(packChunk(values, this.#stream.codec));
|
|
330
|
+
if (n < values.length)
|
|
331
|
+
throwIfPeerTrapped(host.value, where, n);
|
|
332
|
+
return n;
|
|
333
|
+
}
|
|
334
|
+
cancelWrite() {
|
|
335
|
+
if (!this.#stream.bound)
|
|
336
|
+
return;
|
|
337
|
+
hostOf(this.#stream).writable.cancelWrite();
|
|
338
|
+
}
|
|
339
|
+
/** End-of-stream. */
|
|
340
|
+
async close() {
|
|
341
|
+
await this.#stream.whenBound();
|
|
342
|
+
hostOf(this.#stream).writable.drop();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const hostOfStream = new WeakMap();
|
|
346
|
+
function hostOf(s) {
|
|
347
|
+
// The host end lives behind `Stream`'s private field; `takeValue`/`read`
|
|
348
|
+
// are the public routes. The writer needs the writable half, so the handle
|
|
349
|
+
// publishes it here at bind time.
|
|
350
|
+
const h = hostOfStream.get(s);
|
|
351
|
+
if (h === undefined) {
|
|
352
|
+
throw new TypeError("stream writer used before the stream was bound");
|
|
353
|
+
}
|
|
354
|
+
return h;
|
|
355
|
+
}
|
|
356
|
+
/** @internal — publish the host end for `StreamWriter` (see `hostOf`). */
|
|
357
|
+
export function publishHostStream(s, h) {
|
|
358
|
+
hostOfStream.set(s, h);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* A future handle. `await`able directly (`PromiseLike`), and droppable.
|
|
362
|
+
*
|
|
363
|
+
* A future whose write end dropped without ever writing rejects with
|
|
364
|
+
* `DroppedError` — not `undefined`, which `future<void>` legitimately yields.
|
|
365
|
+
*/
|
|
366
|
+
export class Future {
|
|
367
|
+
/** Present once the underlying host end exists. */
|
|
368
|
+
#host;
|
|
369
|
+
/** Always present; resolves to the host end (immediately, when not deferred). */
|
|
370
|
+
#hostP;
|
|
371
|
+
#codec;
|
|
372
|
+
#consumed = false;
|
|
373
|
+
#dropped = false;
|
|
374
|
+
#settled = null;
|
|
375
|
+
constructor(host, hostP, codec) {
|
|
376
|
+
this.#host = host;
|
|
377
|
+
this.#hostP = hostP;
|
|
378
|
+
this.#codec = codec;
|
|
379
|
+
}
|
|
380
|
+
static fromLifted(value, codec) {
|
|
381
|
+
const h = hostFutureFor(value);
|
|
382
|
+
return new Future(h, Promise.resolve(h), codec);
|
|
383
|
+
}
|
|
384
|
+
static fromHostFuture(host, codec) {
|
|
385
|
+
return new Future(host, Promise.resolve(host), codec);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* A future that is still in flight: the guest call that produces it has not
|
|
389
|
+
* resolved yet.
|
|
390
|
+
*
|
|
391
|
+
* CONTRACT (contracts/embedder-api.md): §"Functions and async" makes every
|
|
392
|
+
* export Promise-shaped, and §"Streams and futures" makes `Future<T>` a
|
|
393
|
+
* `PromiseLike<T>`. For an export whose *result* is a `future<T>` those two
|
|
394
|
+
* collide irreducibly: JS promise resolution unconditionally adopts a
|
|
395
|
+
* thenable, so `await someExport()` can never hand back a thenable handle —
|
|
396
|
+
* it hands back the value the handle would have yielded. Conservative
|
|
397
|
+
* reading, implemented here: the export returns the handle **eagerly** (it
|
|
398
|
+
* is itself PromiseLike, so `await` still works and still yields `T`), which
|
|
399
|
+
* keeps `drop()`/`cancel()` reachable for a caller that does not await. The
|
|
400
|
+
* alternative — resolving a Promise *to* the handle — is not expressible.
|
|
401
|
+
* Flagged in the C2 report.
|
|
402
|
+
*/
|
|
403
|
+
static deferred(pending, codec) {
|
|
404
|
+
const hostP = pending.then((v) => {
|
|
405
|
+
const h = hostFutureFor(v);
|
|
406
|
+
f.adopt(h);
|
|
407
|
+
return h;
|
|
408
|
+
});
|
|
409
|
+
// Backstop (issue #182): a deferred handle that is never awaited,
|
|
410
|
+
// dropped, or cancelled still has `#hostP` sitting there uninspected — if
|
|
411
|
+
// the producing export call rejects, that is an unhandled rejection at
|
|
412
|
+
// the process level with no handle-level operation to blame. Attach a
|
|
413
|
+
// no-op rejection handler to a SEPARATE derived promise; `#read()` above
|
|
414
|
+
// still awaits the original `hostP`, so a real failure still surfaces to
|
|
415
|
+
// an awaiter (or through `cancel()`/`drop()`'s own swallows) exactly as
|
|
416
|
+
// before.
|
|
417
|
+
hostP.catch(() => { });
|
|
418
|
+
const f = new Future(null, hostP, codec);
|
|
419
|
+
return f;
|
|
420
|
+
}
|
|
421
|
+
/** @internal */
|
|
422
|
+
adopt(h) {
|
|
423
|
+
this.#host = h;
|
|
424
|
+
}
|
|
425
|
+
/** @internal */
|
|
426
|
+
takeValue() {
|
|
427
|
+
if (this.#host === null) {
|
|
428
|
+
throw new TypeError("this Future is still in flight and cannot be passed to a guest yet");
|
|
429
|
+
}
|
|
430
|
+
if (this.#consumed) {
|
|
431
|
+
throw new TypeError("this Future handle has already been passed to a guest");
|
|
432
|
+
}
|
|
433
|
+
this.#consumed = true;
|
|
434
|
+
return this.#host.value;
|
|
435
|
+
}
|
|
436
|
+
#read() {
|
|
437
|
+
// Post-transfer refusal (#162, amendment A15), the `Stream.read` mirror:
|
|
438
|
+
// once this handle was passed to a guest, the guest owns the readable end
|
|
439
|
+
// and a host read would operate a phantom duplicate. A read MEMOIZED
|
|
440
|
+
// before the transfer keeps resolving — it genuinely happened while the
|
|
441
|
+
// host still owned the end. Rejected rather than thrown: this runs under
|
|
442
|
+
// `then()`, where a synchronous throw escapes the promise chain.
|
|
443
|
+
if (this.#consumed && this.#settled === null) {
|
|
444
|
+
return Promise.reject(new TypeError("this Future handle has already been passed to a guest; the guest " +
|
|
445
|
+
"owns its readable end, so it can no longer be read from the " +
|
|
446
|
+
"host (issue #162)"));
|
|
447
|
+
}
|
|
448
|
+
this.#settled ??= (async () => {
|
|
449
|
+
const host = await this.#hostP;
|
|
450
|
+
const { value, result } = await host.readResult();
|
|
451
|
+
if (result !== CopyResult.COMPLETED) {
|
|
452
|
+
// A drop caused by the writer's instance trapping is a fault, not a
|
|
453
|
+
// "no value" outcome — brand it (#66, amendment A7).
|
|
454
|
+
throwIfPeerTrapped(host.value, this.#codec.where ?? "future read");
|
|
455
|
+
throw new DroppedError(result === CopyResult.CANCELLED
|
|
456
|
+
? "the future read was cancelled"
|
|
457
|
+
: "the future's write end was dropped without a value");
|
|
458
|
+
}
|
|
459
|
+
return this.#codec.toHost(value);
|
|
460
|
+
})();
|
|
461
|
+
return this.#settled;
|
|
462
|
+
}
|
|
463
|
+
then(onfulfilled, onrejected) {
|
|
464
|
+
return this.#read().then(onfulfilled, onrejected);
|
|
465
|
+
}
|
|
466
|
+
cancel() {
|
|
467
|
+
if (this.#host !== null)
|
|
468
|
+
this.#host.cancel();
|
|
469
|
+
// A deferred future whose host end never materialized has nothing to
|
|
470
|
+
// cancel; swallow that rejection rather than let `cancel()` produce an
|
|
471
|
+
// unhandled one (issue #182 — mirrors `drop()` below).
|
|
472
|
+
else
|
|
473
|
+
void this.#hostP.then((h) => h.cancel(), () => { });
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Release this future handle. Total and idempotent (#90): it never throws,
|
|
477
|
+
* and calling it twice — or after `Symbol.dispose` — is a no-op.
|
|
478
|
+
*
|
|
479
|
+
* Dropping a future the host never wrote to, once the guest already holds
|
|
480
|
+
* its readable end, is **abandonment**: the guest's reader can never be
|
|
481
|
+
* satisfied, so it is armed with a trap at its rendezvous point rather than
|
|
482
|
+
* being handed a value-less completion (exec/host_streams.ts
|
|
483
|
+
* `HostFuture.drop`, task/streams.ts `abandonSharedFuture`; the spec keeps
|
|
484
|
+
* that state unreachable by trapping the early writable drop,
|
|
485
|
+
* definitions.py:1183-1184). Write-then-drop is the normal path and is
|
|
486
|
+
* unaffected; a future no guest ever saw is plain cleanup.
|
|
487
|
+
*/
|
|
488
|
+
drop() {
|
|
489
|
+
if (this.#dropped)
|
|
490
|
+
return;
|
|
491
|
+
this.#dropped = true;
|
|
492
|
+
if (this.#host !== null)
|
|
493
|
+
this.#host.drop();
|
|
494
|
+
// A deferred future whose host end never materialized has nothing to
|
|
495
|
+
// release; swallow that rejection rather than let `drop()` produce an
|
|
496
|
+
// unhandled one.
|
|
497
|
+
else
|
|
498
|
+
void this.#hostP.then((h) => h.drop(), () => { });
|
|
499
|
+
}
|
|
500
|
+
/** @internal — see `Stream.dropForTeardown` (#66). */
|
|
501
|
+
dropForTeardown() {
|
|
502
|
+
if (this.#dropped)
|
|
503
|
+
return;
|
|
504
|
+
if (this.#host !== null) {
|
|
505
|
+
this.#dropped = true;
|
|
506
|
+
dropSharedForTeardown(this.#host.value);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// A deferred future (still in flight) cannot be an import argument;
|
|
510
|
+
// fall back to the plain drop for completeness.
|
|
511
|
+
this.drop();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
[Symbol.dispose]() {
|
|
515
|
+
this.drop();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* `error-context` as the contract spells it: `{ readonly message: string }`.
|
|
520
|
+
* The internal value is `task/streams.ts`'s `ErrorContext` (debug message
|
|
521
|
+
* only, per definitions.py).
|
|
522
|
+
*/
|
|
523
|
+
export class ErrorContext {
|
|
524
|
+
message;
|
|
525
|
+
/** @internal — the internal value, preserved so it can be lowered back. */
|
|
526
|
+
internal;
|
|
527
|
+
constructor(internal) {
|
|
528
|
+
this.internal = internal;
|
|
529
|
+
this.message = internal.debugMessage;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// A9 brands (contracts/embedder-api.md §"Module identity"): the three
|
|
533
|
+
// STATEFUL embedder-facing handle classes. Their machinery lives in the copy
|
|
534
|
+
// that minted them, so the brand never makes a foreign handle usable — it
|
|
535
|
+
// makes it DIAGNOSABLE, at the lowering sites below.
|
|
536
|
+
defineBrand(Stream.prototype, STREAM);
|
|
537
|
+
defineBrand(Future.prototype, FUTURE);
|
|
538
|
+
defineBrand(ErrorContext.prototype, ERROR_CONTEXT);
|
|
539
|
+
/**
|
|
540
|
+
* Adapt a producer to a lowered `stream<T>` value, and own the pumping.
|
|
541
|
+
*
|
|
542
|
+
* The driving arm auto-closes on end (the pump drops the write end when the
|
|
543
|
+
* producer is exhausted) and on `DROPPED` (host_streams settles the activity
|
|
544
|
+
* arm) — R-fix review advisory 2, the deadlock-masking activity-lifetime
|
|
545
|
+
* footgun.
|
|
546
|
+
*/
|
|
547
|
+
export function lowerStreamSource(src, codec) {
|
|
548
|
+
// Order matters (amendment A9). Same-copy handle: the fast path, unchanged.
|
|
549
|
+
if (src instanceof Stream) {
|
|
550
|
+
return src.takeValue(codec);
|
|
551
|
+
}
|
|
552
|
+
// Branded but not ours: a `Stream` minted by ANOTHER runtime copy. Without
|
|
553
|
+
// this check it would fall through to producer adaptation below and be
|
|
554
|
+
// pumped by its async iterator — a silent downgrade that quietly voids A5's
|
|
555
|
+
// identity guarantees. Refused, loudly, naming both copies (issue #83).
|
|
556
|
+
if (hasBrand(src, STREAM)) {
|
|
557
|
+
throw new TypeError(describeCrossCopy("this stream handle", "To pipe it by value, pass `src.readable()` instead."));
|
|
558
|
+
}
|
|
559
|
+
const host = hostStream(codec.element);
|
|
560
|
+
const stream = Stream.fromHostStream(host, codec);
|
|
561
|
+
publishHostStream(stream, host);
|
|
562
|
+
void pump(src, host, codec);
|
|
563
|
+
return host.value;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Lower one chunk of stream elements.
|
|
567
|
+
*
|
|
568
|
+
* u8 chunks come out as `Uint8Array` — either the caller's own (bytes are
|
|
569
|
+
* already canonical component values; issue #54's bulk-store path picks the
|
|
570
|
+
* typed array up unchanged at the rendezvous) or packed from a validated
|
|
571
|
+
* plain array. A `Uint8Array` offered to a NON-u8 stream keeps the legacy
|
|
572
|
+
* behavior: elements are fed through the per-element codec like any array.
|
|
573
|
+
*/
|
|
574
|
+
function packChunk(values, codec) {
|
|
575
|
+
const u8 = isU8Element(codec.element);
|
|
576
|
+
if (values instanceof Uint8Array) {
|
|
577
|
+
if (u8)
|
|
578
|
+
return values;
|
|
579
|
+
return Array.from(values).map((v) => codec.fromHost(v));
|
|
580
|
+
}
|
|
581
|
+
const lowered = values.map((v) => codec.fromHost(v));
|
|
582
|
+
return u8
|
|
583
|
+
? Uint8Array.from(lowered)
|
|
584
|
+
: lowered;
|
|
585
|
+
}
|
|
586
|
+
/** Race sentinel: the reader's end dropped while the producer was parked. */
|
|
587
|
+
const READER_GONE = Symbol("polyengine reader gone");
|
|
588
|
+
async function pump(src, host, codec) {
|
|
589
|
+
const where = codec.where ?? "stream producer";
|
|
590
|
+
let failure;
|
|
591
|
+
let produced = 0;
|
|
592
|
+
// A13 cancellation companion: the pump learns of the reader dropping
|
|
593
|
+
// through short writes, but a producer PARKED on an external event (an
|
|
594
|
+
// accept-shaped source holding a live platform resource) offers no write
|
|
595
|
+
// to shorten — this notification is its only stop signal. It also fires
|
|
596
|
+
// on the A7 teardown walk and on our own end-of-pump drop (harmless: the
|
|
597
|
+
// loop has exited by then).
|
|
598
|
+
const gone = new Promise((resolve) => host.writable.onDropped(() => resolve(READER_GONE)));
|
|
599
|
+
try {
|
|
600
|
+
for await (const batch of batches(src, gone)) {
|
|
601
|
+
// Lowering is the likeliest failure (a value of the wrong shape) and it
|
|
602
|
+
// must be attributed to the site, not swallowed into a short stream.
|
|
603
|
+
const lowered = packChunk(batch, codec);
|
|
604
|
+
let n;
|
|
605
|
+
try {
|
|
606
|
+
n = await host.writable.writeAll(lowered);
|
|
607
|
+
}
|
|
608
|
+
catch (e) {
|
|
609
|
+
// A13: elements past the fault's progress point were lowered but
|
|
610
|
+
// will never be taken — destroy them (an `own` element may hold a
|
|
611
|
+
// live platform resource). `PeerTrappedError.progress` reports
|
|
612
|
+
// delivered-before-the-fault; anything else delivered nothing.
|
|
613
|
+
releaseUntaken(lowered, e instanceof PeerTrappedError ? e.progress ?? 0 : 0, codec);
|
|
614
|
+
throw e;
|
|
615
|
+
}
|
|
616
|
+
produced += n;
|
|
617
|
+
if (n < lowered.length) {
|
|
618
|
+
// The reader went away: a clean end — but the un-taken tail of this
|
|
619
|
+
// chunk was already lowered and must be destroyed, not leaked (A13).
|
|
620
|
+
releaseUntaken(lowered, n, codec);
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
failure = e;
|
|
627
|
+
}
|
|
628
|
+
if (failure !== undefined) {
|
|
629
|
+
void produced;
|
|
630
|
+
// Report BEFORE dropping: the drop is what lets the guest see
|
|
631
|
+
// end-of-stream and resolve, and the driving loop checks `hostFailure`
|
|
632
|
+
// before it checks `done()`.
|
|
633
|
+
reportProducerFailure(host, where, failure);
|
|
634
|
+
}
|
|
635
|
+
// End of production == end of stream. Dropping unconditionally is what keeps
|
|
636
|
+
// the activity arm from outliving the data (R-fix advisory 2) and what stops
|
|
637
|
+
// a failed producer from hanging the guest forever; the failure has already
|
|
638
|
+
// been recorded on the store, so the call fails rather than resolving.
|
|
639
|
+
host.writable.drop();
|
|
640
|
+
}
|
|
641
|
+
/** A13: destroy `lowered[taken..]` when a codec's elements hold resources. */
|
|
642
|
+
function releaseUntaken(lowered, taken, codec) {
|
|
643
|
+
const release = codec.release;
|
|
644
|
+
if (release === undefined || lowered instanceof Uint8Array)
|
|
645
|
+
return;
|
|
646
|
+
for (let i = taken; i < lowered.length; i++)
|
|
647
|
+
release(lowered[i]);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Normalize every accepted producer shape to an async iterator of batches,
|
|
651
|
+
* racing each pull against `gone` (A13 cancellation): when the stream dies
|
|
652
|
+
* with the producer parked, a `ReadableStream` source is `cancel()`ed
|
|
653
|
+
* through its reader, and an (async-)iterable source gets its optional
|
|
654
|
+
* `cancel()` method invoked — the documented producer-cancellation hook —
|
|
655
|
+
* then its pending pull is drained so a straggler element the producer
|
|
656
|
+
* already minted still reaches the caller's release path. A source with no
|
|
657
|
+
* cancel hook keeps the pre-A13 behavior: the pump stays parked until the
|
|
658
|
+
* producer's next element (or forever — the documented embedder-negligence
|
|
659
|
+
* hang class).
|
|
660
|
+
*/
|
|
661
|
+
async function* batches(src, gone) {
|
|
662
|
+
if (isReadableStream(src)) {
|
|
663
|
+
const reader = src.getReader();
|
|
664
|
+
try {
|
|
665
|
+
for (;;) {
|
|
666
|
+
const r = await Promise.race([reader.read(), gone]);
|
|
667
|
+
if (r === READER_GONE) {
|
|
668
|
+
// `cancel` settles the pending read and runs the source's own
|
|
669
|
+
// cancel() — releasing whatever platform resource backed it.
|
|
670
|
+
await reader.cancel().catch(() => { });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (r.done)
|
|
674
|
+
return;
|
|
675
|
+
yield asBatch(r.value);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
reader.releaseLock();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (Symbol.asyncIterator in src) {
|
|
683
|
+
const it = src[Symbol.asyncIterator]();
|
|
684
|
+
try {
|
|
685
|
+
for (;;) {
|
|
686
|
+
const pending = it.next();
|
|
687
|
+
const r = await Promise.race([pending, gone]);
|
|
688
|
+
if (r === READER_GONE) {
|
|
689
|
+
src.cancel?.();
|
|
690
|
+
try {
|
|
691
|
+
const last = await pending;
|
|
692
|
+
if (!last.done)
|
|
693
|
+
yield asBatch(last.value);
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
// A cancelled pull rejecting is its natural shape; the
|
|
697
|
+
// producer's own failure reporting has nothing to add here —
|
|
698
|
+
// the stream is already dead.
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (r.done)
|
|
703
|
+
return;
|
|
704
|
+
yield asBatch(r.value);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
finally {
|
|
708
|
+
// Runs the source generator's own finally blocks. Queued behind any
|
|
709
|
+
// still-pending pull, which the GONE arm above has already drained.
|
|
710
|
+
await it.return?.();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
for (const v of src)
|
|
714
|
+
yield asBatch(v);
|
|
715
|
+
}
|
|
716
|
+
function asBatch(v) {
|
|
717
|
+
// Kept whole: `packChunk` decides whether the bytes are already lowered
|
|
718
|
+
// (u8 element) or need the per-element codec (any other element type).
|
|
719
|
+
if (v instanceof Uint8Array)
|
|
720
|
+
return v;
|
|
721
|
+
if (Array.isArray(v))
|
|
722
|
+
return v;
|
|
723
|
+
return [v];
|
|
724
|
+
}
|
|
725
|
+
function isReadableStream(v) {
|
|
726
|
+
return typeof ReadableStream !== "undefined" && v instanceof ReadableStream;
|
|
727
|
+
}
|
|
728
|
+
/** Adapt a `Promise`/`Future`/plain value to a lowered `future<T>` value. */
|
|
729
|
+
export function lowerFutureSource(src, codec) {
|
|
730
|
+
if (src instanceof Future)
|
|
731
|
+
return src.takeValue();
|
|
732
|
+
// Branded but not ours (amendment A9). This one is the sharpest edge in the
|
|
733
|
+
// family: `Future` is a `PromiseLike`, so a foreign future would otherwise
|
|
734
|
+
// be adopted as a plain thenable and appear to work — exactly the silent
|
|
735
|
+
// path A9 bans, since the awaited value would ride the OTHER copy's
|
|
736
|
+
// machinery with no handle transfer at all.
|
|
737
|
+
if (hasBrand(src, FUTURE)) {
|
|
738
|
+
throw new TypeError(describeCrossCopy("this future handle", "To pipe it by value, pass `Promise.resolve(f)` instead."));
|
|
739
|
+
}
|
|
740
|
+
const host = hostFuture(codec.element);
|
|
741
|
+
void (async () => {
|
|
742
|
+
try {
|
|
743
|
+
const v = await src;
|
|
744
|
+
await host.write(codec.fromHost(v));
|
|
745
|
+
}
|
|
746
|
+
catch (e) {
|
|
747
|
+
// The producer failed. `future<T>` has no error channel of its own, so
|
|
748
|
+
// the guest could only ever see a bare drop — the cause goes on the
|
|
749
|
+
// store's host-failure channel instead, exactly as for streams, so the
|
|
750
|
+
// in-flight call fails with a site-named error.
|
|
751
|
+
//
|
|
752
|
+
// And then we do NOT drop -- for ATTRIBUTION, not for safety. Dropping
|
|
753
|
+
// here is now well-defined (#90: an unwritten, lowered future's drop
|
|
754
|
+
// abandons it and the guest reader traps at its rendezvous point,
|
|
755
|
+
// exec/host_streams.ts `HostFuture.drop`); the stale version of this
|
|
756
|
+
// comment claimed it would trip an internal invariant, which was true
|
|
757
|
+
// before the abandonment mechanism existed and is not true now.
|
|
758
|
+
// Reporting instead of dropping is still the better outcome: the
|
|
759
|
+
// store-level failure names the producer and the site, so the in-flight
|
|
760
|
+
// call fails with the real cause rather than with a generic
|
|
761
|
+
// "the writable end went away" trap. Only when there is NO store to
|
|
762
|
+
// report to (the future was never lowered) do we fall back to dropping,
|
|
763
|
+
// so nothing can hang forever.
|
|
764
|
+
const reported = reportProducerFailure({ value: host.value }, codec.where ?? "future producer", e);
|
|
765
|
+
if (!reported)
|
|
766
|
+
host.drop();
|
|
767
|
+
}
|
|
768
|
+
})();
|
|
769
|
+
return host.value;
|
|
770
|
+
}
|