@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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/stream.d.ts +91 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +52 -0
- package/dist/stream.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +11 -0
- package/src/stream.ts +93 -0
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/stream.d.ts
ADDED
|
@@ -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
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
|
+
}
|