@absolutejs/sync 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -24
- package/dist/adapters/drizzle/collection.d.ts +27 -0
- package/dist/adapters/drizzle/index.d.ts +20 -0
- package/dist/adapters/drizzle/index.js +265 -0
- package/dist/adapters/drizzle/index.js.map +14 -0
- package/dist/adapters/drizzle/predicate.d.ts +20 -0
- package/dist/adapters/drizzle/read.d.ts +31 -0
- package/dist/adapters/drizzle/topics.d.ts +41 -0
- package/dist/adapters/drizzle/write.d.ts +69 -0
- package/dist/adapters/mysql/index.d.ts +75 -0
- package/dist/adapters/mysql/index.js +171 -0
- package/dist/adapters/mysql/index.js.map +11 -0
- package/dist/adapters/postgres/index.d.ts +53 -0
- package/dist/adapters/postgres/index.js +86 -0
- package/dist/adapters/postgres/index.js.map +10 -0
- package/dist/adapters/prisma/collection.d.ts +39 -0
- package/dist/adapters/prisma/index.d.ts +23 -0
- package/dist/adapters/prisma/index.js +231 -0
- package/dist/adapters/prisma/index.js.map +14 -0
- package/dist/adapters/prisma/predicate.d.ts +20 -0
- package/dist/adapters/prisma/read.d.ts +28 -0
- package/dist/adapters/prisma/topics.d.ts +29 -0
- package/dist/adapters/prisma/write.d.ts +65 -0
- package/dist/adapters/sqlite/index.d.ts +32 -0
- package/dist/adapters/sqlite/index.js +128 -0
- package/dist/adapters/sqlite/index.js.map +11 -0
- package/dist/angular/index.d.ts +1 -0
- package/dist/angular/index.js +347 -0
- package/dist/angular/index.js.map +11 -0
- package/dist/angular/sync-collection.service.d.ts +20 -0
- package/dist/client/index.d.ts +12 -30
- package/dist/client/index.js +1099 -3
- package/dist/client/index.js.map +10 -4
- package/dist/client/liveQuery.d.ts +75 -0
- package/dist/client/presence.d.ts +37 -0
- package/dist/client/subscriber.d.ts +30 -0
- package/dist/client/syncClient.d.ts +53 -0
- package/dist/client/syncCollection.d.ts +102 -0
- package/dist/client/syncStore.d.ts +81 -0
- package/dist/engine/aggregate.d.ts +45 -0
- package/dist/engine/cluster.d.ts +41 -0
- package/dist/engine/collection.d.ts +87 -0
- package/dist/engine/connection.d.ts +103 -0
- package/dist/engine/dataflow.d.ts +109 -0
- package/dist/engine/equiJoin.d.ts +51 -0
- package/dist/engine/graph.d.ts +85 -0
- package/dist/engine/index.d.ts +40 -0
- package/dist/engine/index.js +1774 -0
- package/dist/engine/index.js.map +23 -0
- package/dist/engine/materializedView.d.ts +53 -0
- package/dist/engine/mutation.d.ts +66 -0
- package/dist/engine/pollingSource.d.ts +42 -0
- package/dist/engine/presence.d.ts +46 -0
- package/dist/engine/reactive.d.ts +67 -0
- package/dist/engine/routes.d.ts +40 -0
- package/dist/engine/socket.d.ts +67 -0
- package/dist/engine/syncEngine.d.ts +132 -0
- package/dist/engine/types.d.ts +45 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +327 -3
- package/dist/index.js.map +8 -5
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +332 -0
- package/dist/react/index.js.map +11 -0
- package/dist/react/useSyncCollection.d.ts +16 -0
- package/dist/reactiveHub.d.ts +6 -0
- package/dist/svelte/createSyncCollectionStore.d.ts +15 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +338 -0
- package/dist/svelte/index.js.map +11 -0
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +331 -0
- package/dist/vue/index.js.map +11 -0
- package/dist/vue/useSyncCollection.d.ts +17 -0
- package/package.json +104 -6
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/engine/pollingSource.ts
|
|
3
|
+
var OP_BY_NAME = {
|
|
4
|
+
insert: "insert",
|
|
5
|
+
INSERT: "insert",
|
|
6
|
+
update: "update",
|
|
7
|
+
UPDATE: "update",
|
|
8
|
+
delete: "delete",
|
|
9
|
+
DELETE: "delete"
|
|
10
|
+
};
|
|
11
|
+
var parseOutboxRow = (row) => {
|
|
12
|
+
if (typeof row.tbl !== "string") {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const op = OP_BY_NAME[row.op];
|
|
16
|
+
if (op === undefined) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let payload = row.payload;
|
|
20
|
+
if (typeof payload === "string") {
|
|
21
|
+
try {
|
|
22
|
+
payload = JSON.parse(payload);
|
|
23
|
+
} catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (typeof payload !== "object" || payload === null) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
return { table: row.tbl, change: { op, row: payload } };
|
|
31
|
+
};
|
|
32
|
+
var createPollingChangeSource = (options) => {
|
|
33
|
+
const intervalMs = options.intervalMs ?? 1000;
|
|
34
|
+
const parse = options.parse ?? parseOutboxRow;
|
|
35
|
+
const onError = options.onError ?? ((error) => {
|
|
36
|
+
console.warn("[sync] polling change source error:", error);
|
|
37
|
+
});
|
|
38
|
+
let cursor = options.startSeq ?? 0;
|
|
39
|
+
let running = false;
|
|
40
|
+
let timer;
|
|
41
|
+
const tick = async (emit) => {
|
|
42
|
+
if (!running) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const rows = await options.poll(cursor);
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
const parsed = parse(row);
|
|
49
|
+
if (parsed !== undefined) {
|
|
50
|
+
await emit(parsed.table, parsed.change);
|
|
51
|
+
}
|
|
52
|
+
if (typeof row.seq === "number" && row.seq > cursor) {
|
|
53
|
+
cursor = row.seq;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (rows.length > 0) {
|
|
57
|
+
await options.onProcessed?.(cursor);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
onError(error);
|
|
61
|
+
}
|
|
62
|
+
if (running) {
|
|
63
|
+
timer = setTimeout(() => {
|
|
64
|
+
tick(emit);
|
|
65
|
+
}, intervalMs);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
start: async (emit) => {
|
|
70
|
+
if (running) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
running = true;
|
|
74
|
+
await tick(emit);
|
|
75
|
+
},
|
|
76
|
+
stop: () => {
|
|
77
|
+
running = false;
|
|
78
|
+
if (timer !== undefined) {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
timer = undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/adapters/sqlite/index.ts
|
|
87
|
+
var DEFAULT_CHANGELOG = "absolute_sync_changelog";
|
|
88
|
+
var DEFAULT_PREFIX = "absolute_sync";
|
|
89
|
+
var OPS = ["insert", "update", "delete"];
|
|
90
|
+
var sqliteChangelogSchema = (options) => {
|
|
91
|
+
const changelog = options.changelogTable ?? DEFAULT_CHANGELOG;
|
|
92
|
+
const prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
93
|
+
const createTable = [
|
|
94
|
+
`CREATE TABLE IF NOT EXISTS ${changelog} (`,
|
|
95
|
+
"\tseq INTEGER PRIMARY KEY AUTOINCREMENT,",
|
|
96
|
+
"\ttbl TEXT NOT NULL,",
|
|
97
|
+
"\top TEXT NOT NULL,",
|
|
98
|
+
"\tpayload TEXT NOT NULL,",
|
|
99
|
+
"\tcreated_at TEXT NOT NULL DEFAULT (datetime('now'))",
|
|
100
|
+
");"
|
|
101
|
+
].join(`
|
|
102
|
+
`);
|
|
103
|
+
const jsonObject = (columns, ref) => `json_object(${columns.map((column) => `'${column}', ${ref}.${column}`).join(", ")})`;
|
|
104
|
+
const triggers = Object.entries(options.tables).flatMap(([table, columns]) => OPS.map((op) => {
|
|
105
|
+
const ref = op === "delete" ? "OLD" : "NEW";
|
|
106
|
+
const name = `${prefix}_${table}_${op}`;
|
|
107
|
+
return [
|
|
108
|
+
`DROP TRIGGER IF EXISTS ${name};`,
|
|
109
|
+
`CREATE TRIGGER ${name} AFTER ${op.toUpperCase()} ON ${table}`,
|
|
110
|
+
"BEGIN",
|
|
111
|
+
` INSERT INTO ${changelog} (tbl, op, payload)`,
|
|
112
|
+
` VALUES ('${table}', '${op}', ${jsonObject(columns, ref)});`,
|
|
113
|
+
"END;"
|
|
114
|
+
].join(`
|
|
115
|
+
`);
|
|
116
|
+
}));
|
|
117
|
+
return [createTable, ...triggers].join(`
|
|
118
|
+
|
|
119
|
+
`);
|
|
120
|
+
};
|
|
121
|
+
export {
|
|
122
|
+
sqliteChangelogSchema,
|
|
123
|
+
parseOutboxRow,
|
|
124
|
+
createPollingChangeSource
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
//# debugId=3D209B4D7BF1282864756E2164756E21
|
|
128
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/engine/pollingSource.ts", "../src/adapters/sqlite/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { ChangeSource, EmitChange, ParsedChange, RowOp } from './types';\n\n/**\n * A database-agnostic CDC {@link ChangeSource} that tails an append-only\n * changelog (outbox) table and emits its rows into the engine.\n *\n * This is the portable way to catch out-of-band writes on databases without a\n * native push channel — MySQL (no `LISTEN/NOTIFY`) and SQLite (no update hook\n * reachable from the JS runtime). Install per-table triggers that append to the\n * changelog (see `@absolutejs/sync/mysql` and `/sqlite`), then poll it here. It\n * is also a fine fallback for Postgres when you'd rather not run `LISTEN`.\n *\n * Driver-agnostic: you supply `poll(sinceSeq)` — a single\n * `SELECT seq, tbl, op, payload FROM <changelog> WHERE seq > ? ORDER BY seq` run\n * with your client — and the adapter tracks the cursor, parses each row, emits,\n * and advances. Delivery is at-least-once across crashes (a row may re-emit if\n * the process dies mid-batch); the engine's per-key last-write-wins makes that\n * safe. Use `onProcessed` to prune the changelog once a watermark is durable.\n */\n\nconst OP_BY_NAME: Record<string, RowOp> = {\n\tinsert: 'insert',\n\tINSERT: 'insert',\n\tupdate: 'update',\n\tUPDATE: 'update',\n\tdelete: 'delete',\n\tDELETE: 'delete'\n};\n\n/** One changelog row, as returned by your `poll` query. */\nexport type OutboxRow = {\n\t/** Monotonic sequence (the cursor advances to the max seen). */\n\tseq: number;\n\t/** Source table the change happened on. */\n\ttbl: string;\n\t/** `insert` | `update` | `delete` (upper- or lower-case). */\n\top: string;\n\t/** The row's captured values — a JSON string or an already-parsed object. */\n\tpayload: unknown;\n};\n\n/**\n * Default changelog-row parser: normalizes `op`, JSON-parses a string `payload`,\n * and returns `{ table, change }`. Returns `undefined` for a malformed row so it\n * is skipped (and its `seq` still advances the cursor) rather than wedging the\n * feed.\n */\nexport const parseOutboxRow = (row: OutboxRow): ParsedChange | undefined => {\n\tif (typeof row.tbl !== 'string') {\n\t\treturn undefined;\n\t}\n\tconst op = OP_BY_NAME[row.op];\n\tif (op === undefined) {\n\t\treturn undefined;\n\t}\n\tlet payload: unknown = row.payload;\n\tif (typeof payload === 'string') {\n\t\ttry {\n\t\t\tpayload = JSON.parse(payload);\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tif (typeof payload !== 'object' || payload === null) {\n\t\treturn undefined;\n\t}\n\treturn { table: row.tbl, change: { op, row: payload } };\n};\n\nexport type PollingChangeSourceOptions = {\n\t/**\n\t * Fetch changelog rows with `seq > sinceSeq`, ordered by `seq` ascending.\n\t * e.g. `(since) => sql\\`SELECT seq, tbl, op, payload FROM absolute_sync_changelog WHERE seq > ${since} ORDER BY seq\\``.\n\t */\n\tpoll: (sinceSeq: number) => Promise<OutboxRow[]> | OutboxRow[];\n\t/** Poll interval in ms. Defaults to 1000. */\n\tintervalMs?: number;\n\t/** Resume cursor (highest `seq` already processed). Defaults to 0. */\n\tstartSeq?: number;\n\t/** Override the row parser (defaults to {@link parseOutboxRow}). */\n\tparse?: (row: OutboxRow) => ParsedChange | undefined;\n\t/** Called after a non-empty batch with the new watermark — prune here. */\n\tonProcessed?: (uptoSeq: number) => void | Promise<void>;\n\t/** Called if a poll throws (the loop keeps running). Defaults to a warning. */\n\tonError?: (error: unknown) => void;\n};\n\n/**\n * Create a polling {@link ChangeSource} over a changelog table. Connect it with\n * `engine.connectSource(...)`; `start` runs an immediate first poll (draining any\n * backlog) and then polls every `intervalMs` until `stop`.\n */\nexport const createPollingChangeSource = (\n\toptions: PollingChangeSourceOptions\n): ChangeSource => {\n\tconst intervalMs = options.intervalMs ?? 1000;\n\tconst parse = options.parse ?? parseOutboxRow;\n\tconst onError =\n\t\toptions.onError ??\n\t\t((error: unknown) => {\n\t\t\tconsole.warn('[sync] polling change source error:', error);\n\t\t});\n\tlet cursor = options.startSeq ?? 0;\n\tlet running = false;\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\n\tconst tick = async (emit: EmitChange): Promise<void> => {\n\t\tif (!running) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst rows = await options.poll(cursor);\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst parsed = parse(row);\n\t\t\t\tif (parsed !== undefined) {\n\t\t\t\t\tawait emit(parsed.table, parsed.change);\n\t\t\t\t}\n\t\t\t\tif (typeof row.seq === 'number' && row.seq > cursor) {\n\t\t\t\t\tcursor = row.seq;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (rows.length > 0) {\n\t\t\t\tawait options.onProcessed?.(cursor);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tonError(error);\n\t\t}\n\t\tif (running) {\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tvoid tick(emit);\n\t\t\t}, intervalMs);\n\t\t}\n\t};\n\n\treturn {\n\t\tstart: async (emit) => {\n\t\t\tif (running) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trunning = true;\n\t\t\tawait tick(emit);\n\t\t},\n\t\tstop: () => {\n\t\t\trunning = false;\n\t\t\tif (timer !== undefined) {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\ttimer = undefined;\n\t\t\t}\n\t\t}\n\t};\n};\n",
|
|
6
|
+
"/**\n * SQLite CDC adapter for @absolutejs/sync (Tier 3, M5).\n *\n * SQLite has no `LISTEN/NOTIFY`, and its `update_hook` isn't reachable from the\n * JS runtimes we target, so out-of-band writes are caught with the portable\n * changelog (outbox) pattern: install triggers that append every row change to a\n * changelog table, then tail it with {@link createPollingChangeSource}. Polling a\n * local SQLite table is cheap (same process, no network).\n *\n * Dependency-free — it only generates SQL; bring your own client (`bun:sqlite`,\n * better-sqlite3, …) to run it and to back the poll query.\n */\n\nexport {\n\tcreatePollingChangeSource,\n\tparseOutboxRow\n} from '../../engine/pollingSource';\nexport type {\n\tOutboxRow,\n\tPollingChangeSourceOptions\n} from '../../engine/pollingSource';\n\nconst DEFAULT_CHANGELOG = 'absolute_sync_changelog';\nconst DEFAULT_PREFIX = 'absolute_sync';\nconst OPS = ['insert', 'update', 'delete'] as const;\n\nexport type SqliteChangelogOptions = {\n\t/** Table name → the column names to capture in the change payload. */\n\ttables: Record<string, string[]>;\n\t/** Changelog table name. Defaults to `absolute_sync_changelog`. */\n\tchangelogTable?: string;\n\t/** Trigger name prefix. Defaults to `absolute_sync`. */\n\tprefix?: string;\n};\n\n/**\n * Generate the SQL that installs the changelog table and per-table\n * insert/update/delete triggers — run it once (e.g. in a migration). Each\n * trigger appends `{ tbl, op, payload }` (payload built with `json_object` from\n * the listed columns) to the changelog for {@link createPollingChangeSource}.\n *\n * The statements are `;`-separated; run them as a script, or split on `;` if your\n * driver executes one statement per call.\n */\nexport const sqliteChangelogSchema = (\n\toptions: SqliteChangelogOptions\n): string => {\n\tconst changelog = options.changelogTable ?? DEFAULT_CHANGELOG;\n\tconst prefix = options.prefix ?? DEFAULT_PREFIX;\n\n\tconst createTable = [\n\t\t`CREATE TABLE IF NOT EXISTS ${changelog} (`,\n\t\t'\\tseq INTEGER PRIMARY KEY AUTOINCREMENT,',\n\t\t'\\ttbl TEXT NOT NULL,',\n\t\t'\\top TEXT NOT NULL,',\n\t\t'\\tpayload TEXT NOT NULL,',\n\t\t\"\\tcreated_at TEXT NOT NULL DEFAULT (datetime('now'))\",\n\t\t');'\n\t].join('\\n');\n\n\tconst jsonObject = (columns: string[], ref: 'NEW' | 'OLD'): string =>\n\t\t`json_object(${columns\n\t\t\t.map((column) => `'${column}', ${ref}.${column}`)\n\t\t\t.join(', ')})`;\n\n\tconst triggers = Object.entries(options.tables).flatMap(\n\t\t([table, columns]) =>\n\t\t\tOPS.map((op) => {\n\t\t\t\tconst ref = op === 'delete' ? 'OLD' : 'NEW';\n\t\t\t\tconst name = `${prefix}_${table}_${op}`;\n\t\t\t\treturn [\n\t\t\t\t\t`DROP TRIGGER IF EXISTS ${name};`,\n\t\t\t\t\t`CREATE TRIGGER ${name} AFTER ${op.toUpperCase()} ON ${table}`,\n\t\t\t\t\t'BEGIN',\n\t\t\t\t\t`\\tINSERT INTO ${changelog} (tbl, op, payload)`,\n\t\t\t\t\t`\\tVALUES ('${table}', '${op}', ${jsonObject(columns, ref)});`,\n\t\t\t\t\t'END;'\n\t\t\t\t].join('\\n');\n\t\t\t})\n\t);\n\n\treturn [createTable, ...triggers].join('\\n\\n');\n};\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "3D209B4D7BF1282864756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SyncCollectionService } from './sync-collection.service';
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __name = (target, name) => {
|
|
5
|
+
Object.defineProperty(target, "name", {
|
|
6
|
+
value: name,
|
|
7
|
+
enumerable: false,
|
|
8
|
+
configurable: true
|
|
9
|
+
});
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
13
|
+
var __typeError = (msg) => {
|
|
14
|
+
throw TypeError(msg);
|
|
15
|
+
};
|
|
16
|
+
var __defNormalProp = (obj, key, value) => (key in obj) ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
17
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
18
|
+
var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
|
|
19
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
20
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
21
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
22
|
+
var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
|
|
23
|
+
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
24
|
+
var __expectFn = (fn) => fn !== undefined && typeof fn !== "function" ? __typeError("Function expected") : fn;
|
|
25
|
+
var __decoratorContext = (kind, name, done, metadata, fns) => ({
|
|
26
|
+
kind: __decoratorStrings[kind],
|
|
27
|
+
name,
|
|
28
|
+
metadata,
|
|
29
|
+
addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null))
|
|
30
|
+
});
|
|
31
|
+
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
|
|
32
|
+
var __runInitializers = (array, flags, self, value) => {
|
|
33
|
+
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length;i < n; i++)
|
|
34
|
+
flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
38
|
+
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
|
|
39
|
+
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
|
|
40
|
+
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
|
|
41
|
+
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : {
|
|
42
|
+
get [name]() {
|
|
43
|
+
return __privateGet(this, extra);
|
|
44
|
+
},
|
|
45
|
+
set [name](x) {
|
|
46
|
+
__privateSet(this, extra, x);
|
|
47
|
+
}
|
|
48
|
+
}, name));
|
|
49
|
+
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
|
|
50
|
+
for (var i = decorators.length - 1;i >= 0; i--) {
|
|
51
|
+
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
|
|
52
|
+
if (k) {
|
|
53
|
+
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => (name in x) };
|
|
54
|
+
if (k ^ 3)
|
|
55
|
+
access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
|
|
56
|
+
if (k > 2)
|
|
57
|
+
access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
|
|
58
|
+
}
|
|
59
|
+
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? undefined : { get: desc.get, set: desc.set } : target, ctx);
|
|
60
|
+
done._ = 1;
|
|
61
|
+
if (k ^ 4 || it === undefined)
|
|
62
|
+
__expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
|
|
63
|
+
else if (typeof it !== "object" || it === null)
|
|
64
|
+
__typeError("Object expected");
|
|
65
|
+
else
|
|
66
|
+
__expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
|
|
67
|
+
}
|
|
68
|
+
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/angular/sync-collection.service.ts
|
|
72
|
+
import { computed, Injectable, signal } from "@angular/core";
|
|
73
|
+
|
|
74
|
+
// src/client/syncCollection.ts
|
|
75
|
+
var localStorageMutationStorage = (key) => ({
|
|
76
|
+
load: () => {
|
|
77
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
78
|
+
return raw ? JSON.parse(raw) : [];
|
|
79
|
+
},
|
|
80
|
+
save: (records) => {
|
|
81
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(records));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
var SUBSCRIPTION_ID = "s";
|
|
85
|
+
var createSyncCollection = (options) => {
|
|
86
|
+
const key = options.key ?? ((row) => row.id);
|
|
87
|
+
const reconnectMs = options.reconnectMs ?? 500;
|
|
88
|
+
const maxReconnectMs = options.maxReconnectMs ?? 1e4;
|
|
89
|
+
const Impl = options.webSocketImpl ?? globalThis.WebSocket;
|
|
90
|
+
if (!Impl) {
|
|
91
|
+
throw new Error("createSyncCollection requires WebSocket. Run in a browser or pass webSocketImpl.");
|
|
92
|
+
}
|
|
93
|
+
const confirmed = new Map;
|
|
94
|
+
const pending = [];
|
|
95
|
+
let mutationSeq = 0;
|
|
96
|
+
let state = {
|
|
97
|
+
data: [],
|
|
98
|
+
status: "connecting",
|
|
99
|
+
error: undefined
|
|
100
|
+
};
|
|
101
|
+
const listeners = new Set;
|
|
102
|
+
const setState = (patch) => {
|
|
103
|
+
state = { ...state, ...patch };
|
|
104
|
+
for (const listener of listeners) {
|
|
105
|
+
listener(state);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const recompute = (patch = {}) => {
|
|
109
|
+
const working = new Map(confirmed);
|
|
110
|
+
const draft = {
|
|
111
|
+
set: (row) => working.set(key(row), row),
|
|
112
|
+
delete: (rowKey) => working.delete(rowKey)
|
|
113
|
+
};
|
|
114
|
+
for (const mutation of pending) {
|
|
115
|
+
mutation.optimistic?.(draft);
|
|
116
|
+
}
|
|
117
|
+
setState({ ...patch, data: [...working.values()] });
|
|
118
|
+
};
|
|
119
|
+
let socket;
|
|
120
|
+
let connected = false;
|
|
121
|
+
let closed = false;
|
|
122
|
+
let attempt = 0;
|
|
123
|
+
let reconnectTimer;
|
|
124
|
+
let appliedVersion = 0;
|
|
125
|
+
const persist = () => {
|
|
126
|
+
options.storage?.save(pending.map((mutation) => ({
|
|
127
|
+
mutationId: mutation.mutationId,
|
|
128
|
+
name: mutation.name,
|
|
129
|
+
args: mutation.args
|
|
130
|
+
})));
|
|
131
|
+
};
|
|
132
|
+
const settlePending = (mutationId) => {
|
|
133
|
+
const index = pending.findIndex((mutation2) => mutation2.mutationId === mutationId);
|
|
134
|
+
if (index === -1) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const [mutation] = pending.splice(index, 1);
|
|
138
|
+
persist();
|
|
139
|
+
return mutation;
|
|
140
|
+
};
|
|
141
|
+
const applyFrame = (frame) => {
|
|
142
|
+
if (frame.type === "snapshot") {
|
|
143
|
+
confirmed.clear();
|
|
144
|
+
for (const row of frame.rows) {
|
|
145
|
+
confirmed.set(key(row), row);
|
|
146
|
+
}
|
|
147
|
+
if (frame.version !== undefined) {
|
|
148
|
+
appliedVersion = frame.version;
|
|
149
|
+
}
|
|
150
|
+
recompute({ status: "ready", error: undefined });
|
|
151
|
+
} else if (frame.type === "diff") {
|
|
152
|
+
for (const row of frame.removed) {
|
|
153
|
+
confirmed.delete(key(row));
|
|
154
|
+
}
|
|
155
|
+
for (const row of frame.added) {
|
|
156
|
+
confirmed.set(key(row), row);
|
|
157
|
+
}
|
|
158
|
+
for (const row of frame.changed) {
|
|
159
|
+
confirmed.set(key(row), row);
|
|
160
|
+
}
|
|
161
|
+
if (frame.version !== undefined) {
|
|
162
|
+
appliedVersion = Math.max(appliedVersion, frame.version);
|
|
163
|
+
}
|
|
164
|
+
recompute();
|
|
165
|
+
} else if (frame.type === "error") {
|
|
166
|
+
setState({ error: frame.message });
|
|
167
|
+
options.onError?.(frame.message);
|
|
168
|
+
} else if (frame.type === "ack") {
|
|
169
|
+
const mutation = settlePending(frame.mutationId);
|
|
170
|
+
if (mutation !== undefined) {
|
|
171
|
+
recompute();
|
|
172
|
+
mutation.resolve(frame.result);
|
|
173
|
+
}
|
|
174
|
+
} else if (frame.type === "reject") {
|
|
175
|
+
const mutation = settlePending(frame.mutationId);
|
|
176
|
+
if (mutation !== undefined) {
|
|
177
|
+
recompute();
|
|
178
|
+
mutation.reject(new Error(String(frame.message)));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const sendMutate = (mutation) => {
|
|
183
|
+
if (connected) {
|
|
184
|
+
socket?.send(JSON.stringify({
|
|
185
|
+
type: "mutate",
|
|
186
|
+
mutationId: mutation.mutationId,
|
|
187
|
+
name: mutation.name,
|
|
188
|
+
args: mutation.args
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const connect = () => {
|
|
193
|
+
if (closed) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
setState({ status: "connecting" });
|
|
197
|
+
const ws = new Impl(options.url);
|
|
198
|
+
socket = ws;
|
|
199
|
+
ws.onopen = () => {
|
|
200
|
+
attempt = 0;
|
|
201
|
+
connected = true;
|
|
202
|
+
ws.send(JSON.stringify({
|
|
203
|
+
type: "subscribe",
|
|
204
|
+
id: SUBSCRIPTION_ID,
|
|
205
|
+
collection: options.collection,
|
|
206
|
+
params: options.params,
|
|
207
|
+
since: appliedVersion > 0 ? appliedVersion : undefined
|
|
208
|
+
}));
|
|
209
|
+
for (const mutation of pending) {
|
|
210
|
+
sendMutate(mutation);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
ws.onmessage = (event) => {
|
|
214
|
+
try {
|
|
215
|
+
applyFrame(JSON.parse(event.data));
|
|
216
|
+
} catch {}
|
|
217
|
+
};
|
|
218
|
+
ws.onclose = () => {
|
|
219
|
+
connected = false;
|
|
220
|
+
if (closed || reconnectMs <= 0) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const delay = Math.min(reconnectMs * 2 ** attempt, maxReconnectMs);
|
|
224
|
+
attempt += 1;
|
|
225
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
connect();
|
|
229
|
+
const hydratePersisted = async () => {
|
|
230
|
+
if (options.storage === undefined) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const records = await options.storage.load();
|
|
234
|
+
for (const record of records) {
|
|
235
|
+
if (pending.some((m) => m.mutationId === record.mutationId)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
pending.push({
|
|
239
|
+
mutationId: record.mutationId,
|
|
240
|
+
name: record.name,
|
|
241
|
+
args: record.args,
|
|
242
|
+
resolve: () => {},
|
|
243
|
+
reject: () => {}
|
|
244
|
+
});
|
|
245
|
+
mutationSeq = Math.max(mutationSeq, record.mutationId);
|
|
246
|
+
}
|
|
247
|
+
if (connected) {
|
|
248
|
+
for (const mutation of pending) {
|
|
249
|
+
sendMutate(mutation);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
hydratePersisted();
|
|
254
|
+
return {
|
|
255
|
+
get: () => state,
|
|
256
|
+
subscribe: (listener) => {
|
|
257
|
+
listeners.add(listener);
|
|
258
|
+
return () => {
|
|
259
|
+
listeners.delete(listener);
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
mutate: (mutateOptions) => new Promise((resolve, reject) => {
|
|
263
|
+
const mutation = {
|
|
264
|
+
mutationId: mutationSeq += 1,
|
|
265
|
+
name: mutateOptions.name,
|
|
266
|
+
args: mutateOptions.args,
|
|
267
|
+
optimistic: mutateOptions.optimistic,
|
|
268
|
+
resolve: (result) => resolve(result),
|
|
269
|
+
reject
|
|
270
|
+
};
|
|
271
|
+
pending.push(mutation);
|
|
272
|
+
persist();
|
|
273
|
+
recompute();
|
|
274
|
+
sendMutate(mutation);
|
|
275
|
+
}),
|
|
276
|
+
close: () => {
|
|
277
|
+
if (closed) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
closed = true;
|
|
281
|
+
connected = false;
|
|
282
|
+
if (reconnectTimer !== undefined) {
|
|
283
|
+
clearTimeout(reconnectTimer);
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
socket?.send(JSON.stringify({ type: "unsubscribe", id: SUBSCRIPTION_ID }));
|
|
287
|
+
socket?.close();
|
|
288
|
+
} catch {}
|
|
289
|
+
for (const mutation of pending.splice(0)) {
|
|
290
|
+
mutation.reject(new Error("sync collection closed"));
|
|
291
|
+
}
|
|
292
|
+
persist();
|
|
293
|
+
setState({ status: "closed" });
|
|
294
|
+
listeners.clear();
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/angular/sync-collection.service.ts
|
|
300
|
+
var _dec = [
|
|
301
|
+
Injectable({ providedIn: "root" })
|
|
302
|
+
];
|
|
303
|
+
var _init = __decoratorStart(undefined);
|
|
304
|
+
|
|
305
|
+
class SyncCollectionService {
|
|
306
|
+
collections = new Set;
|
|
307
|
+
connect(options) {
|
|
308
|
+
const data = signal([]);
|
|
309
|
+
const status = signal("connecting");
|
|
310
|
+
const error = signal(undefined);
|
|
311
|
+
let collection = null;
|
|
312
|
+
if (typeof window !== "undefined") {
|
|
313
|
+
collection = createSyncCollection(options);
|
|
314
|
+
this.collections.add(collection);
|
|
315
|
+
const apply = (state) => {
|
|
316
|
+
data.set(state.data);
|
|
317
|
+
status.set(state.status);
|
|
318
|
+
error.set(state.error);
|
|
319
|
+
};
|
|
320
|
+
apply(collection.get());
|
|
321
|
+
collection.subscribe(apply);
|
|
322
|
+
}
|
|
323
|
+
const mutate = (mutateOptions) => collection ? collection.mutate(mutateOptions) : Promise.reject(new Error("sync collection is not ready"));
|
|
324
|
+
return {
|
|
325
|
+
data: computed(() => data()),
|
|
326
|
+
error: computed(() => error()),
|
|
327
|
+
mutate,
|
|
328
|
+
status: computed(() => status())
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
ngOnDestroy() {
|
|
332
|
+
for (const collection of this.collections) {
|
|
333
|
+
collection.close();
|
|
334
|
+
}
|
|
335
|
+
this.collections.clear();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
SyncCollectionService = __decorateElement(_init, 0, "SyncCollectionService", _dec, SyncCollectionService);
|
|
339
|
+
__runInitializers(_init, 1, SyncCollectionService);
|
|
340
|
+
__decoratorMetadata(_init, SyncCollectionService);
|
|
341
|
+
let _SyncCollectionService = SyncCollectionService;
|
|
342
|
+
export {
|
|
343
|
+
SyncCollectionService
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
//# debugId=5D661943E00CCAFF64756E2164756E21
|
|
347
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/angular/sync-collection.service.ts", "../src/client/syncCollection.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { computed, Injectable, OnDestroy, signal } from '@angular/core';\nimport { createSyncCollection } from '../client/syncCollection';\nimport type {\n\tMutateOptions,\n\tSyncCollection,\n\tSyncCollectionOptions,\n\tSyncCollectionStatus\n} from '../client/syncCollection';\n\n/**\n * Angular binding for live sync-engine collections (the Tier 3 store). Inject\n * the service and call `connect(options)` to get `data`/`status`/`error` signals\n * maintained from the WebSocket diff stream, plus an optimistic `mutate`. All\n * opened collections close on the service's destroy.\n *\n * SSR-safe: the socket only opens in a browser, so server rendering is inert.\n */\n@Injectable({ providedIn: 'root' })\nexport class SyncCollectionService implements OnDestroy {\n\tprivate readonly collections = new Set<SyncCollection<unknown>>();\n\n\tconnect<T>(options: SyncCollectionOptions<T>) {\n\t\tconst data = signal<T[]>([]);\n\t\tconst status = signal<SyncCollectionStatus>('connecting');\n\t\tconst error = signal<unknown>(undefined);\n\n\t\tlet collection: SyncCollection<T> | null = null;\n\n\t\tif (typeof window !== 'undefined') {\n\t\t\tcollection = createSyncCollection<T>(options);\n\t\t\tthis.collections.add(collection as SyncCollection<unknown>);\n\t\t\tconst apply = (state: {\n\t\t\t\tdata: T[];\n\t\t\t\tstatus: SyncCollectionStatus;\n\t\t\t\terror: unknown;\n\t\t\t}) => {\n\t\t\t\tdata.set(state.data);\n\t\t\t\tstatus.set(state.status);\n\t\t\t\terror.set(state.error);\n\t\t\t};\n\t\t\tapply(collection.get());\n\t\t\tcollection.subscribe(apply);\n\t\t}\n\n\t\tconst mutate = <R = unknown>(\n\t\t\tmutateOptions: MutateOptions<T>\n\t\t): Promise<R> =>\n\t\t\tcollection\n\t\t\t\t? collection.mutate<R>(mutateOptions)\n\t\t\t\t: Promise.reject(new Error('sync collection is not ready'));\n\n\t\treturn {\n\t\t\tdata: computed(() => data()),\n\t\t\terror: computed(() => error()),\n\t\t\tmutate,\n\t\t\tstatus: computed(() => status())\n\t\t};\n\t}\n\n\tngOnDestroy() {\n\t\tfor (const collection of this.collections) {\n\t\t\tcollection.close();\n\t\t}\n\t\tthis.collections.clear();\n\t}\n}\n",
|
|
6
|
+
"import type { ServerFrame } from '../engine/connection';\nimport type { RowKey } from '../engine/types';\n\nexport type { ServerFrame } from '../engine/connection';\n\nexport type SyncCollectionStatus = 'connecting' | 'ready' | 'closed';\n\nexport type SyncCollectionState<T> = {\n\t/** Visible rows: the server state with pending optimistic mutations applied. */\n\tdata: T[];\n\t/** Connection/sync status. */\n\tstatus: SyncCollectionStatus;\n\t/** Last error message from the server, or `undefined`. */\n\terror: unknown;\n};\n\n/** A working set a mutation's optimistic effect edits in place. */\nexport type OptimisticDraft<T> = {\n\t/** Insert or replace a row by key. */\n\tset: (row: T) => void;\n\t/** Remove a row by key. */\n\tdelete: (key: RowKey) => void;\n};\n\nexport type MutateOptions<T> = {\n\t/** Registered server mutation name. */\n\tname: string;\n\t/** Arguments forwarded to the mutation handler. */\n\targs?: unknown;\n\t/**\n\t * Apply this mutation's effect to the local set immediately for instant UI.\n\t * Reverted automatically if the server rejects it. Omit for a non-optimistic\n\t * mutation (UI updates only once the authoritative diff arrives).\n\t */\n\toptimistic?: (draft: OptimisticDraft<T>) => void;\n};\n\n/** A pending mutation persisted for replay across reloads. */\nexport type PendingMutationRecord = {\n\tmutationId: number;\n\tname: string;\n\targs: unknown;\n};\n\n/**\n * Durable storage for the pending-mutation queue, so unconfirmed mutations\n * survive a page reload (offline). The queue is replayed when the socket\n * connects; records are dropped as they're acked.\n */\nexport type MutationStorage = {\n\tload: () => PendingMutationRecord[] | Promise<PendingMutationRecord[]>;\n\tsave: (records: PendingMutationRecord[]) => void | Promise<void>;\n};\n\n/**\n * A {@link MutationStorage} backed by `localStorage` under `key`. No-ops where\n * `localStorage` is unavailable (e.g. SSR).\n */\nexport const localStorageMutationStorage = (key: string): MutationStorage => ({\n\tload: () => {\n\t\tconst raw = globalThis.localStorage?.getItem(key);\n\t\treturn raw ? (JSON.parse(raw) as PendingMutationRecord[]) : [];\n\t},\n\tsave: (records) => {\n\t\tglobalThis.localStorage?.setItem(key, JSON.stringify(records));\n\t}\n});\n\nexport type SyncCollectionOptions<T> = {\n\t/** WebSocket URL of the {@link syncSocket} endpoint (e.g. `ws://host/sync/ws`). */\n\turl: string;\n\t/** Registered collection name to subscribe to. */\n\tcollection: string;\n\t/** Query params forwarded to the server collection's hydrate/match/authorize. */\n\tparams?: unknown;\n\t/** Row identity, used to apply diffs and optimistic edits. Defaults to `row.id`. */\n\tkey?: (row: T) => RowKey;\n\t/** WebSocket implementation; defaults to the global one (pass for tests/SSR). */\n\twebSocketImpl?: typeof WebSocket;\n\t/**\n\t * Base reconnect delay (ms), doubled each attempt up to `maxReconnectMs`.\n\t * Set 0 to disable auto-reconnect. Defaults to 500.\n\t */\n\treconnectMs?: number;\n\t/** Maximum reconnect backoff (ms). Defaults to 10000. */\n\tmaxReconnectMs?: number;\n\t/**\n\t * Persist the pending-mutation queue so it survives a reload (offline) and\n\t * replays on connect. See {@link localStorageMutationStorage}.\n\t */\n\tstorage?: MutationStorage;\n\t/** Called with each server error message. */\n\tonError?: (error: unknown) => void;\n};\n\nexport type SyncCollection<T> = {\n\t/** Current state snapshot (stable reference until the next change). */\n\tget: () => SyncCollectionState<T>;\n\t/** Subscribe to state changes; returns an unsubscribe. */\n\tsubscribe: (\n\t\tlistener: (state: SyncCollectionState<T>) => void\n\t) => () => void;\n\t/**\n\t * Run a server mutation, optionally applying it optimistically. Resolves with\n\t * the server's result on ack, rejects (and rolls back) on reject. Pending\n\t * mutations are replayed when the socket reconnects, so they survive a drop.\n\t */\n\tmutate: <R = unknown>(options: MutateOptions<T>) => Promise<R>;\n\t/** Unsubscribe on the server, close the socket, and stop reconnecting. */\n\tclose: () => void;\n};\n\n// One store subscribes to exactly one collection, so a fixed frame id suffices.\nconst SUBSCRIPTION_ID = 's';\n\ntype PendingMutation<T> = {\n\tmutationId: number;\n\tname: string;\n\targs: unknown;\n\toptimistic?: (draft: OptimisticDraft<T>) => void;\n\tresolve: (result: unknown) => void;\n\treject: (error: unknown) => void;\n};\n\n/**\n * A live collection backed by the WebSocket sync engine. Reads: connect,\n * subscribe, apply the server's snapshot then row-level diffs, re-sync on\n * reconnect. Writes: {@link SyncCollection.mutate} applies an optimistic overlay\n * immediately, sends the mutation, and reconciles on ack (drop the overlay — the\n * authoritative diff already arrived) or reject (roll back). Framework-agnostic\n * (`get` + `subscribe`).\n *\n * Mutations are replayed on reconnect, so make server mutations idempotent —\n * delivery is at-least-once if an ack is lost across a drop.\n */\nexport const createSyncCollection = <T>(\n\toptions: SyncCollectionOptions<T>\n): SyncCollection<T> => {\n\tconst key = options.key ?? ((row: T) => (row as { id: RowKey }).id);\n\tconst reconnectMs = options.reconnectMs ?? 500;\n\tconst maxReconnectMs = options.maxReconnectMs ?? 10_000;\n\tconst Impl = options.webSocketImpl ?? globalThis.WebSocket;\n\tif (!Impl) {\n\t\tthrow new Error(\n\t\t\t'createSyncCollection requires WebSocket. Run in a browser or pass webSocketImpl.'\n\t\t);\n\t}\n\n\t// Server-authoritative rows; `pending` is the optimistic overlay on top.\n\tconst confirmed = new Map<RowKey, T>();\n\tconst pending: PendingMutation<T>[] = [];\n\tlet mutationSeq = 0;\n\n\tlet state: SyncCollectionState<T> = {\n\t\tdata: [],\n\t\tstatus: 'connecting',\n\t\terror: undefined\n\t};\n\tconst listeners = new Set<(state: SyncCollectionState<T>) => void>();\n\tconst setState = (patch: Partial<SyncCollectionState<T>>) => {\n\t\tstate = { ...state, ...patch };\n\t\tfor (const listener of listeners) {\n\t\t\tlistener(state);\n\t\t}\n\t};\n\n\t/** Recompute visible rows = confirmed + pending optimistic effects. */\n\tconst recompute = (patch: Partial<SyncCollectionState<T>> = {}) => {\n\t\tconst working = new Map(confirmed);\n\t\tconst draft: OptimisticDraft<T> = {\n\t\t\tset: (row) => working.set(key(row), row),\n\t\t\tdelete: (rowKey) => working.delete(rowKey)\n\t\t};\n\t\tfor (const mutation of pending) {\n\t\t\tmutation.optimistic?.(draft);\n\t\t}\n\t\tsetState({ ...patch, data: [...working.values()] });\n\t};\n\n\tlet socket: WebSocket | undefined;\n\tlet connected = false;\n\tlet closed = false;\n\tlet attempt = 0;\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | undefined;\n\t// Highest change-feed version applied; sent as `since` to resume on reconnect.\n\tlet appliedVersion = 0;\n\n\tconst persist = () => {\n\t\tvoid options.storage?.save(\n\t\t\tpending.map((mutation) => ({\n\t\t\t\tmutationId: mutation.mutationId,\n\t\t\t\tname: mutation.name,\n\t\t\t\targs: mutation.args\n\t\t\t}))\n\t\t);\n\t};\n\n\tconst settlePending = (mutationId: number) => {\n\t\tconst index = pending.findIndex(\n\t\t\t(mutation) => mutation.mutationId === mutationId\n\t\t);\n\t\tif (index === -1) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst [mutation] = pending.splice(index, 1);\n\t\tpersist();\n\t\treturn mutation;\n\t};\n\n\tconst applyFrame = (frame: ServerFrame<T>) => {\n\t\tif (frame.type === 'snapshot') {\n\t\t\tconfirmed.clear();\n\t\t\tfor (const row of frame.rows) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\tif (frame.version !== undefined) {\n\t\t\t\tappliedVersion = frame.version;\n\t\t\t}\n\t\t\trecompute({ status: 'ready', error: undefined });\n\t\t} else if (frame.type === 'diff') {\n\t\t\tfor (const row of frame.removed) {\n\t\t\t\tconfirmed.delete(key(row));\n\t\t\t}\n\t\t\tfor (const row of frame.added) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\tfor (const row of frame.changed) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\tif (frame.version !== undefined) {\n\t\t\t\tappliedVersion = Math.max(appliedVersion, frame.version);\n\t\t\t}\n\t\t\trecompute();\n\t\t} else if (frame.type === 'error') {\n\t\t\tsetState({ error: frame.message });\n\t\t\toptions.onError?.(frame.message);\n\t\t} else if (frame.type === 'ack') {\n\t\t\t// The authoritative diff already arrived (ordered before the ack), so\n\t\t\t// dropping the overlay leaves the confirmed row in place — no flicker.\n\t\t\tconst mutation = settlePending(frame.mutationId);\n\t\t\tif (mutation !== undefined) {\n\t\t\t\trecompute();\n\t\t\t\tmutation.resolve(frame.result);\n\t\t\t}\n\t\t} else if (frame.type === 'reject') {\n\t\t\t// roll the optimistic overlay back.\n\t\t\tconst mutation = settlePending(frame.mutationId);\n\t\t\tif (mutation !== undefined) {\n\t\t\t\trecompute();\n\t\t\t\tmutation.reject(new Error(String(frame.message)));\n\t\t\t}\n\t\t}\n\t\t// A `frame` (multi-collection batch) never reaches a single-collection\n\t\t// store — that's the multiplexed createSyncClient's job — so ignore it.\n\t};\n\n\tconst sendMutate = (mutation: PendingMutation<T>) => {\n\t\tif (connected) {\n\t\t\tsocket?.send(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\ttype: 'mutate',\n\t\t\t\t\tmutationId: mutation.mutationId,\n\t\t\t\t\tname: mutation.name,\n\t\t\t\t\targs: mutation.args\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t};\n\n\tconst connect = () => {\n\t\tif (closed) {\n\t\t\treturn;\n\t\t}\n\t\tsetState({ status: 'connecting' });\n\t\tconst ws = new Impl(options.url);\n\t\tsocket = ws;\n\t\tws.onopen = () => {\n\t\t\tattempt = 0;\n\t\t\tconnected = true;\n\t\t\tws.send(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\ttype: 'subscribe',\n\t\t\t\t\tid: SUBSCRIPTION_ID,\n\t\t\t\t\tcollection: options.collection,\n\t\t\t\t\tparams: options.params,\n\t\t\t\t\t// Resume from what we've applied (catch-up instead of snapshot).\n\t\t\t\t\tsince: appliedVersion > 0 ? appliedVersion : undefined\n\t\t\t\t})\n\t\t\t);\n\t\t\t// Replay anything still pending across the (re)connect.\n\t\t\tfor (const mutation of pending) {\n\t\t\t\tsendMutate(mutation);\n\t\t\t}\n\t\t};\n\t\tws.onmessage = (event) => {\n\t\t\ttry {\n\t\t\t\tapplyFrame(JSON.parse(event.data as string) as ServerFrame<T>);\n\t\t\t} catch {\n\t\t\t\t// ignore non-JSON frames\n\t\t\t}\n\t\t};\n\t\tws.onclose = () => {\n\t\t\tconnected = false;\n\t\t\tif (closed || reconnectMs <= 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst delay = Math.min(reconnectMs * 2 ** attempt, maxReconnectMs);\n\t\t\tattempt += 1;\n\t\t\treconnectTimer = setTimeout(connect, delay);\n\t\t};\n\t};\n\n\tconnect();\n\n\t// Reload recovery: re-queue persisted unconfirmed mutations and replay them.\n\t// They carry no optimistic effect or promise (the fresh snapshot is\n\t// authoritative); resending produces the server diffs that bring them in.\n\tconst hydratePersisted = async () => {\n\t\tif (options.storage === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tconst records = await options.storage.load();\n\t\tfor (const record of records) {\n\t\t\tif (pending.some((m) => m.mutationId === record.mutationId)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpending.push({\n\t\t\t\tmutationId: record.mutationId,\n\t\t\t\tname: record.name,\n\t\t\t\targs: record.args,\n\t\t\t\tresolve: () => {},\n\t\t\t\treject: () => {}\n\t\t\t});\n\t\t\tmutationSeq = Math.max(mutationSeq, record.mutationId);\n\t\t}\n\t\tif (connected) {\n\t\t\tfor (const mutation of pending) {\n\t\t\t\tsendMutate(mutation);\n\t\t\t}\n\t\t}\n\t};\n\tvoid hydratePersisted();\n\n\treturn {\n\t\tget: () => state,\n\t\tsubscribe: (listener) => {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t\tmutate: <R = unknown>(mutateOptions: MutateOptions<T>) =>\n\t\t\tnew Promise<R>((resolve, reject) => {\n\t\t\t\tconst mutation: PendingMutation<T> = {\n\t\t\t\t\tmutationId: (mutationSeq += 1),\n\t\t\t\t\tname: mutateOptions.name,\n\t\t\t\t\targs: mutateOptions.args,\n\t\t\t\t\toptimistic: mutateOptions.optimistic,\n\t\t\t\t\tresolve: (result) => resolve(result as R),\n\t\t\t\t\treject\n\t\t\t\t};\n\t\t\t\tpending.push(mutation);\n\t\t\t\tpersist();\n\t\t\t\trecompute(); // apply the optimistic overlay immediately\n\t\t\t\tsendMutate(mutation);\n\t\t\t}),\n\t\tclose: () => {\n\t\t\tif (closed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tclosed = true;\n\t\t\tconnected = false;\n\t\t\tif (reconnectTimer !== undefined) {\n\t\t\t\tclearTimeout(reconnectTimer);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tsocket?.send(\n\t\t\t\t\tJSON.stringify({ type: 'unsubscribe', id: SUBSCRIPTION_ID })\n\t\t\t\t);\n\t\t\t\tsocket?.close();\n\t\t\t} catch {\n\t\t\t\t// socket already closing/closed\n\t\t\t}\n\t\t\t// Fail any still-pending mutations so their promises don't hang.\n\t\t\tfor (const mutation of pending.splice(0)) {\n\t\t\t\tmutation.reject(new Error('sync collection closed'));\n\t\t\t}\n\t\t\tpersist();\n\t\t\tsetState({ status: 'closed' });\n\t\t\tlisteners.clear();\n\t\t}\n\t};\n};\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "5D661943E00CCAFF64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { OnDestroy } from '@angular/core';
|
|
2
|
+
import type { MutateOptions, SyncCollectionOptions, SyncCollectionStatus } from '../client/syncCollection';
|
|
3
|
+
/**
|
|
4
|
+
* Angular binding for live sync-engine collections (the Tier 3 store). Inject
|
|
5
|
+
* the service and call `connect(options)` to get `data`/`status`/`error` signals
|
|
6
|
+
* maintained from the WebSocket diff stream, plus an optimistic `mutate`. All
|
|
7
|
+
* opened collections close on the service's destroy.
|
|
8
|
+
*
|
|
9
|
+
* SSR-safe: the socket only opens in a browser, so server rendering is inert.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SyncCollectionService implements OnDestroy {
|
|
12
|
+
private readonly collections;
|
|
13
|
+
connect<T>(options: SyncCollectionOptions<T>): {
|
|
14
|
+
data: import("@angular/core").Signal<T[]>;
|
|
15
|
+
error: import("@angular/core").Signal<unknown>;
|
|
16
|
+
mutate: <R = unknown>(mutateOptions: MutateOptions<T>) => Promise<R>;
|
|
17
|
+
status: import("@angular/core").Signal<SyncCollectionStatus>;
|
|
18
|
+
};
|
|
19
|
+
ngOnDestroy(): void;
|
|
20
|
+
}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,31 +1,13 @@
|
|
|
1
|
-
import type { ReactiveEvent } from '../reactiveHub';
|
|
2
1
|
export type { ReactiveEvent } from '../reactiveHub';
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* EventSource implementation to use. Defaults to the global one; pass a polyfill
|
|
16
|
-
* for non-browser runtimes.
|
|
17
|
-
*/
|
|
18
|
-
eventSourceImpl?: typeof EventSource;
|
|
19
|
-
};
|
|
20
|
-
export type SyncSubscriber = {
|
|
21
|
-
close: () => void;
|
|
22
|
-
/** The underlying EventSource, for advanced listeners. */
|
|
23
|
-
source: EventSource;
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* Subscribe a browser to the server's {@link ReactiveHub} over SSE. `onEvent` fires
|
|
27
|
-
* whenever a subscribed topic is published — the cue to refetch (or read the pushed
|
|
28
|
-
* payload) instead of polling. EventSource reconnects automatically on transient
|
|
29
|
-
* network drops.
|
|
30
|
-
*/
|
|
31
|
-
export declare const createSyncSubscriber: ({ topics, onEvent, url, onOpen, onError, withCredentials, eventSourceImpl }: SyncSubscriberOptions) => SyncSubscriber;
|
|
2
|
+
export { createSyncSubscriber } from './subscriber';
|
|
3
|
+
export type { SyncSubscriber, SyncSubscriberOptions } from './subscriber';
|
|
4
|
+
export { createLiveQuery, jsonFetcher } from './liveQuery';
|
|
5
|
+
export type { LiveQuery, LiveQueryOptions, LiveQueryState } from './liveQuery';
|
|
6
|
+
export { createSyncCollection, localStorageMutationStorage } from './syncCollection';
|
|
7
|
+
export type { MutateOptions, MutationStorage, OptimisticDraft, PendingMutationRecord, SyncCollection, SyncCollectionOptions, SyncCollectionState, SyncCollectionStatus } from './syncCollection';
|
|
8
|
+
export { createPresence } from './presence';
|
|
9
|
+
export type { PresenceClient, PresenceClientOptions, PresenceMember } from './presence';
|
|
10
|
+
export { createSyncClient } from './syncClient';
|
|
11
|
+
export type { SyncClient, SyncClientOptions, SyncCollectionHandle, SyncCollectionHandleOptions } from './syncClient';
|
|
12
|
+
export { syncStore, unwrapEden } from './syncStore';
|
|
13
|
+
export type { MutationMap, SyncStore, SyncStoreOptions, SyncStoreState, SyncStoreStatus } from './syncStore';
|