@lunora/replica 1.0.0-alpha.5 → 1.0.0-alpha.50
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/LICENSE.md +214 -0
- package/dist/adapters/better-sqlite3.d.mts +14 -14
- package/dist/adapters/better-sqlite3.d.ts +14 -14
- package/dist/adapters/better-sqlite3.mjs +1 -29
- package/dist/adapters/sqlite-wasm.d.mts +25 -25
- package/dist/adapters/sqlite-wasm.d.ts +25 -25
- package/dist/adapters/sqlite-wasm.mjs +1 -44
- package/dist/adapters/sqljs.d.mts +10 -10
- package/dist/adapters/sqljs.d.ts +10 -10
- package/dist/adapters/sqljs.mjs +1 -55
- package/dist/index.d.mts +633 -622
- package/dist/index.d.ts +633 -622
- package/dist/index.mjs +1 -20
- package/dist/packem_shared/EventEmitter-ovTsLeAj.mjs +1 -0
- package/dist/packem_shared/EventLog-DmlRY_4Z.mjs +1 -0
- package/dist/packem_shared/EventLogDO-q80QYMLe.mjs +1 -0
- package/dist/packem_shared/EventLogDOClient-lkVhWras.mjs +1 -0
- package/dist/packem_shared/EventSource-ArQhJa6P.mjs +1 -0
- package/dist/packem_shared/EventsSync-DWyGGZQZ.mjs +1 -0
- package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
- package/dist/packem_shared/LocalMirror-DCjNXIyf.mjs +4 -0
- package/dist/packem_shared/MaterializerRuntime-CqGGjSxl.mjs +1 -0
- package/dist/packem_shared/SubscriptionManager-CzEXcqvp.mjs +1 -0
- package/dist/packem_shared/applyDiff-BUzddc6r.mjs +1 -0
- package/dist/packem_shared/applyDiffToDb-DSnSZmL4.mjs +1 -0
- package/dist/packem_shared/classifyChanges-IoOGDVVx.mjs +1 -0
- package/dist/packem_shared/defineEvents-D3OcpXb_.mjs +1 -0
- package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
- package/dist/packem_shared/isClientSeq-DSXBJskD.mjs +1 -0
- package/dist/packem_shared/local-mirror.d-ByrYTq4z.d.ts +530 -0
- package/dist/packem_shared/local-mirror.d-Cip3QuMf.d.mts +530 -0
- package/dist/packem_shared/subscribeToMirror-Dru_AYBu.mjs +1 -0
- package/dist/packem_shared/{types.d-BuAWjEY5.d.mts → types.d-CkMkSwLJ.d.mts} +10 -10
- package/dist/packem_shared/{types.d-BuAWjEY5.d.ts → types.d-CkMkSwLJ.d.ts} +10 -10
- package/dist/react.d.mts +88 -61
- package/dist/react.d.ts +88 -61
- package/dist/react.mjs +1 -15
- package/package.json +1 -1
- package/dist/packem_shared/EventEmitter-CMZfct03.mjs +0 -92
- package/dist/packem_shared/EventLog-CnK-3Wge.mjs +0 -264
- package/dist/packem_shared/EventLogDO-DqlsVx0H.mjs +0 -381
- package/dist/packem_shared/EventLogDOClient-F4FO8Si4.mjs +0 -92
- package/dist/packem_shared/EventSource-D5yO9_aI.mjs +0 -221
- package/dist/packem_shared/EventsSync-BP36tC9O.mjs +0 -123
- package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
- package/dist/packem_shared/LocalMirror-a-5jEqFN.mjs +0 -219
- package/dist/packem_shared/MaterializerRuntime-BoIrsMYB.mjs +0 -224
- package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
- package/dist/packem_shared/applyDiff-98tKzmiW.mjs +0 -67
- package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
- package/dist/packem_shared/classifyChanges-RcqLBpLs.mjs +0 -41
- package/dist/packem_shared/defineEvents-DiBkPTh_.mjs +0 -28
- package/dist/packem_shared/eventsContext-Bk_p48hj.mjs +0 -6
- package/dist/packem_shared/isClientSeq-C46BkzqJ.mjs +0 -5
- package/dist/packem_shared/local-mirror.d-ByIjd7sW.d.ts +0 -530
- package/dist/packem_shared/local-mirror.d-DL1XJBB3.d.mts +0 -530
- package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +0 -45
package/dist/index.d.mts
CHANGED
|
@@ -1,130 +1,131 @@
|
|
|
1
1
|
export { createBetterSqlite3Adapter } from "./adapters/better-sqlite3.mjs";
|
|
2
2
|
export { createSqliteWasmAdapter } from "./adapters/sqlite-wasm.mjs";
|
|
3
3
|
export { createSqlJsAdapter } from "./adapters/sqljs.mjs";
|
|
4
|
-
import { S as SqliteAdapter } from "./packem_shared/types.d-
|
|
5
|
-
import { T as TableDiff, I as InputEvent, S as Seq, E as EventLogEntry, a as EventLog, A as AppendOptions, L as LocalMirror } from "./packem_shared/local-mirror.d-
|
|
6
|
-
export { type C as ClientSeq, type b as EventLogOptions, type c as EventLogSnapshot, type G as GlobalSeq, type d as LocalMirrorOptions, type M as MirrorTableDef, type R as RowChange, e as classifyChanges, f as createTableDiff, g as diffSize, i as isClientSeq, h as isDiffEmpty, j as isGlobalSeq, k as isInputEvent, m as mergeDiffs } from "./packem_shared/local-mirror.d-
|
|
7
|
-
/**
|
|
8
|
-
* Apply a single {@link TableDiff} to an in-memory row map and return
|
|
9
|
-
* the updated map.
|
|
10
|
-
*
|
|
11
|
-
* The function creates a **shallow copy** of the input map so the caller's
|
|
12
|
-
* reference stays untouched unless they choose to replace it.
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* const rows = new Map<string, Record<string, unknown>>();
|
|
16
|
-
* rows.set("id-1", { name: "alice" });
|
|
17
|
-
*
|
|
18
|
-
* const diff = createTableDiff("users", [
|
|
19
|
-
* { type: "insert", data: { id: "id-2", name: "bob" } },
|
|
20
|
-
* { type: "update", id: "id-1", data: { name: "alice-updated" } },
|
|
21
|
-
* ]);
|
|
22
|
-
*
|
|
23
|
-
* const updated = applyDiff(rows, diff);
|
|
24
|
-
* updated.get("id-1")?.name // "alice-updated"
|
|
25
|
-
* updated.get("id-2")?.name // "bob"
|
|
26
|
-
* ```
|
|
27
|
-
* @experimental
|
|
28
|
-
*/
|
|
4
|
+
import { S as SqliteAdapter } from "./packem_shared/types.d-CkMkSwLJ.mjs";
|
|
5
|
+
import { T as TableDiff, I as InputEvent, S as Seq, E as EventLogEntry, a as EventLog, A as AppendOptions, L as LocalMirror } from "./packem_shared/local-mirror.d-Cip3QuMf.mjs";
|
|
6
|
+
export { type C as ClientSeq, type b as EventLogOptions, type c as EventLogSnapshot, type G as GlobalSeq, type d as LocalMirrorOptions, type M as MirrorTableDef, type R as RowChange, e as classifyChanges, f as createTableDiff, g as diffSize, i as isClientSeq, h as isDiffEmpty, j as isGlobalSeq, k as isInputEvent, m as mergeDiffs } from "./packem_shared/local-mirror.d-Cip3QuMf.mjs";
|
|
7
|
+
/**
|
|
8
|
+
* Apply a single {@link TableDiff} to an in-memory row map and return
|
|
9
|
+
* the updated map.
|
|
10
|
+
*
|
|
11
|
+
* The function creates a **shallow copy** of the input map so the caller's
|
|
12
|
+
* reference stays untouched unless they choose to replace it.
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const rows = new Map<string, Record<string, unknown>>();
|
|
16
|
+
* rows.set("id-1", { name: "alice" });
|
|
17
|
+
*
|
|
18
|
+
* const diff = createTableDiff("users", [
|
|
19
|
+
* { type: "insert", data: { id: "id-2", name: "bob" } },
|
|
20
|
+
* { type: "update", id: "id-1", data: { name: "alice-updated" } },
|
|
21
|
+
* ]);
|
|
22
|
+
*
|
|
23
|
+
* const updated = applyDiff(rows, diff);
|
|
24
|
+
* updated.get("id-1")?.name // "alice-updated"
|
|
25
|
+
* updated.get("id-2")?.name // "bob"
|
|
26
|
+
* ```
|
|
27
|
+
* @experimental
|
|
28
|
+
*/
|
|
29
29
|
declare const applyDiff: (current: ReadonlyMap<string, Record<string, unknown>>, diff: TableDiff) => Map<string, Record<string, unknown>>;
|
|
30
30
|
/**
|
|
31
|
-
* Apply an array of diffs **in order**, returning the final row map.
|
|
32
|
-
*
|
|
33
|
-
* This is equivalent to calling {@link applyDiff} repeatedly but
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
31
|
+
* Apply an array of diffs **in order**, returning the final row map.
|
|
32
|
+
*
|
|
33
|
+
* This is equivalent to calling {@link applyDiff} repeatedly but copies the
|
|
34
|
+
* input map exactly once rather than once per diff — catch-up replay of an
|
|
35
|
+
* N-diff backlog is a single copy, not N+1.
|
|
36
|
+
* @experimental
|
|
37
|
+
*/
|
|
37
38
|
declare const applyDiffs: (current: ReadonlyMap<string, Record<string, unknown>>, diffs: ReadonlyArray<TableDiff>) => Map<string, Record<string, unknown>>;
|
|
38
39
|
/**
|
|
39
|
-
* Merge the row-level effect of a {@link TableDiff} into plain JSON
|
|
40
|
-
* state keyed by table name, returning a new snapshot.
|
|
41
|
-
* @param snapshot Current snapshot, e.g. `{ users: Map
|
|
42
|
-
* @param diff Contains the target table name and the row-level changes to merge.
|
|
43
|
-
* @returns A shallow copy of `snapshot` with `diff.table`'s map updated.
|
|
44
|
-
* @experimental
|
|
45
|
-
*/
|
|
40
|
+
* Merge the row-level effect of a {@link TableDiff} into plain JSON
|
|
41
|
+
* state keyed by table name, returning a new snapshot.
|
|
42
|
+
* @param snapshot Current snapshot, e.g. `{ users: Map<id, row>, posts: Map<id, row> }`.
|
|
43
|
+
* @param diff Contains the target table name and the row-level changes to merge.
|
|
44
|
+
* @returns A shallow copy of `snapshot` with `diff.table`'s map updated.
|
|
45
|
+
* @experimental
|
|
46
|
+
*/
|
|
46
47
|
declare const applyDiffToSnapshot: (snapshot: ReadonlyMap<string, ReadonlyMap<string, Record<string, unknown>>>, diff: TableDiff) => Map<string, Map<string, Record<string, unknown>>>;
|
|
47
48
|
/** Map a namespace-and-name pair to a qualified event type string. */
|
|
48
49
|
type QualifiedType<Ns extends string, Name extends string> = `${Ns}.${Name}`;
|
|
49
50
|
/**
|
|
50
|
-
* Extract the payload type from an event schema.
|
|
51
|
-
*
|
|
52
|
-
* A `@lunora/values` validator (e.g. `v.object(...)`) carries its output type on
|
|
53
|
-
* the phantom `__type` field (the same hook `Infer` reads), so match that FIRST
|
|
54
|
-
* — otherwise a validator, being object-shaped, would fall through to the
|
|
55
|
-
* `Record` branch and resolve to the validator instance itself rather than its
|
|
56
|
-
* validated `{ … }` output. A bare factory function or plain descriptor object
|
|
57
|
-
* is still supported as a fallback.
|
|
58
|
-
*/
|
|
51
|
+
* Extract the payload type from an event schema.
|
|
52
|
+
*
|
|
53
|
+
* A `@lunora/values` validator (e.g. `v.object(...)`) carries its output type on
|
|
54
|
+
* the phantom `__type` field (the same hook `Infer` reads), so match that FIRST
|
|
55
|
+
* — otherwise a validator, being object-shaped, would fall through to the
|
|
56
|
+
* `Record` branch and resolve to the validator instance itself rather than its
|
|
57
|
+
* validated `{ … }` output. A bare factory function or plain descriptor object
|
|
58
|
+
* is still supported as a fallback.
|
|
59
|
+
*/
|
|
59
60
|
type PayloadOf<T> = T extends {
|
|
60
61
|
readonly __type: infer P;
|
|
61
62
|
} ? P : T extends ((payload: infer P) => unknown) ? P : T extends Record<string, unknown> ? T : never;
|
|
62
63
|
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
|
63
64
|
/**
|
|
64
|
-
* Produce `{ "ns.name": Payload }` for every event, merged.
|
|
65
|
-
*/
|
|
66
|
-
type EventTypeMap<TDefinition extends Record<string, Record<string, unknown>>> = UnionToIntersection<{ [Ns in keyof TDefinition & string]: { [Name in keyof TDefinition[Ns] & string]: { [K in QualifiedType<Ns, Name>]: PayloadOf<TDefinition[Ns][Name]
|
|
65
|
+
* Produce `{ "ns.name": Payload }` for every event, merged.
|
|
66
|
+
*/
|
|
67
|
+
type EventTypeMap<TDefinition extends Record<string, Record<string, unknown>>> = UnionToIntersection<{ [Ns in keyof TDefinition & string]: { [Name in keyof TDefinition[Ns] & string]: { [K in QualifiedType<Ns, Name>]: PayloadOf<TDefinition[Ns][Name]>; }; }[keyof TDefinition[Ns] & string]; }[keyof TDefinition & string]>;
|
|
67
68
|
/**
|
|
68
|
-
* A factory function that creates an {@link InputEvent} for a specific event type.
|
|
69
|
-
*
|
|
70
|
-
* The returned event has no `seq` — it is an optimistic / command payload
|
|
71
|
-
* that the event log will assign a sequence number to on append.
|
|
72
|
-
* @experimental
|
|
73
|
-
*/
|
|
69
|
+
* A factory function that creates an {@link InputEvent} for a specific event type.
|
|
70
|
+
*
|
|
71
|
+
* The returned event has no `seq` — it is an optimistic / command payload
|
|
72
|
+
* that the event log will assign a sequence number to on append.
|
|
73
|
+
* @experimental
|
|
74
|
+
*/
|
|
74
75
|
interface EventFactory<Type extends string, Payload> {
|
|
75
76
|
(payload: Payload): InputEvent<Type, Payload>;
|
|
76
77
|
/** The fully qualified event type string (e.g. `"chat.messageSent"`). */
|
|
77
78
|
readonly type: Type;
|
|
78
79
|
}
|
|
79
80
|
/**
|
|
80
|
-
* The namespace object returned for each group of events.
|
|
81
|
-
* @experimental
|
|
82
|
-
*/
|
|
83
|
-
type EventNamespace<Ns extends string, TDefinition extends Record<string, unknown>> = { [Name in keyof TDefinition & string]: EventFactory<QualifiedType<Ns, Name>, PayloadOf<TDefinition[Name]
|
|
81
|
+
* The namespace object returned for each group of events.
|
|
82
|
+
* @experimental
|
|
83
|
+
*/
|
|
84
|
+
type EventNamespace<Ns extends string, TDefinition extends Record<string, unknown>> = { [Name in keyof TDefinition & string]: EventFactory<QualifiedType<Ns, Name>, PayloadOf<TDefinition[Name]>>; };
|
|
84
85
|
/**
|
|
85
|
-
* The full result of {@link defineEvents}.
|
|
86
|
-
* @experimental
|
|
87
|
-
*/
|
|
88
|
-
type EventsDefinition<TDefinition extends Record<string, Record<string, unknown>>> = { [Ns in keyof TDefinition & string]: EventNamespace<Ns, TDefinition[Ns]
|
|
86
|
+
* The full result of {@link defineEvents}.
|
|
87
|
+
* @experimental
|
|
88
|
+
*/
|
|
89
|
+
type EventsDefinition<TDefinition extends Record<string, Record<string, unknown>>> = { [Ns in keyof TDefinition & string]: EventNamespace<Ns, TDefinition[Ns]>; } & {
|
|
89
90
|
/** Type-level map of event type → payload shape. Useful for generic code. */
|
|
90
91
|
readonly _types: EventTypeMap<TDefinition>;
|
|
91
92
|
};
|
|
92
93
|
/**
|
|
93
|
-
* Declare typed event types for event sourcing.
|
|
94
|
-
*
|
|
95
|
-
* Each key under a namespace becomes a factory function that produces
|
|
96
|
-
* an {@link InputEvent} — an optimistic / command event that the event
|
|
97
|
-
* log will assign a sequence number to on append.
|
|
98
|
-
* @param definition A nested object where the outer keys are namespaces
|
|
99
|
-
* and the inner keys are event names mapped to their
|
|
100
|
-
* payload schemas (or simple type-descriptor objects).
|
|
101
|
-
* @returns An object with the same nesting structure, where each leaf is
|
|
102
|
-
* a factory function plus a `.type` property.
|
|
103
|
-
*/
|
|
94
|
+
* Declare typed event types for event sourcing.
|
|
95
|
+
*
|
|
96
|
+
* Each key under a namespace becomes a factory function that produces
|
|
97
|
+
* an {@link InputEvent} — an optimistic / command event that the event
|
|
98
|
+
* log will assign a sequence number to on append.
|
|
99
|
+
* @param definition A nested object where the outer keys are namespaces
|
|
100
|
+
* and the inner keys are event names mapped to their
|
|
101
|
+
* payload schemas (or simple type-descriptor objects).
|
|
102
|
+
* @returns An object with the same nesting structure, where each leaf is
|
|
103
|
+
* a factory function plus a `.type` property.
|
|
104
|
+
*/
|
|
104
105
|
interface DefineEventsOptions {
|
|
105
106
|
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
* Optional version prefix for all event types.
|
|
108
|
+
*
|
|
109
|
+
* When set, every qualified event type is prefixed with `"v<N>."`, enabling
|
|
110
|
+
* versioned event naming like `"v1.chat.messageSent"` or `"v2.chat.messageSent"`.
|
|
111
|
+
* This allows materializers to evolve their handling logic based on the event
|
|
112
|
+
* version without breaking backward compatibility.
|
|
113
|
+
* @example "v1" → event type becomes "v1.chat.messageSent"
|
|
114
|
+
*/
|
|
114
115
|
readonly version?: string;
|
|
115
116
|
}
|
|
116
117
|
/**
|
|
117
|
-
* `defineEvents` is part of the experimental `@lunora/replica` API and may change without a major version bump.
|
|
118
|
-
* @experimental
|
|
119
|
-
*/
|
|
118
|
+
* `defineEvents` is part of the experimental `@lunora/replica` API and may change without a major version bump.
|
|
119
|
+
* @experimental
|
|
120
|
+
*/
|
|
120
121
|
declare const defineEvents: <TDefinition extends Record<string, Record<string, unknown>>>(definition: TDefinition, options?: DefineEventsOptions) => EventsDefinition<TDefinition>;
|
|
121
122
|
/**
|
|
122
|
-
* Shape of the `events[]` items sent in a POST `/append` body.
|
|
123
|
-
*
|
|
124
|
-
* Like {@link InputEvent} but with `timestamp` optional — omit it to
|
|
125
|
-
* let the server assign the timestamp.
|
|
126
|
-
* @experimental
|
|
127
|
-
*/
|
|
123
|
+
* Shape of the `events[]` items sent in a POST `/append` body.
|
|
124
|
+
*
|
|
125
|
+
* Like {@link InputEvent} but with `timestamp` optional — omit it to
|
|
126
|
+
* let the server assign the timestamp.
|
|
127
|
+
* @experimental
|
|
128
|
+
*/
|
|
128
129
|
interface AppendEventInput {
|
|
129
130
|
/** Globally-unique client identifier (for offline/optimistic support). */
|
|
130
131
|
readonly clientId?: string;
|
|
@@ -140,141 +141,141 @@ interface AppendEventInput {
|
|
|
140
141
|
readonly type: string;
|
|
141
142
|
}
|
|
142
143
|
/**
|
|
143
|
-
* Options for constructing an {@link EventLogDOClient}.
|
|
144
|
-
* @experimental
|
|
145
|
-
*/
|
|
144
|
+
* Options for constructing an {@link EventLogDOClient}.
|
|
145
|
+
* @experimental
|
|
146
|
+
*/
|
|
146
147
|
interface EventLogDOClientOptions {
|
|
147
148
|
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
* A function that dispatches an HTTP request to the target EventLogDO
|
|
150
|
+
* instance. In a Cloudflare Worker this is:
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* (req) => env.MY_DO_NAMESPACE.get(id).fetch(req)
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
155
156
|
fetch: (request: Request) => Promise<Response>;
|
|
156
157
|
}
|
|
157
158
|
/**
|
|
158
|
-
* Lightweight HTTP client for EventLogDO's RPC surface.
|
|
159
|
-
*
|
|
160
|
-
* Each method maps to one of the DO's endpoints, throws on non-OK status,
|
|
161
|
-
* and returns the parsed response body.
|
|
162
|
-
* @experimental
|
|
163
|
-
*/
|
|
159
|
+
* Lightweight HTTP client for EventLogDO's RPC surface.
|
|
160
|
+
*
|
|
161
|
+
* Each method maps to one of the DO's endpoints, throws on non-OK status,
|
|
162
|
+
* and returns the parsed response body.
|
|
163
|
+
* @experimental
|
|
164
|
+
*/
|
|
164
165
|
declare class EventLogDOClient {
|
|
165
166
|
#private;
|
|
166
167
|
constructor(options: EventLogDOClientOptions);
|
|
167
168
|
/**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
* Append one or more events to the log.
|
|
170
|
+
* @param events The events to append.
|
|
171
|
+
* @param options Idempotency controls for the batch.
|
|
172
|
+
* @param options.batchId Optional idempotency key for the whole batch — a
|
|
173
|
+
* retried `append` call with the same `batchId` (e.g. after a network
|
|
174
|
+
* timeout that hid a successful response) returns the originally-persisted
|
|
175
|
+
* entries instead of inserting duplicates.
|
|
176
|
+
* @returns The persisted entries with their assigned `seq` numbers.
|
|
177
|
+
*/
|
|
177
178
|
append(events: AppendEventInput[], options?: {
|
|
178
179
|
batchId?: string;
|
|
179
180
|
}): Promise<EventLogEntry[]>;
|
|
180
181
|
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
* Fetch all entries with `seq >= sinceSeq`.
|
|
183
|
+
*
|
|
184
|
+
* Pass `sinceSeq = 0` to fetch the entire log.
|
|
185
|
+
*/
|
|
185
186
|
getSince(sinceSeq: number): Promise<EventLogEntry[]>;
|
|
186
187
|
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
* Fetch a paginated range of entries.
|
|
189
|
+
* @returns `{ entries, hasMore }` — `hasMore` is `true` when another
|
|
190
|
+
* page exists (i.e. the DO returned `limit + 1` rows).
|
|
191
|
+
*/
|
|
191
192
|
getRange(fromSeq: number, limit?: number): Promise<{
|
|
192
193
|
entries: EventLogEntry[];
|
|
193
194
|
hasMore: boolean;
|
|
194
195
|
}>;
|
|
195
196
|
/**
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
* Return the total number of entries currently in the log.
|
|
198
|
+
*/
|
|
198
199
|
getSize(): Promise<number>;
|
|
199
200
|
/**
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
* Return the full log state — all entries plus the next seq number.
|
|
202
|
+
*/
|
|
202
203
|
getState(): Promise<{
|
|
203
204
|
entries: EventLogEntry[];
|
|
204
205
|
nextSeq: number;
|
|
205
206
|
}>;
|
|
206
207
|
}
|
|
207
208
|
/**
|
|
208
|
-
* Type-safe event emitter that powers the event-sourcing runtime.
|
|
209
|
-
* @example
|
|
210
|
-
* ```ts
|
|
211
|
-
* type MyEvents = { userCreated: { id: string; name: string }; error: { message: string } };
|
|
212
|
-
*
|
|
213
|
-
* const emitter = new EventEmitter<MyEvents>();
|
|
214
|
-
* emitter.on("userCreated", (payload) => console.log(payload.name));
|
|
215
|
-
* emitter.emit("userCreated", { id: "1", name: "alice" });
|
|
216
|
-
* ```
|
|
217
|
-
* @experimental
|
|
218
|
-
*/
|
|
209
|
+
* Type-safe event emitter that powers the event-sourcing runtime.
|
|
210
|
+
* @example
|
|
211
|
+
* ```ts
|
|
212
|
+
* type MyEvents = { userCreated: { id: string; name: string }; error: { message: string } };
|
|
213
|
+
*
|
|
214
|
+
* const emitter = new EventEmitter<MyEvents>();
|
|
215
|
+
* emitter.on("userCreated", (payload) => console.log(payload.name));
|
|
216
|
+
* emitter.emit("userCreated", { id: "1", name: "alice" });
|
|
217
|
+
* ```
|
|
218
|
+
* @experimental
|
|
219
|
+
*/
|
|
219
220
|
declare class EventEmitter<EventMap extends Record<string, unknown>> {
|
|
220
221
|
#private;
|
|
221
222
|
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
* Register a handler for a specific event type.
|
|
224
|
+
* @returns An unsubscribe function (equivalent to calling {@link off}).
|
|
225
|
+
*/
|
|
225
226
|
on<K extends keyof EventMap>(event: K, handler: (payload: EventMap[K]) => void): () => void;
|
|
226
227
|
/**
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
* Remove a previously registered handler.
|
|
229
|
+
*/
|
|
229
230
|
off<K extends keyof EventMap>(event: K, handler: (payload: EventMap[K]) => void): void;
|
|
230
231
|
/**
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
* Register a wildcard handler that fires for **every** event type.
|
|
233
|
+
* @returns An unsubscribe function.
|
|
234
|
+
*/
|
|
234
235
|
onAny(handler: (event: keyof EventMap, payload: unknown) => void): () => void;
|
|
235
236
|
/**
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
* Remove a wildcard handler.
|
|
238
|
+
*/
|
|
238
239
|
offAny(handler: (event: keyof EventMap, payload: unknown) => void): void;
|
|
239
240
|
/**
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
241
|
+
* Emit an event. All registered handlers (typed + wildcard) are invoked
|
|
242
|
+
* synchronously. Exceptions from handlers are caught and silently
|
|
243
|
+
* swallowed — they **must not** break the emitter loop.
|
|
244
|
+
* @returns `true` if at least one handler was called.
|
|
245
|
+
*/
|
|
245
246
|
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): boolean;
|
|
246
247
|
/**
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
* Return `true` when at least one listener is registered for `event`.
|
|
249
|
+
*/
|
|
249
250
|
hasListeners(event: keyof EventMap): boolean;
|
|
250
251
|
/**
|
|
251
|
-
|
|
252
|
-
|
|
252
|
+
* Return the number of typed listeners for a specific event.
|
|
253
|
+
*/
|
|
253
254
|
listenerCount(event: keyof EventMap): number;
|
|
254
255
|
/**
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
* Remove all listeners.
|
|
257
|
+
*/
|
|
257
258
|
clear(): void;
|
|
258
259
|
}
|
|
259
260
|
/**
|
|
260
|
-
* Strategy for handling events whose `type` the reducer does not recognise.
|
|
261
|
-
*
|
|
262
|
-
* - `"warn"` _(default)_ — log a warning and skip the event (state unchanged).
|
|
263
|
-
* - `"ignore"` — skip silently (no warning, no error).
|
|
264
|
-
* - `"fail"` — throw an error, halting the apply / replay cycle.
|
|
265
|
-
* - A **callback** — invoked with the entry; return truthy to mark it as
|
|
266
|
-
* handled (no warning), falsy to fall through to the configured fallback.
|
|
267
|
-
* @experimental
|
|
268
|
-
*/
|
|
261
|
+
* Strategy for handling events whose `type` the reducer does not recognise.
|
|
262
|
+
*
|
|
263
|
+
* - `"warn"` _(default)_ — log a warning and skip the event (state unchanged).
|
|
264
|
+
* - `"ignore"` — skip silently (no warning, no error).
|
|
265
|
+
* - `"fail"` — throw an error, halting the apply / replay cycle.
|
|
266
|
+
* - A **callback** — invoked with the entry; return truthy to mark it as
|
|
267
|
+
* handled (no warning), falsy to fall through to the configured fallback.
|
|
268
|
+
* @experimental
|
|
269
|
+
*/
|
|
269
270
|
type UnknownEventHandling = "warn" | "ignore" | "fail" | ((entry: EventLogEntry) => boolean);
|
|
270
271
|
/**
|
|
271
|
-
* Events emitted by the {@link EventSource} runtime.
|
|
272
|
-
*
|
|
273
|
-
* A `type` (not `interface`) so it satisfies `EventEmitter`'s
|
|
274
|
-
* `Record
|
|
275
|
-
* signature and aren't assignable to `Record
|
|
276
|
-
* @experimental
|
|
277
|
-
*/
|
|
272
|
+
* Events emitted by the {@link EventSource} runtime.
|
|
273
|
+
*
|
|
274
|
+
* A `type` (not `interface`) so it satisfies `EventEmitter`'s
|
|
275
|
+
* `Record<string, unknown>` constraint — interfaces have no implicit index
|
|
276
|
+
* signature and aren't assignable to `Record<string, unknown>`.
|
|
277
|
+
* @experimental
|
|
278
|
+
*/
|
|
278
279
|
type EventSourceEvents = {
|
|
279
280
|
/** Fired (once) after the initial replay completes. */
|
|
280
281
|
ready: {
|
|
@@ -292,143 +293,145 @@ type EventSourceEvents = {
|
|
|
292
293
|
};
|
|
293
294
|
};
|
|
294
295
|
/**
|
|
295
|
-
* Sentinel a reducer can return to EXPLICITLY signal it does not handle a
|
|
296
|
-
* given event's `type` — as opposed to returning the current `state`
|
|
297
|
-
* reference unchanged to represent a legitimate, idempotent no-op for a type
|
|
298
|
-
* it DOES recognise.
|
|
299
|
-
*
|
|
300
|
-
* Reference equality alone can't tell these two cases apart (REPLICA-07): a
|
|
301
|
-
* reducer that intentionally returns `state` for a type it fully understands
|
|
302
|
-
* (e.g. "already applied this event, nothing to do") would otherwise be
|
|
303
|
-
* misclassified as "unhandled" and trigger {@link UnknownEventHandling} — a
|
|
304
|
-
* spurious warning, or worse, a thrown error under `"fail"`. Return `UNHANDLED`
|
|
305
|
-
* only for a `type` your reducer truly does not recognise; every other return
|
|
306
|
-
* (including a `state` returned by reference) is treated as handled.
|
|
307
|
-
*
|
|
308
|
-
* Reducers that always recognise every event they're given (a single
|
|
309
|
-
* always-matching type, or a catch-all) can ignore this entirely.
|
|
310
|
-
* @experimental
|
|
311
|
-
*/
|
|
296
|
+
* Sentinel a reducer can return to EXPLICITLY signal it does not handle a
|
|
297
|
+
* given event's `type` — as opposed to returning the current `state`
|
|
298
|
+
* reference unchanged to represent a legitimate, idempotent no-op for a type
|
|
299
|
+
* it DOES recognise.
|
|
300
|
+
*
|
|
301
|
+
* Reference equality alone can't tell these two cases apart (REPLICA-07): a
|
|
302
|
+
* reducer that intentionally returns `state` for a type it fully understands
|
|
303
|
+
* (e.g. "already applied this event, nothing to do") would otherwise be
|
|
304
|
+
* misclassified as "unhandled" and trigger {@link UnknownEventHandling} — a
|
|
305
|
+
* spurious warning, or worse, a thrown error under `"fail"`. Return `UNHANDLED`
|
|
306
|
+
* only for a `type` your reducer truly does not recognise; every other return
|
|
307
|
+
* (including a `state` returned by reference) is treated as handled.
|
|
308
|
+
*
|
|
309
|
+
* Reducers that always recognise every event they're given (a single
|
|
310
|
+
* always-matching type, or a catch-all) can ignore this entirely.
|
|
311
|
+
* @experimental
|
|
312
|
+
*/
|
|
312
313
|
declare const UNHANDLED: unique symbol;
|
|
313
314
|
/**
|
|
314
|
-
* A function that reduces an event into a state mutation.
|
|
315
|
-
*
|
|
316
|
-
* Pure functions are strongly encouraged: given the same event payload
|
|
317
|
-
* and state, they must produce the same next state. Return {@link UNHANDLED}
|
|
318
|
-
* to explicitly mark an event `type` this reducer does not process — see
|
|
319
|
-
* {@link UNHANDLED} for why reference equality against the input `state`
|
|
320
|
-
* cannot be used for this instead.
|
|
321
|
-
* @experimental
|
|
322
|
-
*/
|
|
315
|
+
* A function that reduces an event into a state mutation.
|
|
316
|
+
*
|
|
317
|
+
* Pure functions are strongly encouraged: given the same event payload
|
|
318
|
+
* and state, they must produce the same next state. Return {@link UNHANDLED}
|
|
319
|
+
* to explicitly mark an event `type` this reducer does not process — see
|
|
320
|
+
* {@link UNHANDLED} for why reference equality against the input `state`
|
|
321
|
+
* cannot be used for this instead.
|
|
322
|
+
* @experimental
|
|
323
|
+
*/
|
|
323
324
|
type EventReducer<S> = (state: S, entry: EventLogEntry) => S | typeof UNHANDLED;
|
|
324
325
|
/**
|
|
325
|
-
* Options for constructing an {@link EventSource}.
|
|
326
|
-
* @experimental
|
|
327
|
-
*/
|
|
326
|
+
* Options for constructing an {@link EventSource}.
|
|
327
|
+
* @experimental
|
|
328
|
+
*/
|
|
328
329
|
interface EventSourceOptions {
|
|
329
330
|
/**
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
331
|
+
* Cap this runtime's internal `log` to this many entries (REPLICA-06).
|
|
332
|
+
* `replayFromLog` copies every entry it replays from the source log into
|
|
333
|
+
* `this.log` too — a second, uncapped copy of the same history — so a
|
|
334
|
+
* long-lived `EventSource` fed by repeated replay accumulates entries in
|
|
335
|
+
* both places forever without a cap.
|
|
336
|
+
*
|
|
337
|
+
* `undefined` (the default) preserves unbounded retention.
|
|
338
|
+
*/
|
|
338
339
|
maxLogEntries?: number;
|
|
339
340
|
/**
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
341
|
+
* How to handle events whose `type` is not recognised by the reducer.
|
|
342
|
+
* @default "warn"
|
|
343
|
+
*/
|
|
343
344
|
unknownEventHandling?: UnknownEventHandling;
|
|
344
345
|
}
|
|
345
346
|
/**
|
|
346
|
-
* Event-sourcing runtime that maintains a derived state by replaying an
|
|
347
|
-
* append-only {@link EventLog}.
|
|
348
|
-
*
|
|
349
|
-
* Usage:
|
|
350
|
-
* ```ts
|
|
351
|
-
* const source = new EventSource(initialState, myReducer);
|
|
352
|
-
* await source.replayFromLog(existingLog);
|
|
353
|
-
*
|
|
354
|
-
* // Later, when a new event arrives:
|
|
355
|
-
* const entry = source.applyEvent("user-created", { id: "1", name: "alice" });
|
|
356
|
-
* console.log(source.state); // updated state
|
|
357
|
-
* ```
|
|
358
|
-
* @experimental
|
|
359
|
-
*/
|
|
347
|
+
* Event-sourcing runtime that maintains a derived state by replaying an
|
|
348
|
+
* append-only {@link EventLog}.
|
|
349
|
+
*
|
|
350
|
+
* Usage:
|
|
351
|
+
* ```ts
|
|
352
|
+
* const source = new EventSource(initialState, myReducer);
|
|
353
|
+
* await source.replayFromLog(existingLog);
|
|
354
|
+
*
|
|
355
|
+
* // Later, when a new event arrives:
|
|
356
|
+
* const entry = source.applyEvent("user-created", { id: "1", name: "alice" });
|
|
357
|
+
* console.log(source.state); // updated state
|
|
358
|
+
* ```
|
|
359
|
+
* @experimental
|
|
360
|
+
*/
|
|
360
361
|
declare class EventSource<S extends Record<string, unknown> = Record<string, unknown>> {
|
|
361
362
|
#private;
|
|
362
363
|
readonly emitter: EventEmitter<EventSourceEvents>;
|
|
363
364
|
readonly log: EventLog;
|
|
364
365
|
constructor(initialState: S, reducer: EventReducer<S>, options?: EventSourceOptions);
|
|
365
366
|
/**
|
|
366
|
-
|
|
367
|
-
|
|
367
|
+
* The current derived state. Read-only snapshot; mutate through events.
|
|
368
|
+
*/
|
|
368
369
|
get state(): Readonly<S>;
|
|
369
370
|
/**
|
|
370
|
-
|
|
371
|
-
|
|
371
|
+
* Whether the initial replay from an existing log has completed.
|
|
372
|
+
*/
|
|
372
373
|
get replayed(): boolean;
|
|
373
374
|
/**
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
375
|
+
* Append a new event to the log and apply it to the current state.
|
|
376
|
+
*
|
|
377
|
+
* Accepts either an {@link InputEvent} (e.g. from a `defineEvents` factory)
|
|
378
|
+
* or the traditional `(type, payload)` pair.
|
|
379
|
+
* @returns The newly created log entry (with its assigned `seq`).
|
|
380
|
+
*/
|
|
380
381
|
applyEvent(event: InputEvent, options?: AppendOptions): EventLogEntry;
|
|
381
382
|
applyEvent(type: string, payload: unknown, options?: AppendOptions): EventLogEntry;
|
|
382
383
|
/**
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
384
|
+
* Replay all entries from an existing {@link EventLog} to bootstrap
|
|
385
|
+
* the current state.
|
|
386
|
+
*
|
|
387
|
+
* Idempotent across calls: only source entries past the `#lastAppliedSeq`
|
|
388
|
+
* watermark are applied, so re-invoking picks up just the new entries.
|
|
389
|
+
* @param log The external log to replay from.
|
|
390
|
+
*/
|
|
390
391
|
replayFromLog(log: EventLog): void;
|
|
391
392
|
/**
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
393
|
+
* Reset the runtime to a base state, optionally resuming from a watermark.
|
|
394
|
+
*
|
|
395
|
+
* Useful after loading a snapshot from the DO: pass the snapshot's state as
|
|
396
|
+
* `initialState` and its highest applied source `seq` as `resumeFromSeq`, so
|
|
397
|
+
* the next {@link replayFromLog} applies ONLY the events after the snapshot
|
|
398
|
+
* (`getSince(resumeFromSeq + 1)`) rather than replaying the whole log on top
|
|
399
|
+
* of the snapshot — which would double-apply non-idempotent reducers.
|
|
400
|
+
*
|
|
401
|
+
* Omit `resumeFromSeq` (default `-1`) for a full reset that replays from the
|
|
402
|
+
* beginning.
|
|
403
|
+
* @param initialState The base state to reset to (e.g. a loaded snapshot).
|
|
404
|
+
* @param resumeFromSeq Highest source `seq` already baked into `initialState`, or `-1` to replay all.
|
|
405
|
+
*/
|
|
405
406
|
reset(initialState: S, resumeFromSeq?: number): void;
|
|
406
407
|
/**
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
408
|
+
* Return an async generator that yields every event as it is applied,
|
|
409
|
+
* starting from the events currently in the log and continuing with
|
|
410
|
+
* every future `applyEvent` / `replayFromLog` call.
|
|
411
|
+
*
|
|
412
|
+
* The generator runs indefinitely unless given a `signal` — callers
|
|
413
|
+
* should break out of the `for await` loop or pass an `AbortSignal` to
|
|
414
|
+
* stop it (an abort settles the generator, `done: true`, on its next
|
|
415
|
+
* iteration step; it does not throw).
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* for await (const entry of source.events()) {
|
|
419
|
+
* console.log("event applied:", entry);
|
|
420
|
+
* }
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
420
423
|
events(signal?: AbortSignal): AsyncGenerator<EventLogEntry>;
|
|
421
424
|
}
|
|
422
425
|
/**
|
|
423
|
-
* Interface for persisting event-sourced state snapshots.
|
|
424
|
-
*
|
|
425
|
-
* In a Lunora app the primary implementation is backed by the
|
|
426
|
-
* SnapshotDO (a Durable Object) on the server side. On the client
|
|
427
|
-
* the {@link InMemorySnapshotStore} is used for the offline-first
|
|
428
|
-
* local mirror, while a production client would implement this
|
|
429
|
-
* over IndexedDB or OPFS.
|
|
430
|
-
* @experimental
|
|
431
|
-
*/
|
|
426
|
+
* Interface for persisting event-sourced state snapshots.
|
|
427
|
+
*
|
|
428
|
+
* In a Lunora app the primary implementation is backed by the
|
|
429
|
+
* SnapshotDO (a Durable Object) on the server side. On the client
|
|
430
|
+
* the {@link InMemorySnapshotStore} is used for the offline-first
|
|
431
|
+
* local mirror, while a production client would implement this
|
|
432
|
+
* over IndexedDB or OPFS.
|
|
433
|
+
* @experimental
|
|
434
|
+
*/
|
|
432
435
|
interface SnapshotStore {
|
|
433
436
|
/** Delete all snapshots. */
|
|
434
437
|
clear: () => Promise<void>;
|
|
@@ -442,11 +445,11 @@ interface SnapshotStore {
|
|
|
442
445
|
save: (key: string, snapshot: unknown) => Promise<void>;
|
|
443
446
|
}
|
|
444
447
|
/**
|
|
445
|
-
* In-memory snapshot store. Useful for testing and for the local
|
|
446
|
-
* offline-first mirror where persistence is handled at a higher
|
|
447
|
-
* layer (IndexedDB adapter).
|
|
448
|
-
* @experimental
|
|
449
|
-
*/
|
|
448
|
+
* In-memory snapshot store. Useful for testing and for the local
|
|
449
|
+
* offline-first mirror where persistence is handled at a higher
|
|
450
|
+
* layer (IndexedDB adapter).
|
|
451
|
+
* @experimental
|
|
452
|
+
*/
|
|
450
453
|
declare class InMemorySnapshotStore implements SnapshotStore {
|
|
451
454
|
#private;
|
|
452
455
|
save(key: string, snapshot: unknown): Promise<void>;
|
|
@@ -456,23 +459,23 @@ declare class InMemorySnapshotStore implements SnapshotStore {
|
|
|
456
459
|
clear(): Promise<void>;
|
|
457
460
|
}
|
|
458
461
|
/**
|
|
459
|
-
* A function that reduces an event entry into a state mutation.
|
|
460
|
-
*
|
|
461
|
-
* Pure functions are strongly encouraged: given the same event and state,
|
|
462
|
-
* they must produce the same next state for deterministic replay.
|
|
463
|
-
* @experimental
|
|
464
|
-
*/
|
|
462
|
+
* A function that reduces an event entry into a state mutation.
|
|
463
|
+
*
|
|
464
|
+
* Pure functions are strongly encouraged: given the same event and state,
|
|
465
|
+
* they must produce the same next state for deterministic replay.
|
|
466
|
+
* @experimental
|
|
467
|
+
*/
|
|
465
468
|
type MaterializerReducer<S> = (state: S, entry: EventLogEntry) => S;
|
|
466
469
|
/**
|
|
467
|
-
* Options for defining a single materializer.
|
|
468
|
-
* @experimental
|
|
469
|
-
*/
|
|
470
|
+
* Options for defining a single materializer.
|
|
471
|
+
* @experimental
|
|
472
|
+
*/
|
|
470
473
|
interface MaterializerDef<S> {
|
|
471
474
|
/**
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
475
|
+
* Reducer invoked for every event in the log.
|
|
476
|
+
*
|
|
477
|
+
* Return the current state unchanged to skip the event.
|
|
478
|
+
*/
|
|
476
479
|
handle: MaterializerReducer<S>;
|
|
477
480
|
/** Factory for the initial (empty) state. */
|
|
478
481
|
initial: () => S;
|
|
@@ -480,9 +483,9 @@ interface MaterializerDef<S> {
|
|
|
480
483
|
readonly name: string;
|
|
481
484
|
}
|
|
482
485
|
/**
|
|
483
|
-
* A constructed materializer ready to be used with a {@link MaterializerRuntime}.
|
|
484
|
-
* @experimental
|
|
485
|
-
*/
|
|
486
|
+
* A constructed materializer ready to be used with a {@link MaterializerRuntime}.
|
|
487
|
+
* @experimental
|
|
488
|
+
*/
|
|
486
489
|
interface Materializer<S> {
|
|
487
490
|
/** Apply a single event entry through the reducer. */
|
|
488
491
|
apply: (entry: EventLogEntry) => void;
|
|
@@ -495,143 +498,143 @@ interface Materializer<S> {
|
|
|
495
498
|
readonly state: Readonly<S>;
|
|
496
499
|
}
|
|
497
500
|
/**
|
|
498
|
-
* Declare a materializer — a named reducer that derives state from events.
|
|
499
|
-
*
|
|
500
|
-
* The returned {@link Materializer} object can be used standalone or passed
|
|
501
|
-
* to a {@link MaterializerRuntime} for automatic log subscription.
|
|
502
|
-
* @experimental
|
|
503
|
-
*/
|
|
501
|
+
* Declare a materializer — a named reducer that derives state from events.
|
|
502
|
+
*
|
|
503
|
+
* The returned {@link Materializer} object can be used standalone or passed
|
|
504
|
+
* to a {@link MaterializerRuntime} for automatic log subscription.
|
|
505
|
+
* @experimental
|
|
506
|
+
*/
|
|
504
507
|
declare const defineMaterializer: <S>(definition: MaterializerDef<S>) => Materializer<S>;
|
|
505
508
|
/**
|
|
506
|
-
* A materializer of any state shape. The {@link MaterializerRuntime} holds a
|
|
507
|
-
* heterogeneous collection and only ever calls `apply(entry)` / `setState(...)`
|
|
508
|
-
* (with cast values) / reads `def.name` — it never needs the concrete state
|
|
509
|
-
* type. `Materializer
|
|
510
|
-
* `Materializer
|
|
511
|
-
* to `Materializer
|
|
512
|
-
*/
|
|
509
|
+
* A materializer of any state shape. The {@link MaterializerRuntime} holds a
|
|
510
|
+
* heterogeneous collection and only ever calls `apply(entry)` / `setState(...)`
|
|
511
|
+
* (with cast values) / reads `def.name` — it never needs the concrete state
|
|
512
|
+
* type. `Materializer<unknown>` won't do: `setState(state: S)` makes
|
|
513
|
+
* `Materializer<S>` invariant in `S`, so `Materializer<number>` isn't assignable
|
|
514
|
+
* to `Materializer<unknown>`. Erasing the type param is the idiomatic fix.
|
|
515
|
+
*/
|
|
513
516
|
type AnyMaterializer = Materializer<any>;
|
|
514
517
|
/**
|
|
515
|
-
* Options for constructing a {@link MaterializerRuntime}.
|
|
516
|
-
* @experimental
|
|
517
|
-
*/
|
|
518
|
+
* Options for constructing a {@link MaterializerRuntime}.
|
|
519
|
+
* @experimental
|
|
520
|
+
*/
|
|
518
521
|
interface MaterializerRuntimeOptions {
|
|
519
522
|
/**
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
523
|
+
* Optional EventLogDO client for persistent event log integration.
|
|
524
|
+
*
|
|
525
|
+
* When provided, the runtime can bootstrap from the DO on startup
|
|
526
|
+
* (recover from snapshots → catch up via `getSince`) and append
|
|
527
|
+
* new events through the DO automatically.
|
|
528
|
+
*/
|
|
526
529
|
doClient?: EventLogDOClient;
|
|
527
530
|
/** Optional snapshot store for persisting/recovering materialized state. */
|
|
528
531
|
snapshotStore?: SnapshotStore;
|
|
529
532
|
/**
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
+
* How to handle events whose type no materializer handles.
|
|
534
|
+
* @default "warn"
|
|
535
|
+
*/
|
|
533
536
|
unknownEventHandling?: UnknownEventHandling;
|
|
534
537
|
}
|
|
535
538
|
/**
|
|
536
|
-
* Runtime that drives one or more materializers from an event log.
|
|
537
|
-
*
|
|
538
|
-
* Handles:
|
|
539
|
-
* - Replaying the full log on startup
|
|
540
|
-
* - Applying new events as they arrive
|
|
541
|
-
* - Periodic snapshot persistence
|
|
542
|
-
* - Recovery from snapshots (replay only what's missing)
|
|
543
|
-
* @experimental
|
|
544
|
-
*/
|
|
539
|
+
* Runtime that drives one or more materializers from an event log.
|
|
540
|
+
*
|
|
541
|
+
* Handles:
|
|
542
|
+
* - Replaying the full log on startup
|
|
543
|
+
* - Applying new events as they arrive
|
|
544
|
+
* - Periodic snapshot persistence
|
|
545
|
+
* - Recovery from snapshots (replay only what's missing)
|
|
546
|
+
* @experimental
|
|
547
|
+
*/
|
|
545
548
|
declare class MaterializerRuntime {
|
|
546
549
|
#private;
|
|
547
550
|
constructor(materializers: AnyMaterializer[], options?: MaterializerRuntimeOptions);
|
|
548
551
|
/**
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
552
|
+
* The lowest per-materializer watermark — the seq of the next event that
|
|
553
|
+
* at least one materializer has not yet applied. `0` when there are no
|
|
554
|
+
* materializers.
|
|
555
|
+
*/
|
|
553
556
|
get appliedSeq(): number;
|
|
554
557
|
/**
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
558
|
+
* Replay a batch of entries, applying each entry only to the
|
|
559
|
+
* materializers whose own watermark is behind it — a materializer at or
|
|
560
|
+
* past an entry's seq (e.g. recovered from a snapshot, or already caught
|
|
561
|
+
* up) skips it, so no materializer ever double-applies an event.
|
|
562
|
+
* @returns The number of entries applied to at least one materializer.
|
|
563
|
+
*/
|
|
561
564
|
applyEntries(entries: ReadonlyArray<EventLogEntry>): number;
|
|
562
565
|
/**
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
566
|
+
* Attempt to recover materialized state from a snapshot store.
|
|
567
|
+
*
|
|
568
|
+
* When a snapshot is found for a materializer, its state AND its own
|
|
569
|
+
* watermark are restored from that snapshot. A materializer with no
|
|
570
|
+
* snapshot keeps its current watermark (`0` for a fresh runtime) — it
|
|
571
|
+
* does NOT inherit another materializer's watermark, so it still catches
|
|
572
|
+
* up from the very beginning (REPLICA-04: previously a shared watermark
|
|
573
|
+
* was bumped to the MAX across snapshots, permanently skipping events 0..N
|
|
574
|
+
* for any un-snapshotted or lagging materializer).
|
|
575
|
+
* @returns The highest snapshot `appliedSeq` across all materializers, or
|
|
576
|
+
* `0` — kept for backward compatibility; callers that need the fetch
|
|
577
|
+
* watermark for catch-up should use the per-materializer minimum instead
|
|
578
|
+
* (see `initialize`).
|
|
579
|
+
*/
|
|
577
580
|
recoverFromSnapshots(): Promise<number>;
|
|
578
581
|
/**
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
+
* Persist the current state of all materializers as snapshots, each
|
|
583
|
+
* tagged with ITS OWN watermark (not a shared one).
|
|
584
|
+
*/
|
|
582
585
|
persistSnapshots(): Promise<void>;
|
|
583
586
|
/**
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
587
|
+
* Bootstrap the runtime from the EventLogDO.
|
|
588
|
+
*
|
|
589
|
+
* 1. Recover materialized state from snapshots (if a snapshotStore is
|
|
590
|
+
* configured).
|
|
591
|
+
* 2. Fetch all entries since the MINIMUM per-materializer watermark from
|
|
592
|
+
* the DO — not the maximum — so a materializer with no snapshot (or a
|
|
593
|
+
* lower one) still receives every event it hasn't seen (REPLICA-04).
|
|
594
|
+
* 3. Apply them through the materializers; `applyEntries` skips each
|
|
595
|
+
* entry for any materializer already past it, so nothing is double-applied.
|
|
596
|
+
*
|
|
597
|
+
* Call this once on startup / after the DO binding is available.
|
|
598
|
+
* @returns The number of entries applied during catch-up.
|
|
599
|
+
*/
|
|
597
600
|
initialize(): Promise<number>;
|
|
598
601
|
/**
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
602
|
+
* Append an event to the EventLogDO and apply it through all
|
|
603
|
+
* materializers.
|
|
604
|
+
*
|
|
605
|
+
* This is a convenience over calling `doClient.append(...)` +
|
|
606
|
+
* `runtime.applyEntries(...)` yourself — it persists the event
|
|
607
|
+
* **then** applies the returned entry (with its assigned seq).
|
|
608
|
+
* @returns The persisted entry with its DO-assigned `seq`.
|
|
609
|
+
*/
|
|
607
610
|
appendEvent(input: AppendEventInput): Promise<EventLogEntry>;
|
|
608
611
|
/**
|
|
609
|
-
|
|
610
|
-
|
|
612
|
+
* Reset all materializers to their initial state and clear snapshots.
|
|
613
|
+
*/
|
|
611
614
|
reset(): void;
|
|
612
615
|
/**
|
|
613
|
-
|
|
614
|
-
|
|
616
|
+
* The list of registered materializers.
|
|
617
|
+
*/
|
|
615
618
|
get materializers(): ReadonlyArray<Materializer<unknown>>;
|
|
616
619
|
}
|
|
617
620
|
/**
|
|
618
|
-
* Apply a {@link TableDiff} to the given SQLite database by translating
|
|
619
|
-
* each row change into an INSERT, UPDATE, or DELETE statement.
|
|
620
|
-
*
|
|
621
|
-
* All statements are wrapped in a single transaction.
|
|
622
|
-
* @param database SQLite adapter the statements run against.
|
|
623
|
-
* @param diff The table diff to apply.
|
|
624
|
-
* @param pkColumn Primary key column for DELETE/UPDATE (default `"id"`).
|
|
625
|
-
* @experimental
|
|
626
|
-
*/
|
|
621
|
+
* Apply a {@link TableDiff} to the given SQLite database by translating
|
|
622
|
+
* each row change into an INSERT, UPDATE, or DELETE statement.
|
|
623
|
+
*
|
|
624
|
+
* All statements are wrapped in a single transaction.
|
|
625
|
+
* @param database SQLite adapter the statements run against.
|
|
626
|
+
* @param diff The table diff to apply.
|
|
627
|
+
* @param pkColumn Primary key column for DELETE/UPDATE (default `"id"`).
|
|
628
|
+
* @experimental
|
|
629
|
+
*/
|
|
627
630
|
declare const applyDiffToDatabase: (database: SqliteAdapter, diff: TableDiff, pkColumn?: string) => void;
|
|
628
631
|
/**
|
|
629
|
-
* Apply multiple diffs **in order** within a single transaction.
|
|
630
|
-
*
|
|
631
|
-
* Each diff uses `"id"` as the primary key column. For tables with a custom
|
|
632
|
-
* PK, use {@link applyDiffToDatabase} per-diff and pass the PK explicitly.
|
|
633
|
-
* @experimental
|
|
634
|
-
*/
|
|
632
|
+
* Apply multiple diffs **in order** within a single transaction.
|
|
633
|
+
*
|
|
634
|
+
* Each diff uses `"id"` as the primary key column. For tables with a custom
|
|
635
|
+
* PK, use {@link applyDiffToDatabase} per-diff and pass the PK explicitly.
|
|
636
|
+
* @experimental
|
|
637
|
+
*/
|
|
635
638
|
declare const applyDiffsToDatabase: (database: SqliteAdapter, diffs: ReadonlyArray<TableDiff>) => void;
|
|
636
639
|
interface EventLogDOState {
|
|
637
640
|
storage: {
|
|
@@ -639,18 +642,18 @@ interface EventLogDOState {
|
|
|
639
642
|
exec: (query: string, ...params: unknown[]) => unknown;
|
|
640
643
|
};
|
|
641
644
|
/**
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
645
|
+
* The DO platform's native atomic-transaction primitive (async;
|
|
646
|
+
* commits on resolve, rolls back on throw/reject). Test doubles that
|
|
647
|
+
* omit it fall back to a bare (non-transactional) call — see
|
|
648
|
+
* `#handleAppend`.
|
|
649
|
+
*/
|
|
647
650
|
transaction?: <T>(closure: () => Promise<T> | T) => Promise<T>;
|
|
648
651
|
};
|
|
649
652
|
}
|
|
650
653
|
/**
|
|
651
|
-
* `EventLogDO` is part of the experimental `@lunora/replica` API and may change without a major version bump.
|
|
652
|
-
* @experimental
|
|
653
|
-
*/
|
|
654
|
+
* `EventLogDO` is part of the experimental `@lunora/replica` API and may change without a major version bump.
|
|
655
|
+
* @experimental
|
|
656
|
+
*/
|
|
654
657
|
declare class EventLogDO {
|
|
655
658
|
#private;
|
|
656
659
|
protected state: EventLogDOState;
|
|
@@ -659,10 +662,10 @@ declare class EventLogDO {
|
|
|
659
662
|
fetch(request: Request): Promise<Response>;
|
|
660
663
|
}
|
|
661
664
|
/**
|
|
662
|
-
* `next()` advances the middleware chain. Called with no argument it forwards
|
|
663
|
-
* the current context unchanged; called with `{ ctx }` it shallow-merges the
|
|
664
|
-
* extension, and the result type reflects the widened context.
|
|
665
|
-
*/
|
|
665
|
+
* `next()` advances the middleware chain. Called with no argument it forwards
|
|
666
|
+
* the current context unchanged; called with `{ ctx }` it shallow-merges the
|
|
667
|
+
* extension, and the result type reflects the widened context.
|
|
668
|
+
*/
|
|
666
669
|
interface MiddlewareNext<ContextIn> {
|
|
667
670
|
(): Promise<ContextIn>;
|
|
668
671
|
<Extension extends Record<string, unknown>>(options: {
|
|
@@ -670,46 +673,45 @@ interface MiddlewareNext<ContextIn> {
|
|
|
670
673
|
}): Promise<ContextIn & Extension>;
|
|
671
674
|
}
|
|
672
675
|
/**
|
|
673
|
-
* A middleware receives the current context and a `next` continuation. Its
|
|
674
|
-
* return type becomes the builder's new context, so `return next({ ctx })`
|
|
675
|
-
* propagates the extension into every downstream `.use()` and the handler.
|
|
676
|
-
*/
|
|
676
|
+
* A middleware receives the current context and a `next` continuation. Its
|
|
677
|
+
* return type becomes the builder's new context, so `return next({ ctx })`
|
|
678
|
+
* propagates the extension into every downstream `.use()` and the handler.
|
|
679
|
+
*/
|
|
677
680
|
type Middleware<ContextIn, ContextOut> = (options: {
|
|
678
681
|
ctx: ContextIn;
|
|
679
682
|
next: MiddlewareNext<ContextIn>;
|
|
680
683
|
}) => ContextOut | Promise<ContextOut>;
|
|
681
|
-
/**
|
|
682
|
-
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
|
|
688
|
-
*/
|
|
684
|
+
/**
|
|
685
|
+
* The per-request `ctx.events` facade that {@link eventsContext} attaches.
|
|
686
|
+
*
|
|
687
|
+
* Each method delegates to the corresponding {@link EventLogDOClient} method,
|
|
688
|
+
* so handlers never need to import or reference the DO client directly.
|
|
689
|
+
* @experimental
|
|
690
|
+
*/
|
|
689
691
|
interface EventsFacade {
|
|
690
692
|
/**
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
693
|
+
* Append one or more events to the log.
|
|
694
|
+
* @returns The persisted entries with their assigned `seq` numbers.
|
|
695
|
+
*/
|
|
694
696
|
append: (events: {
|
|
695
697
|
payload: unknown;
|
|
696
698
|
timestamp?: number;
|
|
697
699
|
type: string;
|
|
698
700
|
}[]) => Promise<EventLogEntry[]>;
|
|
699
701
|
/**
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
702
|
+
* Fetch a paginated range of entries.
|
|
703
|
+
* @returns `{ entries, hasMore }` — `hasMore` is `true` when another
|
|
704
|
+
* page exists.
|
|
705
|
+
*/
|
|
704
706
|
getRange: (fromSeq: number, limit?: number) => Promise<{
|
|
705
707
|
entries: EventLogEntry[];
|
|
706
708
|
hasMore: boolean;
|
|
707
709
|
}>;
|
|
708
710
|
/**
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
711
|
+
* Fetch all entries with `seq >= sinceSeq`.
|
|
712
|
+
*
|
|
713
|
+
* Pass `sinceSeq = 0` to fetch the entire log.
|
|
714
|
+
*/
|
|
713
715
|
getSince: (sinceSeq: number) => Promise<EventLogEntry[]>;
|
|
714
716
|
/** Return the total number of entries currently in the log. */
|
|
715
717
|
getSize: () => Promise<number>;
|
|
@@ -720,47 +722,47 @@ interface EventsFacade {
|
|
|
720
722
|
}>;
|
|
721
723
|
}
|
|
722
724
|
/**
|
|
723
|
-
* The context shape produced by {@link eventsContext}.
|
|
724
|
-
* @experimental
|
|
725
|
-
*/
|
|
725
|
+
* The context shape produced by {@link eventsContext}.
|
|
726
|
+
* @experimental
|
|
727
|
+
*/
|
|
726
728
|
interface EventsContextOutput {
|
|
727
729
|
/** Typed event log facade backed by an {@link EventLogDOClient}. */
|
|
728
730
|
readonly events: EventsFacade;
|
|
729
731
|
}
|
|
730
732
|
/**
|
|
731
|
-
* Create a middleware that attaches a typed `ctx.events` facade backed by
|
|
732
|
-
* the given {@link EventLogDOClient}.
|
|
733
|
-
*
|
|
734
|
-
* The facade surfaces `append`, `getSince`, `getRange`, `getSize`, and
|
|
735
|
-
* `getState` — every method the DO client exposes — so handlers can read
|
|
736
|
-
* and write the event log without reaching for the DO stub directly.
|
|
737
|
-
*
|
|
738
|
-
* The middleware is unopinionated about which context it extends — it works
|
|
739
|
-
* with `MutationCtx`, `ActionCtx`, or `QueryCtx` equally.
|
|
740
|
-
* @param client A configured {@link EventLogDOClient} instance.
|
|
741
|
-
* @returns A Lunora middleware that injects `ctx.events`.
|
|
742
|
-
*
|
|
743
|
-
* ```ts
|
|
744
|
-
* const client = new EventLogDOClient({
|
|
745
|
-
* fetch: (req) => env.EVENTS.get(id).fetch(req),
|
|
746
|
-
* });
|
|
747
|
-
*
|
|
748
|
-
* export const logEvent = mutation
|
|
749
|
-
* .use(eventsContext(client))
|
|
750
|
-
* .mutation(async ({ ctx, args }) => {
|
|
751
|
-
* const [entry] = await ctx.events.append([{ type: "order.placed", payload: args }]);
|
|
752
|
-
* return entry;
|
|
753
|
-
* });
|
|
754
|
-
* ```
|
|
755
|
-
* @experimental
|
|
756
|
-
*/
|
|
733
|
+
* Create a middleware that attaches a typed `ctx.events` facade backed by
|
|
734
|
+
* the given {@link EventLogDOClient}.
|
|
735
|
+
*
|
|
736
|
+
* The facade surfaces `append`, `getSince`, `getRange`, `getSize`, and
|
|
737
|
+
* `getState` — every method the DO client exposes — so handlers can read
|
|
738
|
+
* and write the event log without reaching for the DO stub directly.
|
|
739
|
+
*
|
|
740
|
+
* The middleware is unopinionated about which context it extends — it works
|
|
741
|
+
* with `MutationCtx`, `ActionCtx`, or `QueryCtx` equally.
|
|
742
|
+
* @param client A configured {@link EventLogDOClient} instance.
|
|
743
|
+
* @returns A Lunora middleware that injects `ctx.events`.
|
|
744
|
+
*
|
|
745
|
+
* ```ts
|
|
746
|
+
* const client = new EventLogDOClient({
|
|
747
|
+
* fetch: (req) => env.EVENTS.get(id).fetch(req),
|
|
748
|
+
* });
|
|
749
|
+
*
|
|
750
|
+
* export const logEvent = mutation
|
|
751
|
+
* .use(eventsContext(client))
|
|
752
|
+
* .mutation(async ({ ctx, args }) => {
|
|
753
|
+
* const [entry] = await ctx.events.append([{ type: "order.placed", payload: args }]);
|
|
754
|
+
* return entry;
|
|
755
|
+
* });
|
|
756
|
+
* ```
|
|
757
|
+
* @experimental
|
|
758
|
+
*/
|
|
757
759
|
declare const eventsContext: <Context>(client: EventLogDOClient) => Middleware<Context, Context & EventsContextOutput>;
|
|
758
760
|
/**
|
|
759
|
-
* A dependency-light subscription sink interface that mirrors what
|
|
760
|
-
* `LunoraClient.subscribe` expects, so the mirror helper doesn't
|
|
761
|
-
* need to import `@lunora/client`.
|
|
762
|
-
* @experimental
|
|
763
|
-
*/
|
|
761
|
+
* A dependency-light subscription sink interface that mirrors what
|
|
762
|
+
* `LunoraClient.subscribe` expects, so the mirror helper doesn't
|
|
763
|
+
* need to import `@lunora/client`.
|
|
764
|
+
* @experimental
|
|
765
|
+
*/
|
|
764
766
|
interface SubscriptionClient {
|
|
765
767
|
subscribe: (functionRef: {
|
|
766
768
|
__lunoraRef: string;
|
|
@@ -769,203 +771,212 @@ interface SubscriptionClient {
|
|
|
769
771
|
}) => () => void;
|
|
770
772
|
}
|
|
771
773
|
/**
|
|
772
|
-
* Subscribe a Lunora-query to the local mirror so every server push
|
|
773
|
-
* is applied to the local SQLite store.
|
|
774
|
-
*
|
|
775
|
-
* Each frame from a Lunora live query is the FULL current result set, so the
|
|
776
|
-
* callback treats it as a snapshot: it upserts every row present and emits a
|
|
777
|
-
* `delete` for any id that was mirrored on a previous frame but is absent now —
|
|
778
|
-
* otherwise rows that drop out of the server result would linger stale in the
|
|
779
|
-
* local mirror. Rows are keyed by their `id` field (the mirror's default primary
|
|
780
|
-
* key); a row without an `id` can't be reconciled on removal, and — because the
|
|
781
|
-
* mirror table's `id` column is `NOT NULL` — will fail the insert.
|
|
782
|
-
*
|
|
783
|
-
* The mirror table name is derived from the function ref alone (not `args`), so
|
|
784
|
-
* do NOT mirror two subscriptions to the same function with different `args`
|
|
785
|
-
* into the same mirror: they'd share one table and the snapshot-delete pass of
|
|
786
|
-
* one could remove rows still live in the other.
|
|
787
|
-
*
|
|
788
|
-
* Call the returned unsubscribe function to tear down both the client
|
|
789
|
-
* subscription and future mirror writes.
|
|
790
|
-
* @example
|
|
791
|
-
* ```ts
|
|
792
|
-
* const unsub = subscribeToMirror(client, mirror, api.todos.list, { userId });
|
|
793
|
-
* // Later:
|
|
794
|
-
* unsub();
|
|
795
|
-
* ```
|
|
796
|
-
* @experimental
|
|
797
|
-
*/
|
|
774
|
+
* Subscribe a Lunora-query to the local mirror so every server push
|
|
775
|
+
* is applied to the local SQLite store.
|
|
776
|
+
*
|
|
777
|
+
* Each frame from a Lunora live query is the FULL current result set, so the
|
|
778
|
+
* callback treats it as a snapshot: it upserts every row present and emits a
|
|
779
|
+
* `delete` for any id that was mirrored on a previous frame but is absent now —
|
|
780
|
+
* otherwise rows that drop out of the server result would linger stale in the
|
|
781
|
+
* local mirror. Rows are keyed by their `id` field (the mirror's default primary
|
|
782
|
+
* key); a row without an `id` can't be reconciled on removal, and — because the
|
|
783
|
+
* mirror table's `id` column is `NOT NULL` — will fail the insert.
|
|
784
|
+
*
|
|
785
|
+
* The mirror table name is derived from the function ref alone (not `args`), so
|
|
786
|
+
* do NOT mirror two subscriptions to the same function with different `args`
|
|
787
|
+
* into the same mirror: they'd share one table and the snapshot-delete pass of
|
|
788
|
+
* one could remove rows still live in the other.
|
|
789
|
+
*
|
|
790
|
+
* Call the returned unsubscribe function to tear down both the client
|
|
791
|
+
* subscription and future mirror writes.
|
|
792
|
+
* @example
|
|
793
|
+
* ```ts
|
|
794
|
+
* const unsub = subscribeToMirror(client, mirror, api.todos.list, { userId });
|
|
795
|
+
* // Later:
|
|
796
|
+
* unsub();
|
|
797
|
+
* ```
|
|
798
|
+
* @experimental
|
|
799
|
+
*/
|
|
798
800
|
declare const subscribeToMirror: (client: SubscriptionClient, mirror: LocalMirror, functionRef: {
|
|
799
801
|
__lunoraRef: string;
|
|
800
802
|
}, args: Record<string, unknown>, shardKey?: string) => (() => void);
|
|
801
803
|
/**
|
|
802
|
-
* Callback signature for state-change subscriptions.
|
|
803
|
-
* @experimental
|
|
804
|
-
*/
|
|
804
|
+
* Callback signature for state-change subscriptions.
|
|
805
|
+
* @experimental
|
|
806
|
+
*/
|
|
805
807
|
type StateChangeCallback = (state: Readonly<Record<string, unknown>>) => void;
|
|
806
808
|
/**
|
|
807
|
-
* Callback signature for event-type subscriptions.
|
|
808
|
-
* @experimental
|
|
809
|
-
*/
|
|
809
|
+
* Callback signature for event-type subscriptions.
|
|
810
|
+
* @experimental
|
|
811
|
+
*/
|
|
810
812
|
type EventCallback = (entry: EventLogEntry) => void;
|
|
811
813
|
/**
|
|
812
|
-
* Manages subscriptions to state changes and individual event types
|
|
813
|
-
* for the event-sourcing runtime.
|
|
814
|
-
*
|
|
815
|
-
* Each subscription returns an unsubscribe function — the caller is
|
|
816
|
-
* expected to call it during cleanup (e.g. in a React `useEffect`
|
|
817
|
-
* return or a Svelte `onDestroy`).
|
|
818
|
-
* @example
|
|
819
|
-
* ```ts
|
|
820
|
-
* const subs = new SubscriptionManager();
|
|
821
|
-
*
|
|
822
|
-
* // Subscribe to every state change
|
|
823
|
-
* const unsub1 = subs.onStateChange((state) => console.log("new state", state));
|
|
824
|
-
*
|
|
825
|
-
* // Subscribe to a specific event type
|
|
826
|
-
* const unsub2 = subs.onEvent("user-created", (entry) => console.log("user created", entry.payload));
|
|
827
|
-
*
|
|
828
|
-
* // Later, when state or events arrive:
|
|
829
|
-
* subs.notifyState({ users: [] });
|
|
830
|
-
* subs.notifyEvent({ seq: 1, type: "user-created", payload: { id: "1" }, timestamp: 100 });
|
|
831
|
-
*
|
|
832
|
-
* // Cleanup
|
|
833
|
-
* unsub1();
|
|
834
|
-
* unsub2();
|
|
835
|
-
* ```
|
|
836
|
-
* @experimental
|
|
837
|
-
*/
|
|
814
|
+
* Manages subscriptions to state changes and individual event types
|
|
815
|
+
* for the event-sourcing runtime.
|
|
816
|
+
*
|
|
817
|
+
* Each subscription returns an unsubscribe function — the caller is
|
|
818
|
+
* expected to call it during cleanup (e.g. in a React `useEffect`
|
|
819
|
+
* return or a Svelte `onDestroy`).
|
|
820
|
+
* @example
|
|
821
|
+
* ```ts
|
|
822
|
+
* const subs = new SubscriptionManager();
|
|
823
|
+
*
|
|
824
|
+
* // Subscribe to every state change
|
|
825
|
+
* const unsub1 = subs.onStateChange((state) => console.log("new state", state));
|
|
826
|
+
*
|
|
827
|
+
* // Subscribe to a specific event type
|
|
828
|
+
* const unsub2 = subs.onEvent("user-created", (entry) => console.log("user created", entry.payload));
|
|
829
|
+
*
|
|
830
|
+
* // Later, when state or events arrive:
|
|
831
|
+
* subs.notifyState({ users: [] });
|
|
832
|
+
* subs.notifyEvent({ seq: 1, type: "user-created", payload: { id: "1" }, timestamp: 100 });
|
|
833
|
+
*
|
|
834
|
+
* // Cleanup
|
|
835
|
+
* unsub1();
|
|
836
|
+
* unsub2();
|
|
837
|
+
* ```
|
|
838
|
+
* @experimental
|
|
839
|
+
*/
|
|
838
840
|
declare class SubscriptionManager {
|
|
839
841
|
#private;
|
|
840
842
|
/**
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
843
|
+
* Subscribe to every state change emitted by the event source.
|
|
844
|
+
* @returns Unsubscribe function.
|
|
845
|
+
*/
|
|
844
846
|
onStateChange(callback: StateChangeCallback): () => void;
|
|
845
847
|
/**
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
848
|
+
* Subscribe to a specific event type.
|
|
849
|
+
* @param eventType The event type to listen for (matches `entry.type`).
|
|
850
|
+
* @param callback Invoked with each matching entry.
|
|
851
|
+
* @returns Unsubscribe function.
|
|
852
|
+
*/
|
|
851
853
|
onEvent(eventType: string, callback: EventCallback): () => void;
|
|
852
854
|
/**
|
|
853
|
-
|
|
854
|
-
|
|
855
|
+
* Notify all state-change subscribers with the current state.
|
|
856
|
+
*/
|
|
855
857
|
notifyState(state: Readonly<Record<string, unknown>>): void;
|
|
856
858
|
/**
|
|
857
|
-
|
|
858
|
-
|
|
859
|
+
* Notify event-type subscribers whose `eventType` matches.
|
|
860
|
+
*/
|
|
859
861
|
notifyEvent(entry: EventLogEntry): void;
|
|
860
862
|
/**
|
|
861
|
-
|
|
862
|
-
|
|
863
|
+
* Return the total number of active subscriptions.
|
|
864
|
+
*/
|
|
863
865
|
get size(): number;
|
|
864
866
|
/**
|
|
865
|
-
|
|
866
|
-
|
|
867
|
+
* Remove all subscriptions.
|
|
868
|
+
*/
|
|
867
869
|
clear(): void;
|
|
868
870
|
}
|
|
869
871
|
/**
|
|
870
|
-
* Options for constructing an {@link EventsSync}.
|
|
871
|
-
* @experimental
|
|
872
|
-
*/
|
|
872
|
+
* Options for constructing an {@link EventsSync}.
|
|
873
|
+
* @experimental
|
|
874
|
+
*/
|
|
873
875
|
interface EventsSyncOptions {
|
|
874
876
|
/**
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
877
|
+
* Replay a batch of events through the derived-state machine.
|
|
878
|
+
*
|
|
879
|
+
* Called with every batch of new events fetched from the log. The
|
|
880
|
+
* consumer should feed these events into their state machine
|
|
881
|
+
* (e.g. an {@link import("@lunora/replica").EventSource | EventSource})
|
|
882
|
+
* so that the machine's state reflects the latest log position.
|
|
883
|
+
*/
|
|
882
884
|
applyEvents: (events: ReadonlyArray<EventLogEntry>) => void;
|
|
883
885
|
/**
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
886
|
+
* Fetch all events whose `seq >= sinceSeq`.
|
|
887
|
+
*
|
|
888
|
+
* In a server-side context, this typically wraps
|
|
889
|
+
* {@link import("@lunora/replica").EventLogDOClient.getSince |
|
|
890
|
+
* EventLogDOClient.getSince()}.
|
|
891
|
+
* In a client context it could call a Lunora action that proxies to the
|
|
892
|
+
* event log, or read from an IndexedDB cache.
|
|
893
|
+
*
|
|
894
|
+
* Return an empty array when there are no new events.
|
|
895
|
+
*/
|
|
894
896
|
fetchEventsSince: (sinceSeq: number) => Promise<ReadonlyArray<EventLogEntry>>;
|
|
895
897
|
/**
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
898
|
+
* Produce {@link TableDiff | TableDiffs} from the current derived state.
|
|
899
|
+
*
|
|
900
|
+
* Called after every batch of events has been applied. The consumer
|
|
901
|
+
* **recomputes a full diff from the current mirror-vs-source state** and
|
|
902
|
+
* returns the diffs needed to bring the LocalMirror up to date.
|
|
903
|
+
*
|
|
904
|
+
* **MUST be idempotent** — it must NOT advance a one-shot cursor as a side
|
|
905
|
+
* effect. A batch that fails partway (a `mirror.applyDiff` throws) is
|
|
906
|
+
* retried on the next poll from the same watermark; if this call consumed a
|
|
907
|
+
* cursor on the first attempt it would return `[]` on the retry and the
|
|
908
|
+
* un-mirrored diffs would be lost forever. Recompute-from-current-state has
|
|
909
|
+
* no such hazard: calling it again with no new events returns the same
|
|
910
|
+
* diffs, and calling it after a partial mirror write returns exactly the
|
|
911
|
+
* diffs still missing from the mirror.
|
|
912
|
+
*
|
|
913
|
+
* Return an empty array when there are no changes to push to the mirror.
|
|
914
|
+
*/
|
|
904
915
|
getTableDiffs: () => TableDiff[];
|
|
905
916
|
/**
|
|
906
|
-
|
|
907
|
-
|
|
917
|
+
* The local SQLite mirror to apply diffs to.
|
|
918
|
+
*/
|
|
908
919
|
mirror: LocalMirror;
|
|
909
920
|
/**
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
921
|
+
* Called when an error occurs during a poll cycle.
|
|
922
|
+
*
|
|
923
|
+
* Defaults to `console.error`. Set to a no-op to suppress error logging.
|
|
924
|
+
*/
|
|
914
925
|
onError?: (error: unknown) => void;
|
|
915
926
|
/**
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
927
|
+
* How often to poll for new events (in milliseconds).
|
|
928
|
+
* @default 5000
|
|
929
|
+
*/
|
|
919
930
|
pollInterval?: number;
|
|
920
931
|
}
|
|
921
932
|
/**
|
|
922
|
-
* Periodically polls an event log, replays events through a state machine,
|
|
923
|
-
* converts the resulting state into {@link TableDiff | TableDiffs}, and
|
|
924
|
-
* applies them to a {@link LocalMirror}.
|
|
925
|
-
*
|
|
926
|
-
* The class is **transport-agnostic** — it accepts a generic
|
|
927
|
-
* `fetchEventsSince` function rather than coupling to a specific source
|
|
928
|
-
* (EventLogDO, WebSocket push, IndexedDB, etc.).
|
|
929
|
-
*
|
|
930
|
-
* ## Lifecycle
|
|
931
|
-
*
|
|
932
|
-
* 1. Call `start()` to begin periodic polling.
|
|
933
|
-
* 2. Call `sync()` to perform an immediate one-shot sync.
|
|
934
|
-
* 3. Call `stop()` to halt polling.
|
|
935
|
-
*
|
|
936
|
-
* The current watermark is exposed via `watermark` and advances
|
|
937
|
-
* monotonically as events are applied.
|
|
938
|
-
* @experimental
|
|
939
|
-
*/
|
|
933
|
+
* Periodically polls an event log, replays events through a state machine,
|
|
934
|
+
* converts the resulting state into {@link TableDiff | TableDiffs}, and
|
|
935
|
+
* applies them to a {@link LocalMirror}.
|
|
936
|
+
*
|
|
937
|
+
* The class is **transport-agnostic** — it accepts a generic
|
|
938
|
+
* `fetchEventsSince` function rather than coupling to a specific source
|
|
939
|
+
* (EventLogDO, WebSocket push, IndexedDB, etc.).
|
|
940
|
+
*
|
|
941
|
+
* ## Lifecycle
|
|
942
|
+
*
|
|
943
|
+
* 1. Call `start()` to begin periodic polling.
|
|
944
|
+
* 2. Call `sync()` to perform an immediate one-shot sync.
|
|
945
|
+
* 3. Call `stop()` to halt polling.
|
|
946
|
+
*
|
|
947
|
+
* The current watermark is exposed via `watermark` and advances
|
|
948
|
+
* monotonically as events are applied.
|
|
949
|
+
* @experimental
|
|
950
|
+
*/
|
|
940
951
|
declare class EventsSync {
|
|
941
952
|
#private;
|
|
942
953
|
constructor(options: EventsSyncOptions);
|
|
943
954
|
/**
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
955
|
+
* The current watermark — the next `seq` the sync will fetch from.
|
|
956
|
+
*
|
|
957
|
+
* Starts at `0` (fetch everything). Advances to `max(seq) + 1` after
|
|
958
|
+
* each successful poll cycle.
|
|
959
|
+
*/
|
|
949
960
|
get watermark(): number;
|
|
950
961
|
/**
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
962
|
+
* Start polling for new events on the configured interval.
|
|
963
|
+
*
|
|
964
|
+
* Does nothing if polling is already active.
|
|
965
|
+
* Does **not** perform an initial sync — call {@link sync} once if you
|
|
966
|
+
* need to catch up immediately.
|
|
967
|
+
*/
|
|
957
968
|
start(): void;
|
|
958
969
|
/**
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
970
|
+
* Stop polling for new events.
|
|
971
|
+
*
|
|
972
|
+
* Safe to call when not started.
|
|
973
|
+
*/
|
|
963
974
|
stop(): void;
|
|
964
975
|
/**
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
976
|
+
* Perform a one-shot sync: fetch events since the current watermark,
|
|
977
|
+
* apply them through the state machine, and push diffs to the mirror.
|
|
978
|
+
* @returns The number of events that were fetched and applied.
|
|
979
|
+
*/
|
|
969
980
|
sync(): Promise<number>;
|
|
970
981
|
}
|
|
971
982
|
export { type AppendEventInput, type AppendOptions, type EventCallback, EventEmitter, type EventFactory, EventLog, EventLogDO, EventLogDOClient, type EventLogDOClientOptions, type EventLogEntry, type EventNamespace, type EventReducer, EventSource, type EventSourceEvents, type EventSourceOptions, type EventsContextOutput, type EventsDefinition, type EventsFacade, EventsSync, type EventsSyncOptions, InMemorySnapshotStore, type InputEvent, LocalMirror, type Materializer, type MaterializerDef, type MaterializerReducer, MaterializerRuntime, type MaterializerRuntimeOptions, type Seq, type SnapshotStore, type SqliteAdapter, type StateChangeCallback, type SubscriptionClient, SubscriptionManager, type TableDiff, UNHANDLED, type UnknownEventHandling, applyDiff, applyDiffToDatabase as applyDiffToDb, applyDiffToSnapshot, applyDiffs, applyDiffsToDatabase as applyDiffsToDb, defineEvents, defineMaterializer, eventsContext, subscribeToMirror };
|