@rindle/client 0.7.11 → 0.8.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/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export type { KeyedRow, MutationOp, ServerWriteTx } from "./mutation-ops.ts";
11
11
  export { defineMutators, driveMutationAsync, driveMutationSync, isGeneratorMutator, isoTx, shared, } from "./mutation-ops.ts";
12
12
  export type { ArgSchema, AsyncEffectExec, BatchEffect, IsoTx, IsoTxOf, MutationGen, MutatorCtx, QueryArg, QueryEffect, QueryResultRow, ReadEffect, SharedMutator, SharedMutatorWithArgs, SyncEffectExec, YieldEffect, } from "./mutation-ops.ts";
13
13
  export { stableKey } from "./key.ts";
14
+ export { STREAM_STATUS_STREAMING, assembleDurableText, frameResumePoint, spliceStreamText, } from "./stream.ts";
15
+ export type { StreamFrame, StreamStatus } from "./stream.ts";
14
16
  export { COMPARATOR_VERSION, compareNumber, compareRows, compareString, compareValue } from "./compare.ts";
15
17
  export type { Backend, BackendDevObserver, BackendServerDelta, ChangeEvent, ColType, FlatChange, FlatOp, Mutation, MutationEnvelope, MutationOutcomeFrame, NormalizedEvent, NormalizedOp, NormalizedSource, NormalizedTableSchema, OptimisticSource, PathSeg, ProgressFrame, QueryId, RemoteQuery, ResultType, WireNode, WireProjection, WireRel, WireSchema, WireValue, } from "./types.ts";
16
18
  export { CLIENT_MUTATIONS_SCHEMA, CLIENT_MUTATIONS_TABLE, LMID_QUERY_NAME } from "./types.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AAEzB,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE7D,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,EACL,MAAM,GACP,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,SAAS,EACT,eAAe,EACf,WAAW,EACX,KAAK,EACL,OAAO,EACP,WAAW,EACX,UAAU,EACV,QAAQ,EACR,WAAW,EACX,cAAc,EACd,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,cAAc,EACd,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE3G,YAAY,EACV,OAAO,EACP,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,OAAO,EACP,UAAU,EACV,MAAM,EACN,QAAQ,EACR,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,gBAAgB,EAChB,OAAO,EACP,aAAa,EACb,OAAO,EACP,WAAW,EACX,UAAU,EACV,QAAQ,EACR,cAAc,EACd,OAAO,EACP,UAAU,EACV,SAAS,GACV,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAG9F,YAAY,EACV,SAAS,EACT,GAAG,EACH,KAAK,EACL,SAAS,EACT,kBAAkB,EAClB,WAAW,EACX,GAAG,EACH,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,aAAa,GACd,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AAEzB,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE7D,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,EACL,MAAM,GACP,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,SAAS,EACT,eAAe,EACf,WAAW,EACX,KAAK,EACL,OAAO,EACP,WAAW,EACX,UAAU,EACV,QAAQ,EACR,WAAW,EACX,cAAc,EACd,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,cAAc,EACd,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAKrC,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE3G,YAAY,EACV,OAAO,EACP,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,OAAO,EACP,UAAU,EACV,MAAM,EACN,QAAQ,EACR,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,gBAAgB,EAChB,OAAO,EACP,aAAa,EACb,OAAO,EACP,WAAW,EACX,UAAU,EACV,QAAQ,EACR,cAAc,EACd,OAAO,EACP,UAAU,EACV,SAAS,GACV,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAG9F,YAAY,EACV,SAAS,EACT,GAAG,EACH,KAAK,EACL,SAAS,EACT,kBAAkB,EAClB,WAAW,EACX,GAAG,EACH,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,aAAa,GACd,MAAM,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -12,6 +12,10 @@ export { resolveChange, subRow } from "./resolve.js"; // positional FlatChange
12
12
  // shared (generator) mutator seam — one body, sync on the client + async on the server
13
13
  export { defineMutators, driveMutationAsync, driveMutationSync, isGeneratorMutator, isoTx, shared, } from "./mutation-ops.js";
14
14
  export { stableKey } from "./key.js"; // canonical viewKey for an AST (shared with @rindle/react)
15
+ // The LM stream plane's shared contract (LM-STREAM-CHECKPOINT-DESIGN.md): frame shapes + the two pure
16
+ // reassembly functions. Lives here because BOTH tiers need them — `@rindle/api-server` re-exports
17
+ // every name, and `@rindle/react`'s `useStreamedText` builds on them without a server import.
18
+ export { STREAM_STATUS_STREAMING, assembleDurableText, frameResumePoint, spliceStreamText, } from "./stream.js";
15
19
  export { COMPARATOR_VERSION, compareNumber, compareRows, compareString, compareValue } from "./compare.js";
16
20
  export { CLIENT_MUTATIONS_SCHEMA, CLIENT_MUTATIONS_TABLE, LMID_QUERY_NAME } from "./types.js";
17
21
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,0FAA0F;AAC1F,8CAA8C;AAE9C,cAAc,aAAa,CAAC,CAAC,oMAAoM;AACjO,cAAc,gBAAgB,CAAC,CAAC,mGAAmG;AACnI,cAAc,YAAY,CAAC,CAAC,uIAAuI;AACnK,cAAc,WAAW,CAAC,CAAC,uEAAuE;AAClG,cAAc,YAAY,CAAC,CAAC,kEAAkE;AAC9F,cAAc,aAAa,CAAC,CAAC,qDAAqD;AAClF,cAAc,UAAU,CAAC,CAAC,oGAAoG;AAE9H,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC,CAAC,mEAAmE;AAIzH,uFAAuF;AACvF,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,EACL,MAAM,GACP,MAAM,mBAAmB,CAAC;AAmB3B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC,CAAC,2DAA2D;AAEjG,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AA8B3G,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,0FAA0F;AAC1F,8CAA8C;AAE9C,cAAc,aAAa,CAAC,CAAC,oMAAoM;AACjO,cAAc,gBAAgB,CAAC,CAAC,mGAAmG;AACnI,cAAc,YAAY,CAAC,CAAC,uIAAuI;AACnK,cAAc,WAAW,CAAC,CAAC,uEAAuE;AAClG,cAAc,YAAY,CAAC,CAAC,kEAAkE;AAC9F,cAAc,aAAa,CAAC,CAAC,qDAAqD;AAClF,cAAc,UAAU,CAAC,CAAC,oGAAoG;AAE9H,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC,CAAC,mEAAmE;AAIzH,uFAAuF;AACvF,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,EACL,MAAM,GACP,MAAM,mBAAmB,CAAC;AAmB3B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC,CAAC,2DAA2D;AAEjG,sGAAsG;AACtG,kGAAkG;AAClG,8FAA8F;AAC9F,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AA8B3G,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * How a stream ended.
3
+ * - `complete` — the model finished.
4
+ * - `cancelled` — the reader asked it to stop and the producer honoured it.
5
+ * - `error` — the generation threw.
6
+ * - `interrupted` — the host went away mid-generation. The one status that implies the store may be
7
+ * short of what was produced.
8
+ */
9
+ export type StreamStatus = "complete" | "cancelled" | "error" | "interrupted";
10
+ /** The value in the mapped status column while a stream is live. */
11
+ export declare const STREAM_STATUS_STREAMING = "streaming";
12
+ /**
13
+ * One frame of a subscription. A subscription always begins with `open` and always ends with exactly
14
+ * one terminal frame — `end`, `stale`, or `absent` — after which the iterator completes.
15
+ *
16
+ * `stale` and `absent` are the two "you are on the durable plane now" answers, and both are SAFE:
17
+ * the store holds everything below `floorSeq` and everything through `durableSeq`, so the reader's
18
+ * IVM view converges without the stream. Neither is an error.
19
+ */
20
+ export type StreamFrame =
21
+ /** Join accepted. `from` is the (clamped) offset the replay starts at. */
22
+ {
23
+ type: "open";
24
+ streamId: string;
25
+ from: number;
26
+ seq: number;
27
+ durableSeq: number;
28
+ ended: boolean;
29
+ }
30
+ /** PRODUCED text — not a durability claim. `text.length === seq - from`, always. */
31
+ | {
32
+ type: "chunk";
33
+ from: number;
34
+ seq: number;
35
+ text: string;
36
+ }
37
+ /** The store now holds the prefix through `seq`. */
38
+ | {
39
+ type: "durable";
40
+ seq: number;
41
+ }
42
+ /** Sealed. No further frames. */
43
+ | {
44
+ type: "end";
45
+ seq: number;
46
+ status: StreamStatus;
47
+ error?: string;
48
+ }
49
+ /** `from` is below the producer's retained buffer floor (or the subscriber fell too far behind):
50
+ * read the store. (A raw `EventSource` rejoins automatically on its reconnect; `useStreamedText`
51
+ * deliberately stays on the durable plane instead — correct, at checkpoint granularity.) */
52
+ | {
53
+ type: "stale";
54
+ floorSeq: number;
55
+ durableSeq: number;
56
+ }
57
+ /** The process serving this subscribe is not hosting the stream (wrong instance, already evicted,
58
+ * or it never existed): the store is the whole truth. */
59
+ | {
60
+ type: "absent";
61
+ };
62
+ /** The resume point a frame implies — what rides an SSE `id:` line so a reconnecting `EventSource`
63
+ * hands it straight back as `Last-Event-ID`. `undefined` for frames that are not a position. */
64
+ export declare function frameResumePoint(frame: StreamFrame): number | undefined;
65
+ /**
66
+ * Merge the durable plane with the live tail.
67
+ *
68
+ * `durable` is what the IVM view shows; `produced` is what a subscription has accumulated (the prefix
69
+ * it joined at, plus every `chunk`). Both are prefixes of the same response, so the merge is "take
70
+ * the longer" — no diffing, no overlap handling, no ranges.
71
+ *
72
+ * The length comparison is the whole algorithm, which is why a caller MUST seed its accumulator with
73
+ * the text it joined at: a tail carrying only the chunks it received would read as shorter than the
74
+ * durable text and be discarded. `useStreamedText` does that for you.
75
+ */
76
+ export declare function spliceStreamText(durable: string, produced: string): string;
77
+ /**
78
+ * The durable half of the splice for the mapped-table layout: the compacted `body` followed by
79
+ * whatever chunk rows have not been folded into it yet.
80
+ *
81
+ * Chunks are ALWAYS the suffix after `body` — the closing checkpoint rewrites `body` and drops the
82
+ * chunks it absorbed in ONE transaction — so a reader never observes a torn state where a chunk both
83
+ * is and is not in the body.
84
+ */
85
+ export declare function assembleDurableText(message: {
86
+ body?: string | null;
87
+ } | null | undefined, chunks?: ReadonlyArray<{
88
+ seq: number;
89
+ text: string;
90
+ }>): string;
91
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAUA;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,OAAO,GAAG,aAAa,CAAC;AAE9E,oEAAoE;AACpE,eAAO,MAAM,uBAAuB,cAAc,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW;AACrB,0EAA0E;AACxE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AACnG,oFAAoF;GAClF;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC5D,oDAAoD;GAClD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAClC,iCAAiC;GAC/B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AACpE;;6FAE6F;GAC3F;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE;AACzD;0DAC0D;GACxD;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEvB;iGACiG;AACjG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAWvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACpD,MAAM,GAAE,aAAa,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAM,GACxD,MAAM,CAGR"}
package/dist/stream.js ADDED
@@ -0,0 +1,52 @@
1
+ // The LM stream plane's SHARED contract (designs/LM-STREAM-CHECKPOINT-DESIGN.md): the frame shapes
2
+ // both ends speak, plus the two pure functions that reassemble a response from its two planes.
3
+ //
4
+ // This lives in `@rindle/client` rather than `@rindle/api-server` because BOTH tiers need it and the
5
+ // browser must never pull the server package (sql-client, daemon-client, …) to read a stream.
6
+ // `@rindle/api-server` re-exports every name here, so the server-side import path is unchanged.
7
+ //
8
+ // Nothing in this module has state, I/O, or a React dependency — the hook that drives a subscription
9
+ // is `useStreamedText` in `@rindle/react`; the transport is the app's choice.
10
+ /** The value in the mapped status column while a stream is live. */
11
+ export const STREAM_STATUS_STREAMING = "streaming";
12
+ /** The resume point a frame implies — what rides an SSE `id:` line so a reconnecting `EventSource`
13
+ * hands it straight back as `Last-Event-ID`. `undefined` for frames that are not a position. */
14
+ export function frameResumePoint(frame) {
15
+ switch (frame.type) {
16
+ case "open":
17
+ return frame.from;
18
+ case "chunk":
19
+ case "durable":
20
+ case "end":
21
+ return frame.seq;
22
+ default:
23
+ return undefined;
24
+ }
25
+ }
26
+ /**
27
+ * Merge the durable plane with the live tail.
28
+ *
29
+ * `durable` is what the IVM view shows; `produced` is what a subscription has accumulated (the prefix
30
+ * it joined at, plus every `chunk`). Both are prefixes of the same response, so the merge is "take
31
+ * the longer" — no diffing, no overlap handling, no ranges.
32
+ *
33
+ * The length comparison is the whole algorithm, which is why a caller MUST seed its accumulator with
34
+ * the text it joined at: a tail carrying only the chunks it received would read as shorter than the
35
+ * durable text and be discarded. `useStreamedText` does that for you.
36
+ */
37
+ export function spliceStreamText(durable, produced) {
38
+ return produced.length > durable.length ? durable + produced.slice(durable.length) : durable;
39
+ }
40
+ /**
41
+ * The durable half of the splice for the mapped-table layout: the compacted `body` followed by
42
+ * whatever chunk rows have not been folded into it yet.
43
+ *
44
+ * Chunks are ALWAYS the suffix after `body` — the closing checkpoint rewrites `body` and drops the
45
+ * chunks it absorbed in ONE transaction — so a reader never observes a torn state where a chunk both
46
+ * is and is not in the body.
47
+ */
48
+ export function assembleDurableText(message, chunks = []) {
49
+ const ordered = [...chunks].sort((a, b) => a.seq - b.seq);
50
+ return (message?.body ?? "") + ordered.map((c) => c.text).join("");
51
+ }
52
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,+FAA+F;AAC/F,EAAE;AACF,qGAAqG;AACrG,8FAA8F;AAC9F,gGAAgG;AAChG,EAAE;AACF,qGAAqG;AACrG,8EAA8E;AAY9E,oEAAoE;AACpE,MAAM,CAAC,MAAM,uBAAuB,GAAG,WAAW,CAAC;AA2BnD;iGACiG;AACjG,MAAM,UAAU,gBAAgB,CAAC,KAAkB;IACjD,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,CAAC;QACpB,KAAK,OAAO,CAAC;QACb,KAAK,SAAS,CAAC;QACf,KAAK,KAAK;YACR,OAAO,KAAK,CAAC,GAAG,CAAC;QACnB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,QAAgB;IAChE,OAAO,QAAQ,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAC/F,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAoD,EACpD,SAAuD,EAAE;IAEzD,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IAC1D,OAAO,CAAC,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACrE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rindle/client",
3
- "version": "0.7.11",
3
+ "version": "0.8.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -43,6 +43,17 @@ export type {
43
43
 
44
44
  export { stableKey } from "./key.ts"; // canonical viewKey for an AST (shared with @rindle/react)
45
45
 
46
+ // The LM stream plane's shared contract (LM-STREAM-CHECKPOINT-DESIGN.md): frame shapes + the two pure
47
+ // reassembly functions. Lives here because BOTH tiers need them — `@rindle/api-server` re-exports
48
+ // every name, and `@rindle/react`'s `useStreamedText` builds on them without a server import.
49
+ export {
50
+ STREAM_STATUS_STREAMING,
51
+ assembleDurableText,
52
+ frameResumePoint,
53
+ spliceStreamText,
54
+ } from "./stream.ts";
55
+ export type { StreamFrame, StreamStatus } from "./stream.ts";
56
+
46
57
  export { COMPARATOR_VERSION, compareNumber, compareRows, compareString, compareValue } from "./compare.ts";
47
58
 
48
59
  export type {
package/src/stream.ts ADDED
@@ -0,0 +1,93 @@
1
+ // The LM stream plane's SHARED contract (designs/LM-STREAM-CHECKPOINT-DESIGN.md): the frame shapes
2
+ // both ends speak, plus the two pure functions that reassemble a response from its two planes.
3
+ //
4
+ // This lives in `@rindle/client` rather than `@rindle/api-server` because BOTH tiers need it and the
5
+ // browser must never pull the server package (sql-client, daemon-client, …) to read a stream.
6
+ // `@rindle/api-server` re-exports every name here, so the server-side import path is unchanged.
7
+ //
8
+ // Nothing in this module has state, I/O, or a React dependency — the hook that drives a subscription
9
+ // is `useStreamedText` in `@rindle/react`; the transport is the app's choice.
10
+
11
+ /**
12
+ * How a stream ended.
13
+ * - `complete` — the model finished.
14
+ * - `cancelled` — the reader asked it to stop and the producer honoured it.
15
+ * - `error` — the generation threw.
16
+ * - `interrupted` — the host went away mid-generation. The one status that implies the store may be
17
+ * short of what was produced.
18
+ */
19
+ export type StreamStatus = "complete" | "cancelled" | "error" | "interrupted";
20
+
21
+ /** The value in the mapped status column while a stream is live. */
22
+ export const STREAM_STATUS_STREAMING = "streaming";
23
+
24
+ /**
25
+ * One frame of a subscription. A subscription always begins with `open` and always ends with exactly
26
+ * one terminal frame — `end`, `stale`, or `absent` — after which the iterator completes.
27
+ *
28
+ * `stale` and `absent` are the two "you are on the durable plane now" answers, and both are SAFE:
29
+ * the store holds everything below `floorSeq` and everything through `durableSeq`, so the reader's
30
+ * IVM view converges without the stream. Neither is an error.
31
+ */
32
+ export type StreamFrame =
33
+ /** Join accepted. `from` is the (clamped) offset the replay starts at. */
34
+ | { type: "open"; streamId: string; from: number; seq: number; durableSeq: number; ended: boolean }
35
+ /** PRODUCED text — not a durability claim. `text.length === seq - from`, always. */
36
+ | { type: "chunk"; from: number; seq: number; text: string }
37
+ /** The store now holds the prefix through `seq`. */
38
+ | { type: "durable"; seq: number }
39
+ /** Sealed. No further frames. */
40
+ | { type: "end"; seq: number; status: StreamStatus; error?: string }
41
+ /** `from` is below the producer's retained buffer floor (or the subscriber fell too far behind):
42
+ * read the store. (A raw `EventSource` rejoins automatically on its reconnect; `useStreamedText`
43
+ * deliberately stays on the durable plane instead — correct, at checkpoint granularity.) */
44
+ | { type: "stale"; floorSeq: number; durableSeq: number }
45
+ /** The process serving this subscribe is not hosting the stream (wrong instance, already evicted,
46
+ * or it never existed): the store is the whole truth. */
47
+ | { type: "absent" };
48
+
49
+ /** The resume point a frame implies — what rides an SSE `id:` line so a reconnecting `EventSource`
50
+ * hands it straight back as `Last-Event-ID`. `undefined` for frames that are not a position. */
51
+ export function frameResumePoint(frame: StreamFrame): number | undefined {
52
+ switch (frame.type) {
53
+ case "open":
54
+ return frame.from;
55
+ case "chunk":
56
+ case "durable":
57
+ case "end":
58
+ return frame.seq;
59
+ default:
60
+ return undefined;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Merge the durable plane with the live tail.
66
+ *
67
+ * `durable` is what the IVM view shows; `produced` is what a subscription has accumulated (the prefix
68
+ * it joined at, plus every `chunk`). Both are prefixes of the same response, so the merge is "take
69
+ * the longer" — no diffing, no overlap handling, no ranges.
70
+ *
71
+ * The length comparison is the whole algorithm, which is why a caller MUST seed its accumulator with
72
+ * the text it joined at: a tail carrying only the chunks it received would read as shorter than the
73
+ * durable text and be discarded. `useStreamedText` does that for you.
74
+ */
75
+ export function spliceStreamText(durable: string, produced: string): string {
76
+ return produced.length > durable.length ? durable + produced.slice(durable.length) : durable;
77
+ }
78
+
79
+ /**
80
+ * The durable half of the splice for the mapped-table layout: the compacted `body` followed by
81
+ * whatever chunk rows have not been folded into it yet.
82
+ *
83
+ * Chunks are ALWAYS the suffix after `body` — the closing checkpoint rewrites `body` and drops the
84
+ * chunks it absorbed in ONE transaction — so a reader never observes a torn state where a chunk both
85
+ * is and is not in the body.
86
+ */
87
+ export function assembleDurableText(
88
+ message: { body?: string | null } | null | undefined,
89
+ chunks: ReadonlyArray<{ seq: number; text: string }> = [],
90
+ ): string {
91
+ const ordered = [...chunks].sort((a, b) => a.seq - b.seq);
92
+ return (message?.body ?? "") + ordered.map((c) => c.text).join("");
93
+ }