@lunora/replica 1.0.0-alpha.4 → 1.0.0-alpha.41
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 +29 -19
- package/dist/adapters/sqlite-wasm.d.ts +29 -19
- package/dist/adapters/sqlite-wasm.mjs +1 -56
- 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 +646 -574
- package/dist/index.d.ts +646 -574
- 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-zMy7AYP4.mjs +0 -162
- package/dist/packem_shared/EventLogDO-CZYUvvSr.mjs +0 -235
- package/dist/packem_shared/EventLogDOClient-DGiEdi96.mjs +0 -86
- package/dist/packem_shared/EventSource-DfV4VoRD.mjs +0 -195
- package/dist/packem_shared/EventsSync-DkVbU0WV.mjs +0 -91
- package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
- package/dist/packem_shared/LocalMirror-GeJ26eNe.mjs +0 -188
- package/dist/packem_shared/MaterializerRuntime-HqNXqJxp.mjs +0 -204
- package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
- package/dist/packem_shared/applyDiff-BtbIl1D3.mjs +0 -40
- package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
- package/dist/packem_shared/classifyChanges-aZmkxgVI.mjs +0 -38
- 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-BUeOe5KC.d.mts +0 -439
- package/dist/packem_shared/local-mirror.d-Cd8tAg-W.d.ts +0 -439
- package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +0 -45
package/dist/react.d.mts
CHANGED
|
@@ -1,67 +1,94 @@
|
|
|
1
|
-
import { L as LocalMirror } from "./packem_shared/local-mirror.d-
|
|
2
|
-
import "./packem_shared/types.d-
|
|
1
|
+
import { L as LocalMirror } from "./packem_shared/local-mirror.d-Cip3QuMf.mjs";
|
|
2
|
+
import "./packem_shared/types.d-CkMkSwLJ.mjs";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
4
|
+
* Result of {@link useLocalQuery} — a discriminated union so callers get a
|
|
5
|
+
* typed error instead of a swallowed `undefined`.
|
|
6
|
+
* @experimental
|
|
7
|
+
*/
|
|
8
|
+
type LocalQueryResult<T> = {
|
|
9
|
+
readonly data: T[];
|
|
10
|
+
readonly error?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
readonly data?: undefined;
|
|
13
|
+
readonly error: Error;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Options for the {@link useLocalQuery} hook.
|
|
17
|
+
* @experimental
|
|
18
|
+
*/
|
|
7
19
|
interface UseLocalQueryOptions {
|
|
8
20
|
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
* Optional shard key (reserved for future use; currently unused).
|
|
22
|
+
*
|
|
23
|
+
* Intended for multi-mirror setups where a single app maintains multiple
|
|
24
|
+
* SQLite databases sharded by a key (e.g. user id, tenant id). Currently
|
|
25
|
+
* has no effect — the hook always queries the mirror passed as the first
|
|
26
|
+
* argument.
|
|
27
|
+
*/
|
|
16
28
|
shardKey?: string;
|
|
17
29
|
}
|
|
18
30
|
/**
|
|
19
|
-
* React hook that subscribes to a local SQLite query and returns
|
|
20
|
-
* live-updating results whenever the mirror applies a diff.
|
|
21
|
-
*
|
|
22
|
-
* Uses `useSyncExternalStore`
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* }
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
31
|
+
* React hook that subscribes to a local SQLite query and returns
|
|
32
|
+
* live-updating results whenever the mirror applies a diff.
|
|
33
|
+
*
|
|
34
|
+
* Uses `useSyncExternalStore` with `getSnapshot` reading {@link LocalMirror.version}
|
|
35
|
+
* — a plain number, unconditionally `Object.is`-stable across calls with no
|
|
36
|
+
* mutation in between. The actual query runs in a `useMemo` keyed on
|
|
37
|
+
* `[mirror, version, sql, stableParamsKey(params)]`, so it only re-executes
|
|
38
|
+
* when the mirror actually advances (or `sql`/`params` change) — not on
|
|
39
|
+
* every unrelated re-render.
|
|
40
|
+
*
|
|
41
|
+
* This intentionally does NOT cache query results inside {@link LocalMirror}
|
|
42
|
+
* itself: an LRU-capped cache in the mirror core evicts still-mounted hooks'
|
|
43
|
+
* entries once more than the cap's worth of distinct live queries are open
|
|
44
|
+
* on one mirror, which makes `getSnapshot` return a fresh (non-identical)
|
|
45
|
+
* object on the next read — `useSyncExternalStore` then force-re-renders,
|
|
46
|
+
* re-inserts, evicts another entry, and so on without bound. Keying the
|
|
47
|
+
* external-store snapshot on the version primitive instead of a cached
|
|
48
|
+
* query result has no such cap, so it can't loop.
|
|
49
|
+
*
|
|
50
|
+
* The hook works with React 18+ concurrent features, Suspense, and
|
|
51
|
+
* server-side rendering. During SSR the same value is returned as on
|
|
52
|
+
* the client (the mirror's current state at render time).
|
|
53
|
+
* @param mirror The {@link LocalMirror} instance to query. Must have been
|
|
54
|
+
* constructed with an {@link import("./adapters/types").SqliteAdapter}.
|
|
55
|
+
* @param sql Parameterised SQL query string. Use `?` placeholders for
|
|
56
|
+
* bound parameters (the adapter forwards them to the underlying SQLite
|
|
57
|
+
* engine without rewriting).
|
|
58
|
+
* @param params Optional positional bound parameters matching `?`
|
|
59
|
+
* placeholders in `sql`.
|
|
60
|
+
* @param _options Optional configuration (currently unused; reserved for
|
|
61
|
+
* future features like shard key routing).
|
|
62
|
+
* @returns `{ data }` with the result rows typed via the generic parameter
|
|
63
|
+
* `T`, or `{ error }` when the query fails (e.g. malformed SQL, or the
|
|
64
|
+
* target table doesn't exist yet because no matching diff has been applied
|
|
65
|
+
* — that specific case surfaces as a "no such table" `Error`). Never
|
|
66
|
+
* collapses a failure to `undefined` — check `error` explicitly rather than
|
|
67
|
+
* treating a missing `data` as "still loading".
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* import { useLocalQuery } from "@lunora/replica/react";
|
|
71
|
+
* import { mirror } from "./mirror";
|
|
72
|
+
*
|
|
73
|
+
* function UserList() {
|
|
74
|
+
* const { data: users, error } = useLocalQuery<{ id: string; name: string }>(
|
|
75
|
+
* mirror,
|
|
76
|
+
* "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
|
|
77
|
+
* ["%alice%"],
|
|
78
|
+
* );
|
|
79
|
+
*
|
|
80
|
+
* if (error) {
|
|
81
|
+
* return <p>Query failed: {error.message}</p>;
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* if (users === undefined) {
|
|
85
|
+
* return <p>Waiting for data…</p>;
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
* @experimental
|
|
92
|
+
*/
|
|
93
|
+
declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => LocalQueryResult<T>;
|
|
94
|
+
export { type LocalQueryResult, type UseLocalQueryOptions, useLocalQuery };
|
package/dist/react.d.ts
CHANGED
|
@@ -1,67 +1,94 @@
|
|
|
1
|
-
import { L as LocalMirror } from "./packem_shared/local-mirror.d-
|
|
2
|
-
import "./packem_shared/types.d-
|
|
1
|
+
import { L as LocalMirror } from "./packem_shared/local-mirror.d-ByrYTq4z.js";
|
|
2
|
+
import "./packem_shared/types.d-CkMkSwLJ.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
4
|
+
* Result of {@link useLocalQuery} — a discriminated union so callers get a
|
|
5
|
+
* typed error instead of a swallowed `undefined`.
|
|
6
|
+
* @experimental
|
|
7
|
+
*/
|
|
8
|
+
type LocalQueryResult<T> = {
|
|
9
|
+
readonly data: T[];
|
|
10
|
+
readonly error?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
readonly data?: undefined;
|
|
13
|
+
readonly error: Error;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Options for the {@link useLocalQuery} hook.
|
|
17
|
+
* @experimental
|
|
18
|
+
*/
|
|
7
19
|
interface UseLocalQueryOptions {
|
|
8
20
|
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
* Optional shard key (reserved for future use; currently unused).
|
|
22
|
+
*
|
|
23
|
+
* Intended for multi-mirror setups where a single app maintains multiple
|
|
24
|
+
* SQLite databases sharded by a key (e.g. user id, tenant id). Currently
|
|
25
|
+
* has no effect — the hook always queries the mirror passed as the first
|
|
26
|
+
* argument.
|
|
27
|
+
*/
|
|
16
28
|
shardKey?: string;
|
|
17
29
|
}
|
|
18
30
|
/**
|
|
19
|
-
* React hook that subscribes to a local SQLite query and returns
|
|
20
|
-
* live-updating results whenever the mirror applies a diff.
|
|
21
|
-
*
|
|
22
|
-
* Uses `useSyncExternalStore`
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* }
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
31
|
+
* React hook that subscribes to a local SQLite query and returns
|
|
32
|
+
* live-updating results whenever the mirror applies a diff.
|
|
33
|
+
*
|
|
34
|
+
* Uses `useSyncExternalStore` with `getSnapshot` reading {@link LocalMirror.version}
|
|
35
|
+
* — a plain number, unconditionally `Object.is`-stable across calls with no
|
|
36
|
+
* mutation in between. The actual query runs in a `useMemo` keyed on
|
|
37
|
+
* `[mirror, version, sql, stableParamsKey(params)]`, so it only re-executes
|
|
38
|
+
* when the mirror actually advances (or `sql`/`params` change) — not on
|
|
39
|
+
* every unrelated re-render.
|
|
40
|
+
*
|
|
41
|
+
* This intentionally does NOT cache query results inside {@link LocalMirror}
|
|
42
|
+
* itself: an LRU-capped cache in the mirror core evicts still-mounted hooks'
|
|
43
|
+
* entries once more than the cap's worth of distinct live queries are open
|
|
44
|
+
* on one mirror, which makes `getSnapshot` return a fresh (non-identical)
|
|
45
|
+
* object on the next read — `useSyncExternalStore` then force-re-renders,
|
|
46
|
+
* re-inserts, evicts another entry, and so on without bound. Keying the
|
|
47
|
+
* external-store snapshot on the version primitive instead of a cached
|
|
48
|
+
* query result has no such cap, so it can't loop.
|
|
49
|
+
*
|
|
50
|
+
* The hook works with React 18+ concurrent features, Suspense, and
|
|
51
|
+
* server-side rendering. During SSR the same value is returned as on
|
|
52
|
+
* the client (the mirror's current state at render time).
|
|
53
|
+
* @param mirror The {@link LocalMirror} instance to query. Must have been
|
|
54
|
+
* constructed with an {@link import("./adapters/types").SqliteAdapter}.
|
|
55
|
+
* @param sql Parameterised SQL query string. Use `?` placeholders for
|
|
56
|
+
* bound parameters (the adapter forwards them to the underlying SQLite
|
|
57
|
+
* engine without rewriting).
|
|
58
|
+
* @param params Optional positional bound parameters matching `?`
|
|
59
|
+
* placeholders in `sql`.
|
|
60
|
+
* @param _options Optional configuration (currently unused; reserved for
|
|
61
|
+
* future features like shard key routing).
|
|
62
|
+
* @returns `{ data }` with the result rows typed via the generic parameter
|
|
63
|
+
* `T`, or `{ error }` when the query fails (e.g. malformed SQL, or the
|
|
64
|
+
* target table doesn't exist yet because no matching diff has been applied
|
|
65
|
+
* — that specific case surfaces as a "no such table" `Error`). Never
|
|
66
|
+
* collapses a failure to `undefined` — check `error` explicitly rather than
|
|
67
|
+
* treating a missing `data` as "still loading".
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* import { useLocalQuery } from "@lunora/replica/react";
|
|
71
|
+
* import { mirror } from "./mirror";
|
|
72
|
+
*
|
|
73
|
+
* function UserList() {
|
|
74
|
+
* const { data: users, error } = useLocalQuery<{ id: string; name: string }>(
|
|
75
|
+
* mirror,
|
|
76
|
+
* "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
|
|
77
|
+
* ["%alice%"],
|
|
78
|
+
* );
|
|
79
|
+
*
|
|
80
|
+
* if (error) {
|
|
81
|
+
* return <p>Query failed: {error.message}</p>;
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* if (users === undefined) {
|
|
85
|
+
* return <p>Waiting for data…</p>;
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
* @experimental
|
|
92
|
+
*/
|
|
93
|
+
declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => LocalQueryResult<T>;
|
|
94
|
+
export { type LocalQueryResult, type UseLocalQueryOptions, useLocalQuery };
|
package/dist/react.mjs
CHANGED
|
@@ -1,15 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const useLocalQuery = (mirror, sql, params, _options) => {
|
|
4
|
-
const subscribe = (onStoreChange) => mirror.onChange(onStoreChange);
|
|
5
|
-
const getSnapshot = () => mirror.eventLog.size;
|
|
6
|
-
const getServerSnapshot = () => mirror.eventLog.size;
|
|
7
|
-
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
8
|
-
try {
|
|
9
|
-
return mirror.query(sql, params);
|
|
10
|
-
} catch {
|
|
11
|
-
return void 0;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export { useLocalQuery };
|
|
1
|
+
import{useSyncExternalStore as c,useMemo as u}from"react";const y=r=>JSON.stringify(r??[],(o,t)=>typeof t=="bigint"?`${t.toString()}n`:t),f=(r,o,t,g)=>{const s=n=>r.onChange(n),e=()=>r.version,a=c(s,e,e),i=y(t);return u(()=>{try{return{data:r.query(o,t)}}catch(n){return{error:n instanceof Error?n:new Error(String(n))}}},[r,a,o,i])};export{f as useLocalQuery};
|
package/package.json
CHANGED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
class EventEmitter {
|
|
2
|
-
#listeners = /* @__PURE__ */ new Map();
|
|
3
|
-
#wildcardListeners = /* @__PURE__ */ new Set();
|
|
4
|
-
// ── Registration ──────────────────────────────────────────────────
|
|
5
|
-
/**
|
|
6
|
-
* Register a handler for a specific event type.
|
|
7
|
-
* @returns An unsubscribe function (equivalent to calling {@link off}).
|
|
8
|
-
*/
|
|
9
|
-
on(event, handler) {
|
|
10
|
-
let set = this.#listeners.get(event);
|
|
11
|
-
if (!set) {
|
|
12
|
-
set = /* @__PURE__ */ new Set();
|
|
13
|
-
this.#listeners.set(event, set);
|
|
14
|
-
}
|
|
15
|
-
set.add(handler);
|
|
16
|
-
return () => {
|
|
17
|
-
this.off(event, handler);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Remove a previously registered handler.
|
|
22
|
-
*/
|
|
23
|
-
off(event, handler) {
|
|
24
|
-
this.#listeners.get(event)?.delete(handler);
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Register a wildcard handler that fires for **every** event type.
|
|
28
|
-
* @returns An unsubscribe function.
|
|
29
|
-
*/
|
|
30
|
-
onAny(handler) {
|
|
31
|
-
this.#wildcardListeners.add(handler);
|
|
32
|
-
return () => {
|
|
33
|
-
this.#wildcardListeners.delete(handler);
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Remove a wildcard handler.
|
|
38
|
-
*/
|
|
39
|
-
offAny(handler) {
|
|
40
|
-
this.#wildcardListeners.delete(handler);
|
|
41
|
-
}
|
|
42
|
-
// ── Emission ──────────────────────────────────────────────────────
|
|
43
|
-
/**
|
|
44
|
-
* Emit an event. All registered handlers (typed + wildcard) are invoked
|
|
45
|
-
* synchronously. Exceptions from handlers are caught and silently
|
|
46
|
-
* swallowed — they **must not** break the emitter loop.
|
|
47
|
-
* @returns `true` if at least one handler was called.
|
|
48
|
-
*/
|
|
49
|
-
emit(event, payload) {
|
|
50
|
-
let called = false;
|
|
51
|
-
const typed = this.#listeners.get(event);
|
|
52
|
-
if (typed) {
|
|
53
|
-
for (const handler of typed) {
|
|
54
|
-
try {
|
|
55
|
-
handler(payload);
|
|
56
|
-
called = true;
|
|
57
|
-
} catch {
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
for (const handler of this.#wildcardListeners) {
|
|
62
|
-
try {
|
|
63
|
-
handler(event, payload);
|
|
64
|
-
called = true;
|
|
65
|
-
} catch {
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return called;
|
|
69
|
-
}
|
|
70
|
-
// ── Introspection ─────────────────────────────────────────────────
|
|
71
|
-
/**
|
|
72
|
-
* Return `true` when at least one listener is registered for `event`.
|
|
73
|
-
*/
|
|
74
|
-
hasListeners(event) {
|
|
75
|
-
return (this.#listeners.get(event)?.size ?? 0) > 0 || this.#wildcardListeners.size > 0;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Return the number of typed listeners for a specific event.
|
|
79
|
-
*/
|
|
80
|
-
listenerCount(event) {
|
|
81
|
-
return this.#listeners.get(event)?.size ?? 0;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Remove all listeners.
|
|
85
|
-
*/
|
|
86
|
-
clear() {
|
|
87
|
-
this.#listeners.clear();
|
|
88
|
-
this.#wildcardListeners.clear();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export { EventEmitter };
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
class EventLog {
|
|
2
|
-
#entries = [];
|
|
3
|
-
#nextSeq = 0;
|
|
4
|
-
// eslint-disable-next-line unicorn/no-null -- public contract uses `null` for an empty log head
|
|
5
|
-
#headSeq = null;
|
|
6
|
-
append(typeOrEvent, payload, tableDiffs, options) {
|
|
7
|
-
const type = typeof typeOrEvent === "string" ? typeOrEvent : typeOrEvent.type;
|
|
8
|
-
const pl = typeof typeOrEvent === "string" ? payload : typeOrEvent.payload;
|
|
9
|
-
let diffs;
|
|
10
|
-
let resolvedOptions;
|
|
11
|
-
if (typeof typeOrEvent === "string") {
|
|
12
|
-
diffs = tableDiffs;
|
|
13
|
-
resolvedOptions = options;
|
|
14
|
-
} else {
|
|
15
|
-
diffs = void 0;
|
|
16
|
-
resolvedOptions = payload;
|
|
17
|
-
}
|
|
18
|
-
const parentSeqNumber = resolvedOptions?.parentSeqNum ?? this.#headSeq ?? void 0;
|
|
19
|
-
const seq = this.#nextSeq;
|
|
20
|
-
this.#nextSeq += 1;
|
|
21
|
-
const entry = {
|
|
22
|
-
seq,
|
|
23
|
-
type,
|
|
24
|
-
payload: pl,
|
|
25
|
-
timestamp: Date.now(),
|
|
26
|
-
tableDiffs: diffs,
|
|
27
|
-
clientId: resolvedOptions?.clientId,
|
|
28
|
-
sessionId: resolvedOptions?.sessionId,
|
|
29
|
-
parentSeqNum: parentSeqNumber
|
|
30
|
-
};
|
|
31
|
-
this.#entries.push(entry);
|
|
32
|
-
this.#headSeq = entry.seq;
|
|
33
|
-
return entry;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Atomically append multiple events to the log.
|
|
37
|
-
*
|
|
38
|
-
* All events are assigned sequential global sequence numbers and
|
|
39
|
-
* automatically wired as a causal chain (each event's `parentSeqNum`
|
|
40
|
-
* points to the previous event in the batch, or to the log head for
|
|
41
|
-
* the first event).
|
|
42
|
-
* @param events An array of events to commit atomically.
|
|
43
|
-
* @returns The newly created entries in order.
|
|
44
|
-
*/
|
|
45
|
-
commitAll(events) {
|
|
46
|
-
if (events.length === 0) {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
const entries = [];
|
|
50
|
-
for (const event of events) {
|
|
51
|
-
const { type } = event;
|
|
52
|
-
const payload = "payload" in event ? event.payload : void 0;
|
|
53
|
-
const ts = "timestamp" in event ? event.timestamp : Date.now();
|
|
54
|
-
const parentSeqNumber = entries.at(-1)?.seq ?? this.#headSeq ?? void 0;
|
|
55
|
-
const seq = this.#nextSeq;
|
|
56
|
-
this.#nextSeq += 1;
|
|
57
|
-
const entry = {
|
|
58
|
-
seq,
|
|
59
|
-
type,
|
|
60
|
-
payload,
|
|
61
|
-
timestamp: ts,
|
|
62
|
-
parentSeqNum: parentSeqNumber
|
|
63
|
-
};
|
|
64
|
-
this.#entries.push(entry);
|
|
65
|
-
entries.push(entry);
|
|
66
|
-
}
|
|
67
|
-
this.#headSeq = entries.at(-1)?.seq ?? this.#headSeq;
|
|
68
|
-
return entries;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Replace the log contents with a previously captured snapshot.
|
|
72
|
-
* This is the restore counterpart of {@link EventLog#snapshot}.
|
|
73
|
-
* Restores `headSeq` from the snapshot so auto-parenting continues
|
|
74
|
-
* after restore.
|
|
75
|
-
*/
|
|
76
|
-
load(snapshot) {
|
|
77
|
-
this.#entries = [...snapshot.entries];
|
|
78
|
-
this.#nextSeq = snapshot.nextSeq;
|
|
79
|
-
this.#headSeq = snapshot.headSeq;
|
|
80
|
-
}
|
|
81
|
-
// ── Queries ───────────────────────────────────────────────────────
|
|
82
|
-
/**
|
|
83
|
-
* Return **all** entries whose `seq >= sinceSeq`.
|
|
84
|
-
* Useful for catch-up: "give me everything since my last watermark".
|
|
85
|
-
*/
|
|
86
|
-
getSince(sinceSeq) {
|
|
87
|
-
if (sinceSeq <= 0) {
|
|
88
|
-
return [...this.#entries];
|
|
89
|
-
}
|
|
90
|
-
const first = this.#entries.findIndex((entry) => entry.seq >= sinceSeq);
|
|
91
|
-
return first === -1 ? [] : this.#entries.slice(first);
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Paginated read starting at `fromSeq`.
|
|
95
|
-
* @returns `{ entries, hasMore }` where `hasMore` is `true` when more
|
|
96
|
-
* entries exist beyond the requested page.
|
|
97
|
-
*/
|
|
98
|
-
getFrom(fromSeq, limit = 50) {
|
|
99
|
-
const first = this.#entries.findIndex((entry) => entry.seq >= fromSeq);
|
|
100
|
-
if (first === -1) {
|
|
101
|
-
return { entries: [], hasMore: false };
|
|
102
|
-
}
|
|
103
|
-
const slice = this.#entries.slice(first, first + limit);
|
|
104
|
-
return {
|
|
105
|
-
entries: slice,
|
|
106
|
-
hasMore: first + limit < this.#entries.length
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Return all entries as a snapshot suitable for serialisation.
|
|
111
|
-
*/
|
|
112
|
-
snapshot() {
|
|
113
|
-
return {
|
|
114
|
-
entries: [...this.#entries],
|
|
115
|
-
nextSeq: this.#nextSeq,
|
|
116
|
-
headSeq: this.#headSeq
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
/** Number of entries currently in the log. */
|
|
120
|
-
get size() {
|
|
121
|
-
return this.#entries.length;
|
|
122
|
-
}
|
|
123
|
-
/** The next sequence number that will be assigned. */
|
|
124
|
-
get nextSeq() {
|
|
125
|
-
return this.#nextSeq;
|
|
126
|
-
}
|
|
127
|
-
/** Return `true` when there are no entries. */
|
|
128
|
-
get isEmpty() {
|
|
129
|
-
return this.#entries.length === 0;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* The sequence number of the last (most recent) entry, or `null`
|
|
133
|
-
* when the log is empty. Used internally for auto-parenting and
|
|
134
|
-
* exposed for consumers that need the causal head.
|
|
135
|
-
*/
|
|
136
|
-
get headSeq() {
|
|
137
|
-
return this.#headSeq;
|
|
138
|
-
}
|
|
139
|
-
/** Remove all entries (primarily for testing). */
|
|
140
|
-
clear() {
|
|
141
|
-
this.#entries = [];
|
|
142
|
-
this.#nextSeq = 0;
|
|
143
|
-
this.#headSeq = null;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Return an async generator that yields every entry starting from
|
|
147
|
-
* `fromSeq` (default `0` = all entries).
|
|
148
|
-
*
|
|
149
|
-
* Because `EventLog` is purely in-memory, the generator yields all
|
|
150
|
-
* matching entries synchronously on first iteration and then completes.
|
|
151
|
-
* For a streaming / push-based variant see {@link EventSource.events}.
|
|
152
|
-
*/
|
|
153
|
-
// eslint-disable-next-line @typescript-eslint/require-await -- kept async so callers can uniformly `for await` over any event stream
|
|
154
|
-
async *events(fromSeq = 0) {
|
|
155
|
-
const entries = this.getSince(fromSeq);
|
|
156
|
-
for (const entry of entries) {
|
|
157
|
-
yield entry;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export { EventLog };
|