@rindle/optimistic 0.4.3 → 0.5.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/README.md +30 -61
- package/dist/backend.d.ts +881 -26
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +2033 -205
- package/dist/backend.js.map +1 -1
- package/dist/client.d.ts +156 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +848 -8
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/local-persist.d.ts +102 -0
- package/dist/local-persist.d.ts.map +1 -0
- package/dist/local-persist.js +957 -0
- package/dist/local-persist.js.map +1 -0
- package/dist/system-streams.d.ts +62 -0
- package/dist/system-streams.d.ts.map +1 -0
- package/dist/system-streams.js +107 -0
- package/dist/system-streams.js.map +1 -0
- package/package.json +6 -5
- package/src/backend.ts +2476 -198
- package/src/client.ts +1174 -13
- package/src/index.ts +51 -0
- package/src/local-persist.ts +1129 -0
- package/src/system-streams.ts +140 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The §4 lifecycle SYSTEM-STREAM plane, client half (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md
|
|
2
|
+
// Slice I-iii): the four daemon-registered system tables the api-server mints subscriptions over
|
|
3
|
+
// when its realtime `lifecycle` config is present — the doorbell (`_rindle_scope_sessions`, §4.1)
|
|
4
|
+
// on every labeled lease, and the fence bundle (`_rindle_room_watermark` §4.2 +
|
|
5
|
+
// `_rindle_room_client_mutations` §7.1 + `_rindle_room_mutation_outcomes` §3.3) on every
|
|
6
|
+
// room-served lease.
|
|
7
|
+
//
|
|
8
|
+
// This module holds the leaf vocabulary the backend and the client share:
|
|
9
|
+
// - the table NAMES + wire schemas (the daemon DDL is the source of truth —
|
|
10
|
+
// `rust/rindle-replica/src/mutations.rs` `realtime_lifecycle_ddl()` / the room-ledger DDL in
|
|
11
|
+
// `enable_client_mutations` — mirrored here exactly like `CLIENT_MUTATIONS_SCHEMA` mirrors the
|
|
12
|
+
// lmid table, so the system subscriptions' hellos pass CRIT#4 validation);
|
|
13
|
+
// - the retain SPEC (`SystemStreamSpec`) a system subscription carries so the backend knows,
|
|
14
|
+
// at fold time, which table a system qid serves and which scope/doc it was minted for;
|
|
15
|
+
// - the reserved client-side query NAME system retains subscribe under. Like
|
|
16
|
+
// `LMID_QUERY_NAME`, it is part of the client wire bookkeeping, NOT an app query: the
|
|
17
|
+
// api-server never resolves it by name — the subscribe presents the minted lease's
|
|
18
|
+
// `leaseToken`, and a RE-resolution re-leases the PARENT labeled query and picks the matching
|
|
19
|
+
// entry out of the fresh `lifecycle` block (client.ts).
|
|
20
|
+
//
|
|
21
|
+
// The schemas live HERE (not `@rindle/client`) deliberately: the system plane is an
|
|
22
|
+
// optimistic-store concern — no other backend composition consumes these tables.
|
|
23
|
+
|
|
24
|
+
import type { NormalizedTableSchema, WireValue } from "@rindle/client";
|
|
25
|
+
|
|
26
|
+
/** §4.1 occupancy: `(scope, client_id, expires_at)`, PK `(scope, client_id)`. The row delta IS
|
|
27
|
+
* the upgrade doorbell (Slice I-iv reacts; I-iii only folds the map). */
|
|
28
|
+
export const SCOPE_SESSIONS_TABLE = "_rindle_scope_sessions";
|
|
29
|
+
/** §4.2 downgrade fence: `(doc, flush_seq)`, PK `doc` — monotone, co-committed per room flush. */
|
|
30
|
+
export const ROOM_WATERMARK_TABLE = "_rindle_room_watermark";
|
|
31
|
+
/** §7.1 domain-scoped room ledger: `(doc, client_id, last_mutation_id)`, PK `(doc, client_id)` —
|
|
32
|
+
* the FIRST daemon-carried room-lmid path (the named invariant's enforcement point). */
|
|
33
|
+
export const ROOM_CLIENT_MUTATIONS_TABLE = "_rindle_room_client_mutations";
|
|
34
|
+
/** §3.3 durable outcome rows: `(doc, client_id, mid, kind, reason, name, args)`, PK
|
|
35
|
+
* `(doc, client_id, mid)` — the H-iv-b `mutationOutcome` frame's durable twin (Slice I-ii
|
|
36
|
+
* co-commits one per NON-applied mid; an absent row under a covering lmid means `applied`). */
|
|
37
|
+
export const ROOM_MUTATION_OUTCOMES_TABLE = "_rindle_room_mutation_outcomes";
|
|
38
|
+
|
|
39
|
+
/** The four wire schemas, mirroring the daemon DDL column-for-column (order and PK by name are
|
|
40
|
+
* what `validateAgainstClientSchema` checks at each system hello — CRIT#4). Appended to the
|
|
41
|
+
* backend's expected-schema base unconditionally: extra CLIENT-side entries are inert (validation
|
|
42
|
+
* only checks tables a server actually advertises), so a client that never receives a lifecycle
|
|
43
|
+
* block behaves byte-identically. */
|
|
44
|
+
export const SCOPE_SESSIONS_SCHEMA: NormalizedTableSchema = {
|
|
45
|
+
name: SCOPE_SESSIONS_TABLE,
|
|
46
|
+
columns: ["scope", "client_id", "expires_at"],
|
|
47
|
+
primaryKey: [0, 1],
|
|
48
|
+
};
|
|
49
|
+
export const ROOM_WATERMARK_SCHEMA: NormalizedTableSchema = {
|
|
50
|
+
name: ROOM_WATERMARK_TABLE,
|
|
51
|
+
columns: ["doc", "flush_seq"],
|
|
52
|
+
primaryKey: [0],
|
|
53
|
+
};
|
|
54
|
+
export const ROOM_CLIENT_MUTATIONS_SCHEMA: NormalizedTableSchema = {
|
|
55
|
+
name: ROOM_CLIENT_MUTATIONS_TABLE,
|
|
56
|
+
columns: ["doc", "client_id", "last_mutation_id"],
|
|
57
|
+
primaryKey: [0, 1],
|
|
58
|
+
};
|
|
59
|
+
export const ROOM_MUTATION_OUTCOMES_SCHEMA: NormalizedTableSchema = {
|
|
60
|
+
name: ROOM_MUTATION_OUTCOMES_TABLE,
|
|
61
|
+
columns: ["doc", "client_id", "mid", "kind", "reason", "name", "args"],
|
|
62
|
+
primaryKey: [0, 1, 2],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const LIFECYCLE_TABLE_SCHEMAS: readonly NormalizedTableSchema[] = [
|
|
66
|
+
SCOPE_SESSIONS_SCHEMA,
|
|
67
|
+
ROOM_WATERMARK_SCHEMA,
|
|
68
|
+
ROOM_CLIENT_MUTATIONS_SCHEMA,
|
|
69
|
+
ROOM_MUTATION_OUTCOMES_SCHEMA,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/** The tables a system retain may serve — the fold category discriminant. */
|
|
73
|
+
export type SystemStreamTable =
|
|
74
|
+
| typeof SCOPE_SESSIONS_TABLE
|
|
75
|
+
| typeof ROOM_WATERMARK_TABLE
|
|
76
|
+
| typeof ROOM_CLIENT_MUTATIONS_TABLE
|
|
77
|
+
| typeof ROOM_MUTATION_OUTCOMES_TABLE;
|
|
78
|
+
|
|
79
|
+
/** What a system retain declares about itself (`OptimisticBackend.retainSystemQuery`): which
|
|
80
|
+
* system table its frames carry, and the scope/doc its minted predicate was scoped to. The fold
|
|
81
|
+
* filters rows against this spec (AND against the client's own `clientID`) as DEFENSE IN DEPTH —
|
|
82
|
+
* the server predicate is the tight path, but a server that couldn't scope (no `clientId` on the
|
|
83
|
+
* lease request) legitimately delivers other clients' rows, and a confused server must never
|
|
84
|
+
* invent verdicts for us. */
|
|
85
|
+
export interface SystemStreamSpec {
|
|
86
|
+
table: SystemStreamTable;
|
|
87
|
+
/** The §4.1 occupancy scope the doorbell was minted for (`_rindle_scope_sessions` only). */
|
|
88
|
+
scope?: string;
|
|
89
|
+
/** The room doc the fence entry was minted for (the three room tables). */
|
|
90
|
+
doc?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The reserved name system retains subscribe under (never an app query — see the module doc).
|
|
94
|
+
* Its `args` carry the {@link SystemStreamSpec} identity fields PLUS the parent labeled query's
|
|
95
|
+
* `(name, args)`, so a RE-resolution (reconnect / gate overflow) can re-lease the parent and pick
|
|
96
|
+
* the matching lifecycle entry — renewal-as-reauthorization, the room-token precedent. */
|
|
97
|
+
export const LIFECYCLE_QUERY_NAME = "_rindle/lifecycle";
|
|
98
|
+
|
|
99
|
+
/** The domain/gate key a room doc's daemon-carried confirms fold into — the SAME key the lease
|
|
100
|
+
* wire mints for the room source (`api-server: sourceKey = "room:" + doc`), so a daemon-carried
|
|
101
|
+
* lmid and a live room socket's lmid stream land on ONE watermark entry. */
|
|
102
|
+
export function roomDomainKey(doc: string): string {
|
|
103
|
+
return `room:${doc}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One outcome row, decoded (positional per {@link ROOM_MUTATION_OUTCOMES_SCHEMA}). */
|
|
107
|
+
export interface OutcomeRow {
|
|
108
|
+
doc: string;
|
|
109
|
+
clientId: string;
|
|
110
|
+
mid: number;
|
|
111
|
+
kind: "deopt" | "rejected";
|
|
112
|
+
reason?: string;
|
|
113
|
+
name?: string;
|
|
114
|
+
args?: unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Decode one `_rindle_room_mutation_outcomes` wire row; `undefined` for a malformed one (fail
|
|
118
|
+
* CLOSED into "not a verdict" — a garbled row must not invent a deopt/rejection; the absent-row
|
|
119
|
+
* default of the covering lmid then treats the mid as applied, which is I-ii's sound default). */
|
|
120
|
+
export function decodeOutcomeRow(row: readonly WireValue[]): OutcomeRow | undefined {
|
|
121
|
+
const [doc, clientId, mid, kind, reason, name, args] = row;
|
|
122
|
+
if (typeof doc !== "string" || typeof clientId !== "string") return undefined;
|
|
123
|
+
const midNum = Number(mid);
|
|
124
|
+
if (!Number.isFinite(midNum)) return undefined;
|
|
125
|
+
if (kind !== "deopt" && kind !== "rejected") return undefined;
|
|
126
|
+
const out: OutcomeRow = { doc, clientId, mid: midNum, kind };
|
|
127
|
+
if (typeof reason === "string") out.reason = reason;
|
|
128
|
+
if (typeof name === "string") out.name = name;
|
|
129
|
+
if (typeof args === "string") {
|
|
130
|
+
// The row echoes `args` as JSON TEXT (I-ii). A frame synthesized without parseable args still
|
|
131
|
+
// resolves a PENDING entry (the flip re-uses the entry's own args); only the already-retired
|
|
132
|
+
// re-invoke arm needs them — it drops a frame that is not self-contained, the H-v contract.
|
|
133
|
+
try {
|
|
134
|
+
out.args = JSON.parse(args);
|
|
135
|
+
} catch {
|
|
136
|
+
/* not self-contained — see above */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|