@polyengine/runtime 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,8 @@ import type { ValType } from "../cabi/types.js";
2
2
  import type { ComponentValue } from "../cabi/types.js";
3
3
  import { type HostFuture, type HostStream } from "../exec/host_streams.js";
4
4
  import { ErrorContext as InternalErrorContext } from "../task/mod.js";
5
- /** `Chunk<u8>` is a `Uint8Array`; every other element type chunks as `T[]`. */
6
- export type Chunk<T> = T extends number ? Uint8Array | T[] : T[];
5
+ import { type Chunk, type DirectDestination, type DirectSource, type DirectVerdict, type ErrorContext as ProtocolErrorContext, type Future as ProtocolFuture, type Stream as ProtocolStream, type StreamWriter as ProtocolStreamWriter } from "@polyengine/protocol";
6
+ export type { Chunk } from "@polyengine/protocol";
7
7
  /**
8
8
  * Per-element adaptation, supplied by the value adapter.
9
9
  * @internal — supplied by the value adapter, never by a host.
@@ -26,13 +26,14 @@ export interface ElemCodec<T> {
26
26
  export { StreamProducerError } from "@polyengine/protocol";
27
27
  /** True for `stream<u8>` / `future<u8>`, whose chunks are `Uint8Array`. */
28
28
  export declare function isU8Element(element: ValType | null): boolean;
29
+ export type { DirectDestination, DirectSource, DirectVerdict } from "@polyengine/protocol";
29
30
  /**
30
31
  * A stream handle.
31
32
  *
32
33
  * `read` returning an empty chunk is end-of-stream, exactly as the contract
33
34
  * spells it; `readable()` and the async iterator are built on it.
34
35
  */
35
- export declare class Stream<T> {
36
+ export declare class Stream<T> implements ProtocolStream<T> {
36
37
  #private;
37
38
  private constructor();
38
39
  /** Wrap a stream value that was lifted out of a guest. */
@@ -66,6 +67,29 @@ export declare class Stream<T> {
66
67
  get codec(): ElemCodec<T> | null;
67
68
  /** Low-level read: up to `max` elements; an empty chunk means end-of-stream. */
68
69
  read(max: number): Promise<Chunk<T>>;
70
+ /**
71
+ * Consume the writer's bytes in place, without an intermediate chunk
72
+ * (`stream<u8>` only — contracts/embedder-api.md amendment A21,
73
+ * polyengine#128).
74
+ *
75
+ * At every rendezvous with a writer of nonzero capacity, `consume` runs
76
+ * exactly once, synchronously, with a `DirectSource` over the writer's
77
+ * unread bytes — guest linear memory when the peer is a guest, so the
78
+ * consumer's own `set()`/`subarray` copy IS the canonical-ABI copy.
79
+ * `"more"` keeps the session parked for the next rendezvous; `"done"` ends
80
+ * it. Resolves with the session's total byte count. Marking a prefix is
81
+ * normal: the writer re-offers the rest on its own schedule.
82
+ *
83
+ * `"done"` with zero bytes marked *retracts*: the session ends and the
84
+ * writer's operation stays parked, with no event delivered. `"more"` with
85
+ * zero marked, and a throwing callback, reject — and in both cases the
86
+ * writer's parked operation survives and the stream stays alive.
87
+ *
88
+ * Refusals mirror `read`: an unbound `Stream.create()` handle and a handle
89
+ * already passed to a guest (the A15 transfer guard) both throw, as does a
90
+ * non-`u8` element type.
91
+ */
92
+ readDirect(consume: (src: DirectSource) => DirectVerdict): Promise<number>;
69
93
  /**
70
94
  * Cancel an in-flight `read` (R-fix review advisory 1).
71
95
  *
@@ -103,7 +127,7 @@ export declare class Stream<T> {
103
127
  [Symbol.asyncIterator](): AsyncIterator<Chunk<T>>;
104
128
  }
105
129
  /** Writer half of `Stream.create()`. */
106
- export declare class StreamWriter<T> {
130
+ export declare class StreamWriter<T> implements ProtocolStreamWriter<T> {
107
131
  #private;
108
132
  constructor(stream: Stream<T>);
109
133
  /**
@@ -117,6 +141,29 @@ export declare class StreamWriter<T> {
117
141
  * window is misuse. Plain-array chunks are lowered (copied) up front.
118
142
  */
119
143
  write(values: Chunk<T>): Promise<number>;
144
+ /**
145
+ * Fill the reader's landing zone in place, without an intermediate chunk
146
+ * (`stream<u8>` only — contracts/embedder-api.md amendment A21,
147
+ * polyengine#128).
148
+ *
149
+ * At every rendezvous with a reader of nonzero capacity, `produce` runs
150
+ * exactly once, synchronously, with a `DirectDestination` over the reader's
151
+ * unfilled landing zone — guest linear memory when the peer is a guest, so
152
+ * the producer's own `set()` IS the canonical-ABI copy and an external byte
153
+ * mover (a websocket frame, a SAB ring segment, a transferred
154
+ * `ArrayBuffer`) never pays a second copy inside the runtime. `"more"`
155
+ * keeps the session parked for the next rendezvous; `"done"` ends it.
156
+ * Resolves with the session's total byte count.
157
+ *
158
+ * `"done"` with zero bytes marked *retracts* (the session ends, the
159
+ * reader's operation stays parked, no event — the speculative-park
160
+ * correction); `"more"` with zero marked, and a throwing callback, reject.
161
+ *
162
+ * Parks until the element type is known, exactly as `write` does — a
163
+ * `Stream.create()` writer has no element type until the lowering site
164
+ * binds one — and then requires `u8`.
165
+ */
166
+ writeDirect(produce: (dest: DirectDestination) => DirectVerdict): Promise<number>;
120
167
  /** Offer values until all are taken or the reader goes away. */
121
168
  writeAll(values: Chunk<T>): Promise<number>;
122
169
  cancelWrite(): void;
@@ -131,7 +178,7 @@ export declare function publishHostStream<T>(s: Stream<T>, h: HostStream<T>): vo
131
178
  * A future whose write end dropped without ever writing rejects with
132
179
  * `DroppedError` — not `undefined`, which `future<void>` legitimately yields.
133
180
  */
134
- export declare class Future<T> implements PromiseLike<T> {
181
+ export declare class Future<T> implements ProtocolFuture<T> {
135
182
  #private;
136
183
  private constructor();
137
184
  static fromLifted<T>(value: ComponentValue, codec: ElemCodec<T>): Future<T>;
@@ -182,7 +229,7 @@ export declare class Future<T> implements PromiseLike<T> {
182
229
  * The internal value is `task/streams.ts`'s `ErrorContext` (debug message
183
230
  * only, per definitions.py).
184
231
  */
185
- export declare class ErrorContext {
232
+ export declare class ErrorContext implements ProtocolErrorContext {
186
233
  readonly message: string;
187
234
  /** @internal — the internal value, preserved so it can be lowered back. */
188
235
  readonly internal: InternalErrorContext;
@@ -336,6 +336,19 @@ export declare function createLoweredImport(input: {
336
336
  mode: SuspensionMode;
337
337
  /** Host fn carries the `suspending()` brand (embedder-api.md A1). */
338
338
  suspendable: boolean;
339
+ /**
340
+ * Host fn carries the `deferCancel()` brand (embedder-api.md A23): the
341
+ * import must run to completion, so a cancellation is accepted and ignored
342
+ * instead of taking the default discard.
343
+ */
344
+ deferCancel: boolean;
345
+ /**
346
+ * Host fn carries the `abortable()` brand (embedder-api.md A24): every call
347
+ * receives a fresh `AbortSignal` appended after the WIT-declared params, and
348
+ * the runtime aborts it when — and only when — the call is discarded by a
349
+ * guest cancellation.
350
+ */
351
+ abortable: boolean;
339
352
  }): CoreFn;
340
353
  /**
341
354
  * The callback-ABI dispatch loop of `canon_lift` (definitions.py lines
@@ -37,6 +37,49 @@ export declare class HostBuffer {
37
37
  * writer used.
38
38
  */
39
39
  taken(): PayloadChunk;
40
+ byteView(n: number): Uint8Array;
41
+ advanceBytes(k: number): void;
42
+ endWindow(): void;
43
+ }
44
+ /** The scoped landing zone handed to a `writeDirect` producer (A21, #128). */
45
+ export interface DirectDestination {
46
+ /**
47
+ * The reader's still-unfilled bytes. Re-derived on every call (a
48
+ * `memory.grow` between two rendezvous of one session never yields a stale
49
+ * view) and shrinking by whatever has been marked so far in THIS
50
+ * invocation. DEAD once the callback returns.
51
+ */
52
+ remaining(): Uint8Array;
53
+ /**
54
+ * Acknowledge bytes written into the view. Cumulative within the
55
+ * invocation; acknowledged only if the callback then returns cleanly.
56
+ */
57
+ markWritten(n: number): void;
58
+ }
59
+ /** The scoped view handed to a `readDirect` consumer (A21, #128). */
60
+ export interface DirectSource {
61
+ /**
62
+ * The writer's unread bytes; read-only by contract. Same scoping and
63
+ * re-derivation rules as `DirectDestination.remaining`.
64
+ */
65
+ remaining(): Uint8Array;
66
+ /** Acknowledge bytes consumed from the view. See `markWritten`. */
67
+ markRead(n: number): void;
68
+ }
69
+ /** The callback's poll cadence, spelled event-style (A21). */
70
+ export type DirectVerdict = "more" | "done";
71
+ /**
72
+ * Out-parameter of the low-level direct forms: `true` iff the session ended
73
+ * because the callback itself returned `"done"`, rather than because the peer
74
+ * dropped / the operation was cancelled / the peer's instance trapped.
75
+ *
76
+ * The conventions layer needs the distinction for A7 precision — a session
77
+ * the producer already completed keeps its resolution even if the peer then
78
+ * trapped — and `Promise<number>` is the contract's return shape, so it rides
79
+ * here rather than in the resolved value.
80
+ */
81
+ export interface DirectSessionInfo {
82
+ endedByVerdict: boolean;
40
83
  }
41
84
  /** Host end the embedder WRITES; the guest reads. */
42
85
  export interface HostWritableEnd<T> {
@@ -65,6 +108,24 @@ export interface HostWritableEnd<T> {
65
108
  * if the reader dropped.
66
109
  */
67
110
  writeAll(values: T[]): Promise<number>;
111
+ /**
112
+ * Park a **direct session** on this end (`stream<u8>` only — embedder-api
113
+ * amendment A21, polyengine#128).
114
+ *
115
+ * At every rendezvous with a reader of nonzero capacity, `produce` runs
116
+ * exactly once, synchronously, inside the rendezvous, with a
117
+ * `DirectDestination` over the reader's unfilled landing zone — guest linear
118
+ * memory when the peer is a guest, so the producer's own `set()` is the
119
+ * canonical-ABI copy. `"more"` keeps the session parked for the next
120
+ * rendezvous; `"done"` ends it. Resolves with the session's total.
121
+ *
122
+ * Marks acknowledge on clean return only. `"done"` with zero marked is
123
+ * *retraction* (the session ends, the reader's operation stays parked, no
124
+ * event); `"more"` with zero marked, and a throwing callback, reject.
125
+ *
126
+ * Participates in the one-in-flight-per-end rule exactly as `write` does.
127
+ */
128
+ writeDirect(produce: (dest: DirectDestination) => DirectVerdict, info?: DirectSessionInfo): Promise<number>;
68
129
  /**
69
130
  * Cancel an in-flight `write`/`writeAll` (definitions.py
70
131
  * `SharedStreamImpl.cancel` -> `CopyResult.CANCELLED`). No-op when nothing
@@ -92,6 +153,15 @@ export interface HostReadableEnd<T> {
92
153
  * array.
93
154
  */
94
155
  read(max: number): Promise<T[]>;
156
+ /**
157
+ * Park a **direct session** on this end (`stream<u8>` only — embedder-api
158
+ * amendment A21, polyengine#128). The mirror of
159
+ * `HostWritableEnd.writeDirect`: `consume` receives a `DirectSource` over
160
+ * the writer's unread bytes (a view of guest memory, or of the offered
161
+ * host chunk itself) and may take a prefix — a partial take is normal, and
162
+ * the writer re-offers on its own schedule.
163
+ */
164
+ readDirect(consume: (src: DirectSource) => DirectVerdict, info?: DirectSessionInfo): Promise<number>;
95
165
  /** Cancel an in-flight `read`; see `HostWritableEnd.cancelWrite`. */
96
166
  cancelRead(): void;
97
167
  drop(): void;
@@ -1 +1 @@
1
- export { anySuspendingImport, isSuspending, suspending } from "@polyengine/protocol";
1
+ export { abortable, anySuspendingImport, deferCancel, isAbortable, isDeferCancel, isSuspending, suspending, } from "@polyengine/protocol";
@@ -51,7 +51,90 @@ export declare class GuestBuffer {
51
51
  read(n: number): PayloadChunk;
52
52
  /** definitions.py `WritableBufferGuestImpl.write`. */
53
53
  write(vs: PayloadChunk): void;
54
+ /**
55
+ * A fresh view over the next `n` bytes of this buffer's remaining range.
56
+ *
57
+ * Fresh on every call, via `bytesOf` (cabi/memory.ts:195) over the
58
+ * `LiveMemory` getters — so a `memory.grow` between two rendezvous of one
59
+ * parked direct session never yields a view onto the detached buffer.
60
+ */
61
+ byteView(n: number): Uint8Array;
62
+ /**
63
+ * Advance by `k` WITHOUT copying: the bytes already moved through the view
64
+ * `byteView` handed out. Called by the seam only after the direct callback
65
+ * returned cleanly, which is what makes marks acknowledge-on-clean-return.
66
+ */
67
+ advanceBytes(k: number): void;
68
+ }
69
+ /**
70
+ * The buffer surface the rendezvous actually uses (definitions.py `Buffer`,
71
+ * line 918). `GuestBuffer` and the host layer's `HostBuffer` both satisfy it.
72
+ */
73
+ export interface RendezvousBuffer {
74
+ remain(): number;
75
+ isZeroLength(): boolean;
76
+ read(n: number): PayloadChunk;
77
+ write(vs: PayloadChunk): void;
78
+ }
79
+ /**
80
+ * A21: the peer half of a direct rendezvous — a buffer that can expose its
81
+ * remaining range as bytes and be advanced without a copy.
82
+ *
83
+ * Implemented by `GuestBuffer` (a view into guest linear memory: the
84
+ * embedder's own `set()` becomes the one ABI copy) and by `HostBuffer` (a
85
+ * view of the offered chunk when it is the source; a synthesized scratch that
86
+ * becomes the delivered chunk when it is the destination).
87
+ */
88
+ export interface ByteWindow {
89
+ /**
90
+ * A view over the next `n` bytes. May be called several times within one
91
+ * direct invocation (`remaining()` re-derives on every call); an
92
+ * implementation that *synthesizes* the window must return the same
93
+ * storage for the whole invocation and release it in `endWindow`.
94
+ */
95
+ byteView(n: number): Uint8Array;
96
+ /** Record `k` bytes as moved. Called only after a clean callback return. */
97
+ advanceBytes(k: number): void;
98
+ /** End of one direct invocation; drop any synthesized window. */
99
+ endWindow?(): void;
100
+ }
101
+ /**
102
+ * A21: the parked direct session, as the rendezvous sees it. It presents the
103
+ * ordinary buffer surface (so `remain()`/`isZeroLength()` keep the reference
104
+ * control flow working) but its `read`/`write` are never called — the seam
105
+ * routes it through `runDirect` instead.
106
+ */
107
+ export interface DirectBuffer extends RendezvousBuffer {
108
+ readonly direct: true;
109
+ /**
110
+ * Run this session's callback exactly once against the peer's window,
111
+ * with `n` bytes of capacity. Applies the acknowledged marks to `peer`
112
+ * itself, and settles the session on failure — the seam only routes the
113
+ * rendezvous state that follows.
114
+ */
115
+ runDirect(peer: ByteWindow, n: number): DirectOutcome;
116
+ /**
117
+ * Reject this session out-of-band (the two-direct-sessions rendezvous,
118
+ * where neither side owns memory).
119
+ */
120
+ failDirect(error: Error): void;
54
121
  }
122
+ /**
123
+ * What the seam did, and hence how the rendezvous must continue.
124
+ *
125
+ * * `"chunk"` — no direct session was involved: the reference copy ran.
126
+ * * `"copied"` — the callback acknowledged ≥ 1 byte; continue exactly as
127
+ * after a reference copy (fire the pending side's `on_copy`).
128
+ * * `"retracted"` — `"done"` with zero marked. Continue as if the direct
129
+ * side's buffer had had `remain() == 0` all along, which is a state
130
+ * definitions.py already routes.
131
+ * * `"failed"` — misuse or a throwing callback; the session has already
132
+ * rejected. No copy, no event, the peer's parked operation survives.
133
+ * * `"both-direct"` — neither side owns memory; the ARRIVING side is
134
+ * rejected by the caller and the parked side is left undisturbed.
135
+ */
136
+ export type DirectOutcome = "copied" | "retracted" | "failed";
137
+ export type RendezvousOutcome = DirectOutcome | "chunk" | "both-direct";
55
138
  /** Common shape of the object a `stream`/`future` *value* refers to. */
56
139
  export interface SharedBase {
57
140
  readonly t: ValType | null;