@rindle/api-server 0.8.0 → 0.10.0

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/dist/streams.d.ts CHANGED
@@ -138,6 +138,38 @@ export interface AuthorizeStreamInput<User> {
138
138
  meta: unknown;
139
139
  request?: unknown;
140
140
  }
141
+ /**
142
+ * Optional cross-process transport for the LIVE plane
143
+ * (designs-implemented/LM-STREAM-RELAY-DESIGN.md). Both methods are independently optional; which
144
+ * ones you implement is which topology you built — an addressing adapter (a Durable Object named by
145
+ * `streamId`, `fly-replay`) implements only `attach`; a broadcast adapter (Redis pub/sub, NATS)
146
+ * mirrors with `publish` and subscribes with `attach`; a log adapter (Redis Streams, Kafka) appends
147
+ * and replays. Never consulted for the durable plane — checkpoints are unaffected by any of this.
148
+ *
149
+ * The plane does not trust what `attach` yields: frames are run through the conform pass
150
+ * ({@link StreamRelayConform}) and any contract violation downgrades the subscription to `stale`,
151
+ * which already means "you are on the durable plane now". A broken relay costs a reader smooth
152
+ * tokens, never corrupted text — and can never reach the producer.
153
+ */
154
+ export interface StreamRelay {
155
+ /** Producer side: every frame this process's producer fans out (`chunk`, `durable`, and the
156
+ * terminal `end`), mirrored outward. MUST NOT block; a throw or rejected promise is caught and
157
+ * routed to {@link RindleStreamOptions.onRelayError} — a relay outage may cost the live leg,
158
+ * never the generation. Returned promises are observed but never awaited. */
159
+ publish?(streamId: string, frame: StreamFrame): void | PromiseLike<void>;
160
+ /** Subscriber side: this process is not hosting `streamId`. Return a frame source, or `undefined`
161
+ * for `absent` — exactly the no-relay answer. Consulted only AFTER `authorize` has passed, and
162
+ * only on a live-plane miss (a local stream always wins). The plane closes the source
163
+ * (`return()`) when the reader disconnects. An adapter that cannot serve `from` (pub/sub has no
164
+ * history) yields `stale` and stops — the reader converges on the durable plane (§5). */
165
+ attach?(streamId: string, from: number): Promise<AsyncIterable<StreamFrame> | undefined>;
166
+ }
167
+ export interface StreamRelayErrorInfo {
168
+ streamId: string;
169
+ /** Where it failed: mirroring a frame out (`publish`), dialing the adapter (`attach`), or
170
+ * consuming/conforming its frames (`frames`). */
171
+ phase: "publish" | "attach" | "frames";
172
+ }
141
173
  export interface RindleStreamOptions<User> {
142
174
  /** Where checkpoints land: the app's tables (the default path) or a raw `commit` callback. */
143
175
  checkpoint: StreamCheckpointTarget;
@@ -160,7 +192,9 @@ export interface RindleStreamOptions<User> {
160
192
  retainChars?: number;
161
193
  /** How long a sealed stream stays joinable before eviction. Default 30s. */
162
194
  lingerMs?: number;
163
- /** Per-subscriber frame queue cap; overflow drops that subscriber with `stale` (§4). Default 1024. */
195
+ /** Per-subscriber frame queue cap; overflow drops that subscriber with `stale` (§4). Default 1024.
196
+ * Relayed readers reuse the same bound: a slow reader on a relayed stream costs itself the live
197
+ * leg exactly as a local one does. */
164
198
  maxQueuedFrames?: number;
165
199
  /** A checkpoint that exhausted its retries. The stream keeps streaming — this is a durability
166
200
  * stall, not a stream stall — so the error must not vanish. Absent ⇒ `console.error`. */
@@ -169,6 +203,19 @@ export interface RindleStreamOptions<User> {
169
203
  from: number;
170
204
  seq: number;
171
205
  }) => void;
206
+ /** Cross-process transport for the live plane ({@link StreamRelay}). Without one, a subscriber
207
+ * that lands on a process not hosting its stream gets `absent` and reads the durable plane at
208
+ * checkpoint granularity — correct, just chunky. */
209
+ relay?: StreamRelay;
210
+ /** Bound on `relay.attach`: a hung adapter yields `absent`, not a hung HTTP request. An
211
+ * addressing adapter MAY deliberately spend this window waiting out a subscribe that races its
212
+ * own kick. Default 2000. */
213
+ relayAttachTimeoutMs?: number;
214
+ /** A diagnostic, never a control path: relay failures (a throwing or rejecting `publish`, a failed
215
+ * or timed-out `attach`, a conform violation in the frames) land here, wrapped so a throwing hook
216
+ * cannot reach the plane. The reader-facing outcome is always the same legal `absent`/`stale`.
217
+ * Absent ⇒ `console.error`. */
218
+ onRelayError?: (err: unknown, info: StreamRelayErrorInfo) => void;
172
219
  }
173
220
  export interface OpenStreamInput<User> {
174
221
  user: User;
@@ -245,6 +292,49 @@ export declare function streamChunkId(streamId: string, seq: number): string;
245
292
  * generated rather than hand-written.
246
293
  */
247
294
  export declare function streamChunkTableDdl(tables: StreamTables, dialect: SqlDialect): string[];
295
+ /**
296
+ * One frame source arriving over a relay, conformed to the CP §4 contract
297
+ * (designs-implemented/LM-STREAM-RELAY-DESIGN.md §4).
298
+ *
299
+ * An adapter is app code talking to Redis or a socket, and its frames feed `spliceStreamText` on a
300
+ * browser — so the plane does not trust them. This pass enforces the frame invariants against the
301
+ * prefix actually delivered and downgrades EVERY violation to a legal `stale` and nothing else:
302
+ * `stale` already means "you are on the durable plane now, the store is the whole truth", so a
303
+ * broken relay costs a reader smooth tokens, never corrupted text — and cannot wedge a producer.
304
+ *
305
+ * Replayed spans (a reconnecting adapter re-delivering what it already sent) are ABSORBED rather
306
+ * than punished — deduping against the delivered prefix is what makes reconnect-replay safe without
307
+ * every adapter hand-rolling it. Spans that overlap the prefix but extend past it pass through
308
+ * whole: the client splices at the frame's own offset, so an exact overlap re-covers and appends.
309
+ *
310
+ * Pure state, no I/O, no plane: `feed` maps one incoming frame to 0-2 outgoing frames (a missing
311
+ * `open` is synthesized at the join offset); `end`/`fail` close out a source that finished or threw
312
+ * without a terminal. After a terminal, every method returns `[]`.
313
+ */
314
+ export declare class StreamRelayConform {
315
+ private readonly streamId;
316
+ /** The requested join offset — the synthesized `open`'s position, and where the prefix starts. */
317
+ private readonly from;
318
+ private readonly onViolation;
319
+ /** End of the delivered prefix. */
320
+ private pos;
321
+ private lastDurable;
322
+ private opened;
323
+ private done;
324
+ constructor(streamId: string, from: number, onViolation?: (reason: string) => void);
325
+ feed(frame: StreamFrame): StreamFrame[];
326
+ /** The source completed without a terminal (a truncated relay): the reader falls back. */
327
+ end(): StreamFrame[];
328
+ /** The source threw mid-iteration, or the plane is dropping a reader that stopped draining:
329
+ * a bare `stale` at the delivered position. */
330
+ fail(): StreamFrame[];
331
+ /** A synthesized join, for an adapter that (correctly, in broadcast mode) never mirrors the
332
+ * per-subscriber `open`: positioned at the requested offset, which the reader asked from because
333
+ * its durable view already holds it. */
334
+ private synthOpen;
335
+ private terminate;
336
+ private violate;
337
+ }
248
338
  /** What the plane needs from the api-server to write a checkpoint: the backend's OUTSIDE-transaction
249
339
  * SQL surface (`batch` is one transaction on every backend) and its dialect. Deliberately narrow so
250
340
  * `streams.ts` never imports the server (no cycle). */
@@ -259,7 +349,10 @@ export declare class StreamPlane<User> {
259
349
  readonly retainChars: number;
260
350
  readonly lingerMs: number;
261
351
  readonly maxQueuedFrames: number;
352
+ readonly relayAttachTimeoutMs: number;
262
353
  private readonly live;
354
+ /** Live relayed subscriptions, so teardown ({@link closeSync}) releases their drivers too. */
355
+ private readonly relayed;
263
356
  private readonly opts;
264
357
  private readonly sink;
265
358
  private readonly mapped;
@@ -290,6 +383,24 @@ export declare class StreamPlane<User> {
290
383
  */
291
384
  private assertOpenable;
292
385
  subscribe(input: SubscribeStreamInput<User>): Promise<StreamSubscription>;
386
+ /** The subscribe-miss leg (LM-STREAM-RELAY §3): ask the app's relay for the frames of a stream
387
+ * this process is not hosting. `undefined` — no relay, no `attach`, the adapter declined, timed
388
+ * out, or threw — is `absent`, exactly today's answer. */
389
+ private attachRelay;
390
+ /** `attach`, bounded by {@link RindleStreamOptions.relayAttachTimeoutMs}: a hung adapter yields
391
+ * `absent`, not a hung HTTP request. A source that resolves after the deadline is closed, not
392
+ * leaked. */
393
+ private boundedAttach;
394
+ /** Wrap an adapter's frame source as a plane subscription: conform every frame (LM-STREAM-RELAY
395
+ * §4), bound the reader with the same queue cap as a local one (§7), and tear the adapter down
396
+ * when either side lets go. The driver never throws into the plane: adapter failures become one
397
+ * `stale`. */
398
+ private relaySubscription;
399
+ /** Mirror one producer frame outward (LM-STREAM-RELAY §3). Never blocks or breaks the producer:
400
+ * a throw or rejected promise is reported and swallowed — a relay outage may cost relayed
401
+ * readers the live leg, never the generation or its checkpoints. */
402
+ publishRelay(streamId: string, frame: StreamFrame): void;
403
+ reportRelayError(err: unknown, info: StreamRelayErrorInfo): void;
293
404
  /** Seal every live stream `interrupted`. In mapped-table mode the seal IS the compaction, so a
294
405
  * graceful drain loses nothing PRODUCED; the status still says the response was cut short rather
295
406
  * than claiming completion (§5). Wire it to SIGTERM. */
@@ -1 +1 @@
1
- {"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":"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"}
package/dist/streams.js CHANGED
@@ -1,4 +1,4 @@
1
- // LM stream checkpointing — the two-plane response path (designs/LM-STREAM-CHECKPOINT-DESIGN.md).
1
+ // LM stream checkpointing — the two-plane response path (designs-implemented/LM-STREAM-CHECKPOINT-DESIGN.md).
2
2
  //
3
3
  // A model response arrives as hundreds of tiny deltas per second. Every one wants to be on a screen
4
4
  // immediately; none wants to be a durable write. So the response runs on TWO planes sharing ONE
@@ -49,6 +49,7 @@ const DEFAULT_CHECKPOINT_RETRIES = 3;
49
49
  const DEFAULT_RETAIN_CHARS = 64 * 1024;
50
50
  const DEFAULT_LINGER_MS = 30_000;
51
51
  const DEFAULT_MAX_QUEUED_FRAMES = 1024;
52
+ const DEFAULT_RELAY_ATTACH_TIMEOUT_MS = 2000;
52
53
  /** Retry backoff base — 50ms, 100ms, 200ms, … A checkpoint failure is usually a blip at the write
53
54
  * authority; the text is safe in the buffer meanwhile, so there is nothing to rush. */
54
55
  const RETRY_BACKOFF_MS = 50;
@@ -303,6 +304,148 @@ class Subscriber {
303
304
  };
304
305
  }
305
306
  }
307
+ // ------------------------------------------------------------------------------- the relay conform pass
308
+ /**
309
+ * One frame source arriving over a relay, conformed to the CP §4 contract
310
+ * (designs-implemented/LM-STREAM-RELAY-DESIGN.md §4).
311
+ *
312
+ * An adapter is app code talking to Redis or a socket, and its frames feed `spliceStreamText` on a
313
+ * browser — so the plane does not trust them. This pass enforces the frame invariants against the
314
+ * prefix actually delivered and downgrades EVERY violation to a legal `stale` and nothing else:
315
+ * `stale` already means "you are on the durable plane now, the store is the whole truth", so a
316
+ * broken relay costs a reader smooth tokens, never corrupted text — and cannot wedge a producer.
317
+ *
318
+ * Replayed spans (a reconnecting adapter re-delivering what it already sent) are ABSORBED rather
319
+ * than punished — deduping against the delivered prefix is what makes reconnect-replay safe without
320
+ * every adapter hand-rolling it. Spans that overlap the prefix but extend past it pass through
321
+ * whole: the client splices at the frame's own offset, so an exact overlap re-covers and appends.
322
+ *
323
+ * Pure state, no I/O, no plane: `feed` maps one incoming frame to 0-2 outgoing frames (a missing
324
+ * `open` is synthesized at the join offset); `end`/`fail` close out a source that finished or threw
325
+ * without a terminal. After a terminal, every method returns `[]`.
326
+ */
327
+ export class StreamRelayConform {
328
+ streamId;
329
+ /** The requested join offset — the synthesized `open`'s position, and where the prefix starts. */
330
+ from;
331
+ onViolation;
332
+ /** End of the delivered prefix. */
333
+ pos;
334
+ lastDurable = 0;
335
+ opened = false;
336
+ done = false;
337
+ constructor(streamId, from, onViolation) {
338
+ this.streamId = streamId;
339
+ this.from = from;
340
+ this.pos = from;
341
+ this.onViolation = onViolation;
342
+ }
343
+ feed(frame) {
344
+ if (this.done)
345
+ return [];
346
+ switch (frame.type) {
347
+ case "open": {
348
+ if (this.opened)
349
+ return []; // a reconnecting adapter's second open: absorbed
350
+ if (frame.streamId !== this.streamId ||
351
+ !Number.isInteger(frame.from) ||
352
+ !Number.isInteger(frame.seq) ||
353
+ !Number.isInteger(frame.durableSeq) ||
354
+ frame.from < 0 ||
355
+ frame.seq < frame.from) {
356
+ return this.violate(`relay open for ${JSON.stringify(frame.streamId)} at ${frame.from} is malformed`);
357
+ }
358
+ // An open PAST the requested offset is the adapter saying it cannot serve `from` (pub/sub
359
+ // has no history, §5): delivering it would leave a hole in the middle of the response, so
360
+ // the honest answer is the durable plane.
361
+ if (frame.from > this.from) {
362
+ return this.violate(`relay open at ${frame.from} cannot serve the requested ${this.from}`);
363
+ }
364
+ this.opened = true;
365
+ this.pos = frame.from;
366
+ return [frame];
367
+ }
368
+ case "chunk": {
369
+ if (!Number.isInteger(frame.from) ||
370
+ !Number.isInteger(frame.seq) ||
371
+ frame.from < 0 ||
372
+ typeof frame.text !== "string" ||
373
+ frame.text.length !== frame.seq - frame.from) {
374
+ return this.violate(`relay chunk ${frame.from}→${frame.seq} does not span exactly its offsets`);
375
+ }
376
+ if (frame.from > this.pos) {
377
+ return this.violate(`relay chunk at ${frame.from} leaves a gap after ${this.pos}`);
378
+ }
379
+ if (frame.seq <= this.pos)
380
+ return []; // entirely within the delivered prefix (a replay): absorbed
381
+ const out = this.opened ? [] : [this.synthOpen()];
382
+ this.pos = frame.seq;
383
+ out.push(frame);
384
+ return out;
385
+ }
386
+ case "durable": {
387
+ // Purely informational to a reader (the hook ignores it; SSE uses it as a resume id), so a
388
+ // claim that rewinds — or outruns what this subscription has SEEN produced, which P forbids
389
+ // — is dropped rather than downgraded.
390
+ if (!Number.isInteger(frame.seq) || frame.seq < this.lastDurable || frame.seq > this.pos)
391
+ return [];
392
+ const out = this.opened ? [] : [this.synthOpen()];
393
+ this.lastDurable = frame.seq;
394
+ out.push(frame);
395
+ return out;
396
+ }
397
+ case "end": {
398
+ if (!Number.isInteger(frame.seq) || frame.seq < 0) {
399
+ return this.violate(`relay end at ${String(frame.seq)} is malformed`);
400
+ }
401
+ // An `end` whose durable length outruns the delivered prefix means the adapter LOST text
402
+ // (durable never exceeds produced), so the reader is short and must not be told it saw
403
+ // everything. Downgrading keeps `end` meaning the same thing relayed as local: the whole
404
+ // produced text arrived.
405
+ if (frame.seq > this.pos) {
406
+ return this.violate(`relay end at ${frame.seq} outruns the ${this.pos} characters delivered`);
407
+ }
408
+ this.done = true;
409
+ const out = this.opened ? [] : [this.synthOpen()];
410
+ out.push(frame);
411
+ return out;
412
+ }
413
+ case "stale":
414
+ case "absent": {
415
+ // Legal bare — the local plane's own floor/eviction answers carry no `open` either.
416
+ this.done = true;
417
+ return [frame];
418
+ }
419
+ }
420
+ }
421
+ /** The source completed without a terminal (a truncated relay): the reader falls back. */
422
+ end() {
423
+ if (this.done)
424
+ return [];
425
+ this.onViolation?.("relay source ended without a terminal frame");
426
+ return this.terminate();
427
+ }
428
+ /** The source threw mid-iteration, or the plane is dropping a reader that stopped draining:
429
+ * a bare `stale` at the delivered position. */
430
+ fail() {
431
+ return this.done ? [] : this.terminate();
432
+ }
433
+ /** A synthesized join, for an adapter that (correctly, in broadcast mode) never mirrors the
434
+ * per-subscriber `open`: positioned at the requested offset, which the reader asked from because
435
+ * its durable view already holds it. */
436
+ synthOpen() {
437
+ this.opened = true;
438
+ return { type: "open", streamId: this.streamId, from: this.from, seq: this.from, durableSeq: this.from, ended: false };
439
+ }
440
+ terminate() {
441
+ this.done = true;
442
+ return [{ type: "stale", floorSeq: this.pos, durableSeq: this.lastDurable }];
443
+ }
444
+ violate(reason) {
445
+ this.onViolation?.(reason);
446
+ return this.terminate();
447
+ }
448
+ }
306
449
  class LiveStream {
307
450
  /** Retained text; `buf[0]` sits at offset {@link bufFrom}. */
308
451
  buf = "";
@@ -541,6 +684,8 @@ class LiveStream {
541
684
  status: seal.status,
542
685
  ...(seal.error !== undefined ? { error: seal.error } : {}),
543
686
  };
687
+ // The terminal reaches the relay too (it bypasses `fanout` locally only to bypass the cap).
688
+ this.plane.publishRelay(this.streamId, frame);
544
689
  for (const sub of [...this.subs])
545
690
  sub.finish(frame);
546
691
  this.plane.retire(this.streamId);
@@ -587,6 +732,9 @@ class LiveStream {
587
732
  }
588
733
  // ---- subscriber side
589
734
  fanout(frame) {
735
+ // Every frame local subscribers get, the relay gets — including when nobody local is attached
736
+ // (a broadcast relay's whole point). Wrapped so an outage costs the live leg, never this stream.
737
+ this.plane.publishRelay(this.streamId, frame);
590
738
  for (const sub of [...this.subs]) {
591
739
  if (!sub.offer(frame)) {
592
740
  // Bounded, then dropped: a reader that stopped draining costs itself a rejoin, never the
@@ -644,7 +792,10 @@ export class StreamPlane {
644
792
  retainChars;
645
793
  lingerMs;
646
794
  maxQueuedFrames;
795
+ relayAttachTimeoutMs;
647
796
  live = new Map();
797
+ /** Live relayed subscriptions, so teardown ({@link closeSync}) releases their drivers too. */
798
+ relayed = new Set();
648
799
  opts;
649
800
  sink;
650
801
  mapped;
@@ -660,7 +811,15 @@ export class StreamPlane {
660
811
  this.retries = opts.policy?.retries ?? DEFAULT_CHECKPOINT_RETRIES;
661
812
  this.lingerMs = opts.lingerMs ?? DEFAULT_LINGER_MS;
662
813
  this.maxQueuedFrames = opts.maxQueuedFrames ?? DEFAULT_MAX_QUEUED_FRAMES;
814
+ this.relayAttachTimeoutMs = opts.relayAttachTimeoutMs ?? DEFAULT_RELAY_ATTACH_TIMEOUT_MS;
663
815
  this.openToken = opts.hostId ?? randomOpenToken();
816
+ // Relay misconfiguration is refused loudly (the room-profile rule), never ignored.
817
+ if (opts.relay !== undefined && opts.relay.publish === undefined && opts.relay.attach === undefined) {
818
+ throw new TypeError("streams.relay implements neither publish nor attach — which topology is this? (LM-STREAM-RELAY §3.1)");
819
+ }
820
+ if (opts.relay === undefined && (opts.relayAttachTimeoutMs !== undefined || opts.onRelayError !== undefined)) {
821
+ throw new TypeError("streams.relayAttachTimeoutMs/onRelayError do nothing without streams.relay");
822
+ }
664
823
  if ("tables" in opts.checkpoint) {
665
824
  if (!sink)
666
825
  throw new TypeError("streams.checkpoint.tables needs a SQL-capable mutation backend");
@@ -787,10 +946,162 @@ export class StreamPlane {
787
946
  });
788
947
  if (verdict === false)
789
948
  throw new StreamForbidden(input.streamId);
790
- if (!stream)
791
- return oneFrame(input.streamId, { type: "absent" });
949
+ if (!stream) {
950
+ // The relay is consulted only on a live-plane miss, and only after `authorize` passed — a
951
+ // denial must not become an existence probe against the relay either. A local stream always
952
+ // wins: the producer's own readers keep the lowest-latency path.
953
+ const relayed = await this.attachRelay(input.streamId, from);
954
+ return relayed ?? oneFrame(input.streamId, { type: "absent" });
955
+ }
792
956
  return stream.subscribe(from);
793
957
  }
958
+ /** The subscribe-miss leg (LM-STREAM-RELAY §3): ask the app's relay for the frames of a stream
959
+ * this process is not hosting. `undefined` — no relay, no `attach`, the adapter declined, timed
960
+ * out, or threw — is `absent`, exactly today's answer. */
961
+ async attachRelay(streamId, from) {
962
+ const relay = this.opts.relay;
963
+ if (relay?.attach === undefined)
964
+ return undefined;
965
+ // Floored like the local join, but clamped only below: the producer's length is not known here.
966
+ const at = Number.isFinite(from) ? Math.max(Math.floor(from), 0) : 0;
967
+ let source;
968
+ try {
969
+ source = await this.boundedAttach(relay, streamId, at);
970
+ }
971
+ catch (err) {
972
+ this.reportRelayError(err, { streamId, phase: "attach" });
973
+ return undefined;
974
+ }
975
+ if (source === undefined)
976
+ return undefined;
977
+ return this.relaySubscription(streamId, at, source);
978
+ }
979
+ /** `attach`, bounded by {@link RindleStreamOptions.relayAttachTimeoutMs}: a hung adapter yields
980
+ * `absent`, not a hung HTTP request. A source that resolves after the deadline is closed, not
981
+ * leaked. */
982
+ boundedAttach(relay, streamId, from) {
983
+ // `async` wrapping so a synchronously-throwing adapter is an attach failure, not a plane throw.
984
+ const attempt = (async () => relay.attach(streamId, from))();
985
+ return new Promise((resolve, reject) => {
986
+ let late = false;
987
+ const t = timer(() => {
988
+ late = true;
989
+ reject(new Error(`stream ${streamId}: relay.attach timed out after ${this.relayAttachTimeoutMs}ms`));
990
+ }, this.relayAttachTimeoutMs);
991
+ attempt.then((source) => {
992
+ clearTimeout(t);
993
+ if (!late)
994
+ return resolve(source);
995
+ closeFrameSource(source); // too late to serve the reader; don't leak the channel
996
+ }, (err) => {
997
+ clearTimeout(t);
998
+ if (!late)
999
+ reject(err);
1000
+ });
1001
+ });
1002
+ }
1003
+ /** Wrap an adapter's frame source as a plane subscription: conform every frame (LM-STREAM-RELAY
1004
+ * §4), bound the reader with the same queue cap as a local one (§7), and tear the adapter down
1005
+ * when either side lets go. The driver never throws into the plane: adapter failures become one
1006
+ * `stale`. */
1007
+ relaySubscription(streamId, from, source) {
1008
+ const conform = new StreamRelayConform(streamId, from, (reason) => this.reportRelayError(new Error(reason), { streamId, phase: "frames" }));
1009
+ let closed = false;
1010
+ let signalClose;
1011
+ const closedP = new Promise((resolve) => (signalClose = resolve));
1012
+ const closedTag = closedP.then(() => "closed");
1013
+ const release = () => {
1014
+ if (!closed) {
1015
+ closed = true;
1016
+ signalClose();
1017
+ }
1018
+ };
1019
+ const sub = new Subscriber(this.maxQueuedFrames, (s) => {
1020
+ this.relayed.delete(s);
1021
+ release();
1022
+ });
1023
+ this.relayed.add(sub);
1024
+ let it;
1025
+ /** @returns false once the subscription finished (terminal delivered, or the reader dropped). */
1026
+ const deliver = (frames) => {
1027
+ for (const frame of frames) {
1028
+ if (frame.type === "end" || frame.type === "stale" || frame.type === "absent") {
1029
+ sub.finish(frame);
1030
+ return false;
1031
+ }
1032
+ if (!sub.offer(frame)) {
1033
+ // The same bound as a local reader: a relayed subscriber that stops draining costs
1034
+ // itself the live leg, never unbounded memory.
1035
+ const [stale] = conform.fail();
1036
+ if (stale)
1037
+ sub.finish(stale);
1038
+ return false;
1039
+ }
1040
+ }
1041
+ return true;
1042
+ };
1043
+ void (async () => {
1044
+ try {
1045
+ // Iterator construction is adapter code too: keep a throwing factory inside the same
1046
+ // stale/report/cleanup boundary as a throwing `next()`.
1047
+ it = source[Symbol.asyncIterator]();
1048
+ for (;;) {
1049
+ // Raced rather than awaited bare: a reader disconnect must release this driver even when
1050
+ // the adapter never yields another frame (its own `return()` may be queued behind the
1051
+ // pending `next()` forever).
1052
+ const res = await Promise.race([it.next(), closedTag]);
1053
+ if (res === "closed")
1054
+ return;
1055
+ if (res.done) {
1056
+ deliver(conform.end());
1057
+ return;
1058
+ }
1059
+ if (!deliver(conform.feed(res.value)))
1060
+ return;
1061
+ }
1062
+ }
1063
+ catch (err) {
1064
+ this.reportRelayError(err, { streamId, phase: "frames" });
1065
+ deliver(conform.fail());
1066
+ }
1067
+ finally {
1068
+ release();
1069
+ // If construction itself threw there is no iterator to return, and asking the source to
1070
+ // construct a second one during cleanup could repeat side effects or throw again.
1071
+ closeFrameSource(undefined, it);
1072
+ }
1073
+ })();
1074
+ return { streamId, frames: sub.frames(), close: () => sub.close() };
1075
+ }
1076
+ /** Mirror one producer frame outward (LM-STREAM-RELAY §3). Never blocks or breaks the producer:
1077
+ * a throw or rejected promise is reported and swallowed — a relay outage may cost relayed
1078
+ * readers the live leg, never the generation or its checkpoints. */
1079
+ publishRelay(streamId, frame) {
1080
+ const relay = this.opts.relay;
1081
+ if (relay?.publish === undefined)
1082
+ return;
1083
+ try {
1084
+ const published = relay.publish(streamId, frame);
1085
+ if (published !== undefined) {
1086
+ void Promise.resolve(published).catch((err) => this.reportRelayError(err, { streamId, phase: "publish" }));
1087
+ }
1088
+ }
1089
+ catch (err) {
1090
+ this.reportRelayError(err, { streamId, phase: "publish" });
1091
+ }
1092
+ }
1093
+ reportRelayError(err, info) {
1094
+ // Same discipline as reportCheckpointError: a diagnostic must never take its caller down.
1095
+ try {
1096
+ if (this.opts.onRelayError)
1097
+ this.opts.onRelayError(err, info);
1098
+ else
1099
+ console.error(`[rindle api-server] stream ${info.streamId}: relay ${info.phase} failed:`, err);
1100
+ }
1101
+ catch (hookErr) {
1102
+ console.error(`[rindle api-server] stream ${info.streamId}: onRelayError itself threw:`, hookErr);
1103
+ }
1104
+ }
794
1105
  /** Seal every live stream `interrupted`. In mapped-table mode the seal IS the compaction, so a
795
1106
  * graceful drain loses nothing PRODUCED; the status still says the response was cut short rather
796
1107
  * than claiming completion (§5). Wire it to SIGTERM. */
@@ -802,6 +1113,9 @@ export class StreamPlane {
802
1113
  for (const s of this.live.values())
803
1114
  s.detachAll();
804
1115
  this.live.clear();
1116
+ for (const s of [...this.relayed])
1117
+ s.close();
1118
+ this.relayed.clear();
805
1119
  }
806
1120
  /** A sealed stream stays joinable for the linger window, so a subscribe that races the last token
807
1121
  * still gets `end` (and the tail it missed) rather than a bare `absent`. */
@@ -950,6 +1264,18 @@ export class StreamForbidden extends Error {
950
1264
  this.streamId = streamId;
951
1265
  }
952
1266
  }
1267
+ /** Best-effort adapter teardown (the `for await` discipline, by hand): never awaited into the
1268
+ * plane — a hung `return()` must not hold anything — and a synchronously-throwing one is the
1269
+ * adapter's bug, not the plane's problem. */
1270
+ function closeFrameSource(source, it) {
1271
+ try {
1272
+ const iter = it ?? source?.[Symbol.asyncIterator]();
1273
+ void Promise.resolve(iter?.return?.()).catch(() => { });
1274
+ }
1275
+ catch {
1276
+ // ignored — see above
1277
+ }
1278
+ }
953
1279
  function oneFrame(streamId, frame) {
954
1280
  let taken = false;
955
1281
  return {