@absolutejs/sync 0.0.1 → 0.1.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 +264 -24
- package/dist/adapters/drizzle/index.d.ts +17 -0
- package/dist/adapters/drizzle/index.js +128 -0
- package/dist/adapters/drizzle/index.js.map +12 -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 +8 -30
- package/dist/client/index.js +744 -3
- package/dist/client/index.js.map +8 -4
- package/dist/client/liveQuery.d.ts +75 -0
- package/dist/client/subscriber.d.ts +30 -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/collection.d.ts +87 -0
- package/dist/engine/connection.d.ts +71 -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 +34 -0
- package/dist/engine/index.js +1269 -0
- package/dist/engine/index.js.map +20 -0
- package/dist/engine/materializedView.d.ts +53 -0
- package/dist/engine/mutation.d.ts +30 -0
- package/dist/engine/pollingSource.d.ts +42 -0
- package/dist/engine/routes.d.ts +40 -0
- package/dist/engine/socket.d.ts +64 -0
- package/dist/engine/syncEngine.d.ts +100 -0
- package/dist/engine/types.d.ts +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +160 -2
- package/dist/index.js.map +7 -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 +102 -6
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { CollectionContext, CollectionDefinition, JoinCollectionDefinition } from './collection';
|
|
2
|
+
import type { GraphCollectionDefinition } from './graph';
|
|
3
|
+
import type { MutationDefinition } from './mutation';
|
|
4
|
+
import type { ChangeSource, RowChange, ViewDiff } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when `authorize` denies a subscribe or a mutation. The message names
|
|
7
|
+
* the denied action; the message always starts with "Not authorized".
|
|
8
|
+
*/
|
|
9
|
+
export declare class UnauthorizedError extends Error {
|
|
10
|
+
constructor(subject: string);
|
|
11
|
+
}
|
|
12
|
+
export type SubscribeArgs<T, P, Ctx> = {
|
|
13
|
+
/** Registered collection name. */
|
|
14
|
+
collection: string;
|
|
15
|
+
/** Query params (e.g. a filter value); passed to hydrate/match/authorize. */
|
|
16
|
+
params: P;
|
|
17
|
+
/** Caller context (e.g. session); passed to hydrate/match/authorize. */
|
|
18
|
+
ctx: Ctx;
|
|
19
|
+
/** Receives every non-empty diff (with its version) after the initial reply. */
|
|
20
|
+
onDiff: (diff: ViewDiff<T>, version: number) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Resume from a version the client already applied. When the change log still
|
|
23
|
+
* covers `(since, now]` for a single-table collection, the engine replies with
|
|
24
|
+
* a catch-up diff instead of a full snapshot; otherwise it falls back to a
|
|
25
|
+
* snapshot.
|
|
26
|
+
*/
|
|
27
|
+
since?: number;
|
|
28
|
+
};
|
|
29
|
+
export type Subscription<T> = {
|
|
30
|
+
/** The result set at subscribe time — a snapshot (empty when resuming). */
|
|
31
|
+
initial: T[];
|
|
32
|
+
/** Catch-up diff when resuming via `since` (instead of `initial`). */
|
|
33
|
+
catchup?: ViewDiff<T>;
|
|
34
|
+
/** The engine version this reply brings the client up to. */
|
|
35
|
+
version: number;
|
|
36
|
+
/** Stop receiving diffs and release the view. */
|
|
37
|
+
unsubscribe: () => void;
|
|
38
|
+
};
|
|
39
|
+
export type SyncEngine = {
|
|
40
|
+
/** Register a collection definition (see {@link defineCollection}). */
|
|
41
|
+
register: <T, P = void, Ctx = CollectionContext>(collection: CollectionDefinition<T, P, Ctx>) => void;
|
|
42
|
+
/** Register an incremental join collection (see {@link defineJoinCollection}). */
|
|
43
|
+
registerJoin: <L, R, Out, P = void, Ctx = CollectionContext>(collection: JoinCollectionDefinition<L, R, Out, P, Ctx>) => void;
|
|
44
|
+
/** Register an operator-graph collection (see {@link defineGraphCollection}). */
|
|
45
|
+
registerGraph: <Out, P = void, Ctx = CollectionContext>(collection: GraphCollectionDefinition<Out, P, Ctx>) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Open a live subscription: authorize, hydrate the initial set, and stream
|
|
48
|
+
* diffs as changes arrive. Rejects with {@link UnauthorizedError} on deny.
|
|
49
|
+
*/
|
|
50
|
+
subscribe: <T, P = void, Ctx = CollectionContext>(args: SubscribeArgs<T, P, Ctx>) => Promise<Subscription<T>>;
|
|
51
|
+
/**
|
|
52
|
+
* One-shot read: authorize and return a collection's current rows without
|
|
53
|
+
* subscribing. Powers an Eden-typed HTTP hydrate route (and SSR). Rejects
|
|
54
|
+
* with {@link UnauthorizedError} on deny.
|
|
55
|
+
*/
|
|
56
|
+
hydrate: (collection: string, params: unknown, ctx: unknown) => Promise<unknown[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Feed a committed change to `table` into the engine, fanning the resulting
|
|
59
|
+
* diff to every live subscription of every collection that reads that table.
|
|
60
|
+
* Call after a mutation, or wire a {@link ChangeSource} via `connectSource`.
|
|
61
|
+
* Single-table subscriptions diff the row; multi-table / refetch ones
|
|
62
|
+
* re-hydrate.
|
|
63
|
+
*/
|
|
64
|
+
applyChange: <T>(table: string, change: RowChange<T>) => Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Connect a change source (e.g. a CDC adapter): its emitted changes flow into
|
|
67
|
+
* `applyChange`. Resolves to a disconnect function that stops the source.
|
|
68
|
+
*/
|
|
69
|
+
connectSource: (source: ChangeSource) => Promise<() => Promise<void>>;
|
|
70
|
+
/** Active subscription count, optionally for one collection. */
|
|
71
|
+
subscriptionCount: (collection?: string) => number;
|
|
72
|
+
/** Register a mutation definition (see {@link defineMutation}). */
|
|
73
|
+
registerMutation: <Args, Ctx = CollectionContext, Result = unknown>(mutation: MutationDefinition<Args, Ctx, Result>) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Run a registered mutation: authorize, invoke its handler (which writes and
|
|
76
|
+
* emits changes via `applyChange`), and resolve with the handler's result.
|
|
77
|
+
* Rejects with {@link UnauthorizedError} on deny, or an error for an unknown
|
|
78
|
+
* mutation / a handler throw. Drive this from the transport's mutate frame.
|
|
79
|
+
*/
|
|
80
|
+
runMutation: (name: string, args: unknown, ctx: unknown) => Promise<unknown>;
|
|
81
|
+
};
|
|
82
|
+
export type SyncEngineOptions = {
|
|
83
|
+
/**
|
|
84
|
+
* How many recent changes to retain for resumable reconnects. A client that
|
|
85
|
+
* reconnects within this window gets a catch-up diff; beyond it, a fresh
|
|
86
|
+
* snapshot. Defaults to 1024.
|
|
87
|
+
*/
|
|
88
|
+
changeLogSize?: number;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The Tier 3 sync engine: a registry of collections plus the view syncer. It is
|
|
92
|
+
* transport-agnostic — `subscribe` returns the initial snapshot and an
|
|
93
|
+
* `onDiff` stream, which an Elysia/SSE layer wires to a connection, and
|
|
94
|
+
* `applyChange` is the change feed you drive from your mutations.
|
|
95
|
+
*
|
|
96
|
+
* Access control is first-class: every subscribe runs the collection's
|
|
97
|
+
* `authorize`, and a collection's `match`/`hydrate` scope rows to the caller, so
|
|
98
|
+
* a change to a row the caller can't see never reaches them.
|
|
99
|
+
*/
|
|
100
|
+
export declare const createSyncEngine: (options?: SyncEngineOptions) => SyncEngine;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types shared across the Tier 3 sync engine (server-side). Deliberately
|
|
3
|
+
* ORM-agnostic: the engine operates on plain row objects, a key function, and a
|
|
4
|
+
* predicate — the Drizzle/Prisma adapters and the transport layer build on top.
|
|
5
|
+
*/
|
|
6
|
+
/** A scalar that identifies a row within a collection. */
|
|
7
|
+
export type RowKey = string | number | bigint;
|
|
8
|
+
/** The kind of row-level change flowing through the engine's change feed. */
|
|
9
|
+
export type RowOp = 'insert' | 'update' | 'delete';
|
|
10
|
+
/** One row-level change: a row that was inserted, updated, or deleted. */
|
|
11
|
+
export type RowChange<T> = {
|
|
12
|
+
op: RowOp;
|
|
13
|
+
/**
|
|
14
|
+
* The affected row. For `insert`/`update` it is the new row value; for
|
|
15
|
+
* `delete` only the key field(s) need be present.
|
|
16
|
+
*/
|
|
17
|
+
row: T;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The delta a change makes to a query's result set: rows that entered (`added`),
|
|
21
|
+
* left (`removed`), or stayed in the set but changed value (`changed`). The
|
|
22
|
+
* `removed` rows are the values the view last held for those keys.
|
|
23
|
+
*/
|
|
24
|
+
export type ViewDiff<T> = {
|
|
25
|
+
added: T[];
|
|
26
|
+
removed: T[];
|
|
27
|
+
changed: T[];
|
|
28
|
+
};
|
|
29
|
+
/** Report a committed row change on `table` into the engine. */
|
|
30
|
+
export type EmitChange = (table: string, change: RowChange<unknown>) => void | Promise<void>;
|
|
31
|
+
/** A committed change parsed from a CDC source, ready to feed the engine. */
|
|
32
|
+
export type ParsedChange = {
|
|
33
|
+
table: string;
|
|
34
|
+
change: RowChange<unknown>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A pluggable source of committed row changes — the seam for catching writes the
|
|
38
|
+
* mutation API didn't make (CDC: Postgres LISTEN/NOTIFY or logical replication,
|
|
39
|
+
* MySQL binlog, SQLite update hooks). `start` begins emitting via the supplied
|
|
40
|
+
* callback; `stop` tears it down. Wire one with {@link SyncEngine.connectSource}.
|
|
41
|
+
*/
|
|
42
|
+
export type ChangeSource = {
|
|
43
|
+
start: (emit: EmitChange) => void | Promise<void>;
|
|
44
|
+
stop: () => void | Promise<void>;
|
|
45
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,5 @@ export { createReactiveHub } from './reactiveHub';
|
|
|
4
4
|
export type { ReactiveEvent, ReactiveHub, ReactiveListener } from './reactiveHub';
|
|
5
5
|
export { sync } from './plugin';
|
|
6
6
|
export type { SyncPluginOptions, SyncRequestContext } from './plugin';
|
|
7
|
+
export { syncSocket } from './engine/socket';
|
|
8
|
+
export type { SyncSocketOptions } from './engine/socket';
|
package/dist/index.js
CHANGED
|
@@ -67,6 +67,7 @@ var createWriteBehindCache = (options) => {
|
|
|
67
67
|
};
|
|
68
68
|
};
|
|
69
69
|
// src/reactiveHub.ts
|
|
70
|
+
var SYNC_OPEN_TOPIC = "@absolutejs/sync:open";
|
|
70
71
|
var createReactiveHub = () => {
|
|
71
72
|
const subscriptions = new Set;
|
|
72
73
|
const matches = (subscription, topic) => {
|
|
@@ -141,7 +142,7 @@ var sync = ({
|
|
|
141
142
|
`);
|
|
142
143
|
};
|
|
143
144
|
send({
|
|
144
|
-
topic:
|
|
145
|
+
topic: SYNC_OPEN_TOPIC,
|
|
145
146
|
at: Date.now(),
|
|
146
147
|
payload: { topics }
|
|
147
148
|
});
|
|
@@ -166,11 +167,168 @@ var sync = ({
|
|
|
166
167
|
}
|
|
167
168
|
});
|
|
168
169
|
});
|
|
170
|
+
// src/engine/socket.ts
|
|
171
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
172
|
+
|
|
173
|
+
// src/engine/connection.ts
|
|
174
|
+
var parseFrame = (raw) => {
|
|
175
|
+
let value = raw;
|
|
176
|
+
if (typeof value === "string") {
|
|
177
|
+
try {
|
|
178
|
+
value = JSON.parse(value);
|
|
179
|
+
} catch {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (typeof value !== "object" || value === null) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const frame = value;
|
|
187
|
+
if (frame.type === "subscribe") {
|
|
188
|
+
return typeof frame.id === "string" && typeof frame.collection === "string" ? {
|
|
189
|
+
type: "subscribe",
|
|
190
|
+
id: frame.id,
|
|
191
|
+
collection: frame.collection,
|
|
192
|
+
params: frame.params,
|
|
193
|
+
since: typeof frame.since === "number" ? frame.since : undefined
|
|
194
|
+
} : undefined;
|
|
195
|
+
}
|
|
196
|
+
if (frame.type === "unsubscribe") {
|
|
197
|
+
return typeof frame.id === "string" ? { type: "unsubscribe", id: frame.id } : undefined;
|
|
198
|
+
}
|
|
199
|
+
if (frame.type === "mutate") {
|
|
200
|
+
return typeof frame.mutationId === "number" && typeof frame.name === "string" ? {
|
|
201
|
+
type: "mutate",
|
|
202
|
+
mutationId: frame.mutationId,
|
|
203
|
+
name: frame.name,
|
|
204
|
+
args: frame.args
|
|
205
|
+
} : undefined;
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
};
|
|
209
|
+
var createSyncConnection = ({
|
|
210
|
+
engine,
|
|
211
|
+
ctx,
|
|
212
|
+
send
|
|
213
|
+
}) => {
|
|
214
|
+
const subscriptions = new Map;
|
|
215
|
+
const handle = async (raw) => {
|
|
216
|
+
const frame = parseFrame(raw);
|
|
217
|
+
if (frame === undefined) {
|
|
218
|
+
send({ type: "error", message: "Malformed sync frame" });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (frame.type === "mutate") {
|
|
222
|
+
try {
|
|
223
|
+
const result = await engine.runMutation(frame.name, frame.args, ctx);
|
|
224
|
+
send({ type: "ack", mutationId: frame.mutationId, result });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
send({
|
|
227
|
+
type: "reject",
|
|
228
|
+
mutationId: frame.mutationId,
|
|
229
|
+
message: error instanceof Error ? error.message : String(error)
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (frame.type === "unsubscribe") {
|
|
235
|
+
subscriptions.get(frame.id)?.unsubscribe();
|
|
236
|
+
subscriptions.delete(frame.id);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (subscriptions.has(frame.id)) {
|
|
240
|
+
send({
|
|
241
|
+
type: "error",
|
|
242
|
+
id: frame.id,
|
|
243
|
+
message: `Subscription id "${frame.id}" already in use`
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const subscription = await engine.subscribe({
|
|
249
|
+
collection: frame.collection,
|
|
250
|
+
params: frame.params,
|
|
251
|
+
ctx,
|
|
252
|
+
since: frame.since,
|
|
253
|
+
onDiff: (diff, diffVersion) => {
|
|
254
|
+
send({
|
|
255
|
+
type: "diff",
|
|
256
|
+
id: frame.id,
|
|
257
|
+
added: diff.added,
|
|
258
|
+
removed: diff.removed,
|
|
259
|
+
changed: diff.changed,
|
|
260
|
+
version: diffVersion
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
subscriptions.set(frame.id, subscription);
|
|
265
|
+
if (subscription.catchup !== undefined) {
|
|
266
|
+
send({
|
|
267
|
+
type: "diff",
|
|
268
|
+
id: frame.id,
|
|
269
|
+
added: subscription.catchup.added,
|
|
270
|
+
removed: subscription.catchup.removed,
|
|
271
|
+
changed: subscription.catchup.changed,
|
|
272
|
+
version: subscription.version
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
send({
|
|
276
|
+
type: "snapshot",
|
|
277
|
+
id: frame.id,
|
|
278
|
+
rows: subscription.initial,
|
|
279
|
+
version: subscription.version
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
send({
|
|
284
|
+
type: "error",
|
|
285
|
+
id: frame.id,
|
|
286
|
+
message: error instanceof Error ? error.message : String(error)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const close = () => {
|
|
291
|
+
for (const subscription of subscriptions.values()) {
|
|
292
|
+
subscription.unsubscribe();
|
|
293
|
+
}
|
|
294
|
+
subscriptions.clear();
|
|
295
|
+
};
|
|
296
|
+
return { handle, close };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/engine/socket.ts
|
|
300
|
+
var syncSocket = ({
|
|
301
|
+
engine,
|
|
302
|
+
path = "/sync/ws",
|
|
303
|
+
resolveContext
|
|
304
|
+
}) => {
|
|
305
|
+
const connections = new Map;
|
|
306
|
+
return new Elysia2({ name: "@absolutejs/sync/socket" }).ws(path, {
|
|
307
|
+
async open(ws) {
|
|
308
|
+
const ctx = resolveContext ? await resolveContext(ws.data) : {};
|
|
309
|
+
connections.set(ws.id, createSyncConnection({
|
|
310
|
+
engine,
|
|
311
|
+
ctx,
|
|
312
|
+
send: (frame) => {
|
|
313
|
+
ws.send(frame);
|
|
314
|
+
}
|
|
315
|
+
}));
|
|
316
|
+
},
|
|
317
|
+
async message(ws, message) {
|
|
318
|
+
await connections.get(ws.id)?.handle(message);
|
|
319
|
+
},
|
|
320
|
+
close(ws) {
|
|
321
|
+
connections.get(ws.id)?.close();
|
|
322
|
+
connections.delete(ws.id);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
};
|
|
169
326
|
export {
|
|
327
|
+
syncSocket,
|
|
170
328
|
sync,
|
|
171
329
|
createWriteBehindCache,
|
|
172
330
|
createReactiveHub
|
|
173
331
|
};
|
|
174
332
|
|
|
175
|
-
//# debugId=
|
|
333
|
+
//# debugId=45E96CAA075E408764756E2164756E21
|
|
176
334
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/writeBehindCache.ts", "../src/reactiveHub.ts", "../src/plugin.ts"],
|
|
3
|
+
"sources": ["../src/writeBehindCache.ts", "../src/reactiveHub.ts", "../src/plugin.ts", "../src/engine/socket.ts", "../src/engine/connection.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"export type WriteBehindCacheOptions<K, V> = {\n\t/**\n\t * Read a value from the durable store on a cache miss. Called at most once per\n\t * key until the entry is evicted.\n\t */\n\tload: (key: K) => Promise<V | undefined> | V | undefined;\n\t/** Persist a value to the durable store. Runs in the background (write-behind). */\n\tpersist: (key: K, value: V) => Promise<void> | void;\n\t/** Remove a value from the durable store. */\n\tremove?: (key: K) => Promise<void> | void;\n\t/**\n\t * Coalesce writes: each key persists at most once per window. A burst of\n\t * `set`s collapses into a single durable write. Defaults to 250ms.\n\t */\n\tdebounceMs?: number;\n\t/**\n\t * After a key persists, return true to drop it from the in-memory cache so the\n\t * cache stays bounded to \"hot\" entries (e.g. evict terminal sessions). The next\n\t * `get` reloads it via `load`. Defaults to never evicting.\n\t */\n\tevict?: (value: V, key: K) => boolean;\n\t/**\n\t * Called when a background persist throws. The cache stays authoritative and the\n\t * key re-persists on its next `set`, so a transient durable-store blip does not\n\t * drop live state. Defaults to a no-op.\n\t */\n\tonPersistError?: (error: unknown, key: K) => void;\n};\n\nexport type WriteBehindCache<K, V> = {\n\t/** Cached value, or load-through from the durable store on a miss. */\n\tget: (key: K) => Promise<V | undefined>;\n\t/** Cached value only — synchronous, never touches the durable store. */\n\tpeek: (key: K) => V | undefined;\n\thas: (key: K) => boolean;\n\t/** Write to memory immediately and schedule a coalesced durable persist. */\n\tset: (key: K, value: V) => void;\n\t/** Drop from cache and the durable store. */\n\tdelete: (key: K) => Promise<void>;\n\tkeys: () => IterableIterator<K>;\n\tvalues: () => IterableIterator<V>;\n\tsize: () => number;\n\t/** Persist every pending key to the durable store now. Call on shutdown. */\n\tflush: () => Promise<void>;\n};\n\n/**\n * Wrap a durable store (Postgres, SQLite, Drizzle, Prisma, file, S3, an HTTP API …)\n * with an in-memory hot cache and write-behind persistence.\n *\n * Reads are served from memory; writes hit memory synchronously and are flushed to\n * the durable store in coalesced background batches. The durable store stays the\n * source of truth for history and cross-instance reads, while a latency-sensitive\n * hot path (a per-frame voice session, presence, cursors, game state) stays fast.\n *\n * This is the \"fast authoritative local state, durable persistence synced behind it\"\n * split a sync engine like Convex makes — without adopting a whole sync-engine\n * backend. Bring your own store via `load`/`persist`/`remove`.\n */\nexport const createWriteBehindCache = <K, V>(\n\toptions: WriteBehindCacheOptions<K, V>\n): WriteBehindCache<K, V> => {\n\tconst debounceMs = options.debounceMs ?? 250;\n\tconst cache = new Map<K, V>();\n\tconst timers = new Map<K, ReturnType<typeof setTimeout>>();\n\n\tconst persist = async (key: K) => {\n\t\ttimers.delete(key);\n\t\tconst value = cache.get(key);\n\t\tif (value === undefined) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait options.persist(key, value);\n\t\t\tif (options.evict?.(value, key)) {\n\t\t\t\tcache.delete(key);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\toptions.onPersistError?.(error, key);\n\t\t}\n\t};\n\n\tconst schedulePersist = (key: K) => {\n\t\tif (timers.has(key)) {\n\t\t\treturn;\n\t\t}\n\t\ttimers.set(\n\t\t\tkey,\n\t\t\tsetTimeout(() => {\n\t\t\t\tvoid persist(key);\n\t\t\t}, debounceMs)\n\t\t);\n\t};\n\n\treturn {\n\t\tget: async (key) => {\n\t\t\tconst cached = cache.get(key);\n\t\t\tif (cached !== undefined) {\n\t\t\t\treturn cached;\n\t\t\t}\n\t\t\tconst loaded = await options.load(key);\n\t\t\tif (loaded !== undefined) {\n\t\t\t\tcache.set(key, loaded);\n\t\t\t}\n\t\t\treturn loaded;\n\t\t},\n\t\tpeek: (key) => cache.get(key),\n\t\thas: (key) => cache.has(key),\n\t\tset: (key, value) => {\n\t\t\tcache.set(key, value);\n\t\t\tschedulePersist(key);\n\t\t},\n\t\tdelete: async (key) => {\n\t\t\tconst timer = timers.get(key);\n\t\t\tif (timer) {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\ttimers.delete(key);\n\t\t\t}\n\t\t\tcache.delete(key);\n\t\t\tawait options.remove?.(key);\n\t\t},\n\t\tkeys: () => cache.keys(),\n\t\tvalues: () => cache.values(),\n\t\tsize: () => cache.size,\n\t\tflush: async () => {\n\t\t\tfor (const timer of timers.values()) {\n\t\t\t\tclearTimeout(timer);\n\t\t\t}\n\t\t\ttimers.clear();\n\t\t\tawait Promise.all([...cache.keys()].map((key) => persist(key)));\n\t\t}\n\t};\n};\n",
|
|
6
|
-
"
|
|
7
|
-
"import { Elysia } from 'elysia';\nimport type { ReactiveEvent, ReactiveHub } from './reactiveHub';\n\nexport type SyncRequestContext = {\n\tquery: Record<string, string | undefined>;\n\trequest: Request;\n};\n\nexport type SyncPluginOptions = {\n\thub: ReactiveHub;\n\t/** Route the SSE stream is served from. Defaults to `/sync`. */\n\tpath?: string;\n\t/**\n\t * Which topics a connection subscribes to. Defaults to a comma-separated\n\t * `?topics=a,b,c` query param. Override to derive topics from the session,\n\t * params, or auth instead of trusting the client.\n\t */\n\tresolveTopics?: (context: SyncRequestContext) => string[];\n\t/**\n\t * Server→client heartbeat comment, so idle proxies don't drop the SSE stream.\n\t * Defaults to 25000ms.\n\t */\n\theartbeatMs?: number;\n};\n\nconst defaultResolveTopics = (context: SyncRequestContext) =>\n\t(context.query.topics ?? '')\n\t\t.split(',')\n\t\t.map((topic) => topic.trim())\n\t\t.filter(Boolean);\n\n/**\n * Elysia plugin that streams {@link ReactiveHub} events to browsers over Server-Sent\n * Events. Mount it once, point {@link createSyncSubscriber} at the same path, and\n * `hub.publish(topic)` from your mutations — subscribed clients are notified the\n * moment data changes, so they can refetch (or read the pushed payload) instead of\n * polling on a timer.\n */\nexport const sync = ({\n\thub,\n\tpath = '/sync',\n\tresolveTopics = defaultResolveTopics,\n\theartbeatMs = 25_000\n}: SyncPluginOptions) =>\n\tnew Elysia({ name: '@absolutejs/sync' }).get(path, (context) => {\n\t\tconst topics = resolveTopics({\n\t\t\tquery: context.query as Record<string, string | undefined>,\n\t\t\trequest: context.request\n\t\t});\n\t\tconst encoder = new TextEncoder();\n\n\t\tconst stream = new ReadableStream<Uint8Array>({\n\t\t\tstart(controller) {\n\t\t\t\tconst write = (chunk: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcontroller.enqueue(encoder.encode(chunk));\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// controller already closed by an abort race\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst send = (event: ReactiveEvent) => {\n\t\t\t\t\twrite(`data: ${JSON.stringify(event)}\\n\\n`);\n\t\t\t\t};\n\n\t\t\t\tsend({\n\t\t\t\t\ttopic:
|
|
6
|
+
"/**\n * Topic of the synthetic frame the SSE plugin emits when a stream opens (and\n * re-opens after a reconnect). Clients use it to tell \"the stream connected\"\n * apart from a real data-change event.\n */\nexport const SYNC_OPEN_TOPIC = '@absolutejs/sync:open';\n\nexport type ReactiveEvent<TPayload = unknown> = {\n\ttopic: string;\n\tat: number;\n\tpayload?: TPayload;\n};\n\nexport type ReactiveListener<TPayload = unknown> = (\n\tevent: ReactiveEvent<TPayload>\n) => void;\n\nexport type ReactiveHub = {\n\t/**\n\t * Notify every subscriber of `topic` (and any prefix-wildcard subscriber that\n\t * matches it). Call this from a mutation after the durable write commits.\n\t */\n\tpublish: (topic: string, payload?: unknown) => void;\n\t/**\n\t * Listen on one or more topics. A topic ending in `*` matches every topic that\n\t * starts with the prefix before it (e.g. `voice:session:*`). Returns an\n\t * unsubscribe function.\n\t */\n\tsubscribe: (topics: string[], listener: ReactiveListener) => () => void;\n\t/** Number of active subscribers, optionally for a single exact topic. */\n\tsubscriberCount: (topic?: string) => number;\n};\n\ntype Subscription = {\n\texact: Set<string>;\n\tprefixes: string[];\n\tlistener: ReactiveListener;\n};\n\n/**\n * An in-memory topic pub/sub for reactive, push-on-change updates.\n *\n * The pattern that replaces polling: a query/widget subscribes to the topics its\n * data depends on; a mutation `publish`es those topics after it writes; subscribers\n * are notified immediately and refetch (or receive the pushed payload) — instead of\n * every client hammering the server on a timer.\n *\n * Dependencies are explicit (you name the topics) rather than auto-tracked from a\n * query's read set — deliberately small, with no sandbox or query interception.\n * Pair it with the {@link sync} Elysia plugin to stream events to browsers over SSE.\n */\nexport const createReactiveHub = (): ReactiveHub => {\n\tconst subscriptions = new Set<Subscription>();\n\n\tconst matches = (subscription: Subscription, topic: string) => {\n\t\tif (subscription.exact.has(topic)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn subscription.prefixes.some((prefix) => topic.startsWith(prefix));\n\t};\n\n\treturn {\n\t\tpublish: (topic, payload) => {\n\t\t\tconst event: ReactiveEvent = { topic, at: Date.now(), payload };\n\t\t\tfor (const subscription of subscriptions) {\n\t\t\t\tif (matches(subscription, topic)) {\n\t\t\t\t\tsubscription.listener(event);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tsubscribe: (topics, listener) => {\n\t\t\tconst exact = new Set<string>();\n\t\t\tconst prefixes: string[] = [];\n\t\t\tfor (const topic of topics) {\n\t\t\t\tif (topic.endsWith('*')) {\n\t\t\t\t\tprefixes.push(topic.slice(0, -1));\n\t\t\t\t} else {\n\t\t\t\t\texact.add(topic);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst subscription: Subscription = { exact, prefixes, listener };\n\t\t\tsubscriptions.add(subscription);\n\t\t\treturn () => {\n\t\t\t\tsubscriptions.delete(subscription);\n\t\t\t};\n\t\t},\n\t\tsubscriberCount: (topic) => {\n\t\t\tif (topic === undefined) {\n\t\t\t\treturn subscriptions.size;\n\t\t\t}\n\t\t\tlet count = 0;\n\t\t\tfor (const subscription of subscriptions) {\n\t\t\t\tif (matches(subscription, topic)) {\n\t\t\t\t\tcount += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t}\n\t};\n};\n",
|
|
7
|
+
"import { Elysia } from 'elysia';\nimport { SYNC_OPEN_TOPIC } from './reactiveHub';\nimport type { ReactiveEvent, ReactiveHub } from './reactiveHub';\n\nexport type SyncRequestContext = {\n\tquery: Record<string, string | undefined>;\n\trequest: Request;\n};\n\nexport type SyncPluginOptions = {\n\thub: ReactiveHub;\n\t/** Route the SSE stream is served from. Defaults to `/sync`. */\n\tpath?: string;\n\t/**\n\t * Which topics a connection subscribes to. Defaults to a comma-separated\n\t * `?topics=a,b,c` query param. Override to derive topics from the session,\n\t * params, or auth instead of trusting the client.\n\t */\n\tresolveTopics?: (context: SyncRequestContext) => string[];\n\t/**\n\t * Server→client heartbeat comment, so idle proxies don't drop the SSE stream.\n\t * Defaults to 25000ms.\n\t */\n\theartbeatMs?: number;\n};\n\nconst defaultResolveTopics = (context: SyncRequestContext) =>\n\t(context.query.topics ?? '')\n\t\t.split(',')\n\t\t.map((topic) => topic.trim())\n\t\t.filter(Boolean);\n\n/**\n * Elysia plugin that streams {@link ReactiveHub} events to browsers over Server-Sent\n * Events. Mount it once, point {@link createSyncSubscriber} at the same path, and\n * `hub.publish(topic)` from your mutations — subscribed clients are notified the\n * moment data changes, so they can refetch (or read the pushed payload) instead of\n * polling on a timer.\n */\nexport const sync = ({\n\thub,\n\tpath = '/sync',\n\tresolveTopics = defaultResolveTopics,\n\theartbeatMs = 25_000\n}: SyncPluginOptions) =>\n\tnew Elysia({ name: '@absolutejs/sync' }).get(path, (context) => {\n\t\tconst topics = resolveTopics({\n\t\t\tquery: context.query as Record<string, string | undefined>,\n\t\t\trequest: context.request\n\t\t});\n\t\tconst encoder = new TextEncoder();\n\n\t\tconst stream = new ReadableStream<Uint8Array>({\n\t\t\tstart(controller) {\n\t\t\t\tconst write = (chunk: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcontroller.enqueue(encoder.encode(chunk));\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// controller already closed by an abort race\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst send = (event: ReactiveEvent) => {\n\t\t\t\t\twrite(`data: ${JSON.stringify(event)}\\n\\n`);\n\t\t\t\t};\n\n\t\t\t\tsend({\n\t\t\t\t\ttopic: SYNC_OPEN_TOPIC,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tpayload: { topics }\n\t\t\t\t});\n\n\t\t\t\tconst unsubscribe =\n\t\t\t\t\ttopics.length > 0 ? hub.subscribe(topics, send) : () => {};\n\t\t\t\tconst heartbeat = setInterval(\n\t\t\t\t\t() => write(': ping\\n\\n'),\n\t\t\t\t\theartbeatMs\n\t\t\t\t);\n\n\t\t\t\tcontext.request.signal.addEventListener(\n\t\t\t\t\t'abort',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tclearInterval(heartbeat);\n\t\t\t\t\t\tunsubscribe();\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tcontroller.close();\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// already closed\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true }\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\n\t\treturn new Response(stream, {\n\t\t\theaders: {\n\t\t\t\t'cache-control': 'no-cache, no-transform',\n\t\t\t\tconnection: 'keep-alive',\n\t\t\t\t'content-type': 'text/event-stream'\n\t\t\t}\n\t\t});\n\t});\n",
|
|
8
|
+
"import { Elysia } from 'elysia';\nimport { createSyncConnection } from './connection';\nimport type { SyncConnection } from './connection';\nimport type { SyncEngine } from './syncEngine';\n\nexport type SyncSocketOptions = {\n\t/** The sync engine whose collections this socket serves. */\n\tengine: SyncEngine;\n\t/** WebSocket route. Defaults to `/sync/ws`. */\n\tpath?: string;\n\t/**\n\t * Build the per-connection auth context from the upgrade request data\n\t * (`ws.data`: query, headers, cookies, and anything you `derive`d/`resolve`d\n\t * earlier in the chain). Whatever you return is the `ctx` passed to every\n\t * collection's `authorize`/`hydrate`/`match`. Defaults to an empty object.\n\t */\n\tresolveContext?: (\n\t\tdata: Record<string, unknown>\n\t) => unknown | Promise<unknown>;\n};\n\n/**\n * Elysia WebSocket plugin for the Tier 3 sync engine. One socket multiplexes any\n * number of collection subscriptions: the client sends `subscribe`/`unsubscribe`\n * frames and receives `snapshot`/`diff`/`error` frames (see\n * {@link createSyncConnection}). Mount it once and drive `engine.applyChange`\n * from your mutations.\n *\n * Uses Elysia's first-class `.ws()` rather than a hand-rolled stream — the\n * bidirectional channel carries both subscriptions and (later) mutations, and\n * `ws.send` serializes frames for us.\n */\nexport const syncSocket = ({\n\tengine,\n\tpath = '/sync/ws',\n\tresolveContext\n}: SyncSocketOptions) => {\n\tconst connections = new Map<string, SyncConnection>();\n\n\treturn new Elysia({ name: '@absolutejs/sync/socket' }).ws(path, {\n\t\tasync open(ws) {\n\t\t\tconst ctx = resolveContext\n\t\t\t\t? await resolveContext(ws.data as Record<string, unknown>)\n\t\t\t\t: {};\n\t\t\tconnections.set(\n\t\t\t\tws.id,\n\t\t\t\tcreateSyncConnection({\n\t\t\t\t\tengine,\n\t\t\t\t\tctx,\n\t\t\t\t\tsend: (frame) => {\n\t\t\t\t\t\tws.send(frame);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t);\n\t\t},\n\t\tasync message(ws, message) {\n\t\t\tawait connections.get(ws.id)?.handle(message);\n\t\t},\n\t\tclose(ws) {\n\t\t\tconnections.get(ws.id)?.close();\n\t\t\tconnections.delete(ws.id);\n\t\t}\n\t});\n};\n",
|
|
9
|
+
"import type { Subscription, SyncEngine } from './syncEngine';\n\n/**\n * Wire protocol for the sync-engine WebSocket. One connection multiplexes many\n * collection subscriptions, each tagged with a client-chosen `id`.\n */\n\n/** Client → server. */\nexport type ClientFrame =\n\t| {\n\t\t\ttype: 'subscribe';\n\t\t\tid: string;\n\t\t\tcollection: string;\n\t\t\tparams?: unknown;\n\t\t\t/** Resume from a version already applied (catch-up instead of snapshot). */\n\t\t\tsince?: number;\n\t }\n\t| { type: 'unsubscribe'; id: string }\n\t| { type: 'mutate'; mutationId: number; name: string; args?: unknown };\n\n/** Server → client. `version` is the change-feed watermark this frame brings. */\nexport type ServerFrame<T = unknown> =\n\t| { type: 'snapshot'; id: string; rows: T[]; version?: number }\n\t| {\n\t\t\ttype: 'diff';\n\t\t\tid: string;\n\t\t\tadded: T[];\n\t\t\tremoved: T[];\n\t\t\tchanged: T[];\n\t\t\tversion?: number;\n\t }\n\t| { type: 'error'; id?: string; message: string }\n\t| { type: 'ack'; mutationId: number; result?: unknown }\n\t| { type: 'reject'; mutationId: number; message: string };\n\nexport type SyncConnectionOptions = {\n\tengine: SyncEngine;\n\t/** Resolved auth context for this connection; passed to every subscribe. */\n\tctx: unknown;\n\t/** Send a frame to the client (the transport serializes it). */\n\tsend: (frame: ServerFrame) => void;\n};\n\nexport type SyncConnection = {\n\t/** Handle one client frame (a parsed object or a raw JSON string). */\n\thandle: (raw: unknown) => Promise<void>;\n\t/** Tear down every subscription on this connection (call on socket close). */\n\tclose: () => void;\n};\n\nconst parseFrame = (raw: unknown): ClientFrame | undefined => {\n\tlet value: unknown = raw;\n\tif (typeof value === 'string') {\n\t\ttry {\n\t\t\tvalue = JSON.parse(value);\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tif (typeof value !== 'object' || value === null) {\n\t\treturn undefined;\n\t}\n\tconst frame = value as {\n\t\ttype?: unknown;\n\t\tid?: unknown;\n\t\tcollection?: unknown;\n\t\tparams?: unknown;\n\t\tsince?: unknown;\n\t\tmutationId?: unknown;\n\t\tname?: unknown;\n\t\targs?: unknown;\n\t};\n\tif (frame.type === 'subscribe') {\n\t\treturn typeof frame.id === 'string' &&\n\t\t\ttypeof frame.collection === 'string'\n\t\t\t? {\n\t\t\t\t\ttype: 'subscribe',\n\t\t\t\t\tid: frame.id,\n\t\t\t\t\tcollection: frame.collection,\n\t\t\t\t\tparams: frame.params,\n\t\t\t\t\tsince:\n\t\t\t\t\t\ttypeof frame.since === 'number'\n\t\t\t\t\t\t\t? frame.since\n\t\t\t\t\t\t\t: undefined\n\t\t\t\t}\n\t\t\t: undefined;\n\t}\n\tif (frame.type === 'unsubscribe') {\n\t\treturn typeof frame.id === 'string'\n\t\t\t? { type: 'unsubscribe', id: frame.id }\n\t\t\t: undefined;\n\t}\n\tif (frame.type === 'mutate') {\n\t\treturn typeof frame.mutationId === 'number' &&\n\t\t\ttypeof frame.name === 'string'\n\t\t\t? {\n\t\t\t\t\ttype: 'mutate',\n\t\t\t\t\tmutationId: frame.mutationId,\n\t\t\t\t\tname: frame.name,\n\t\t\t\t\targs: frame.args\n\t\t\t\t}\n\t\t\t: undefined;\n\t}\n\treturn undefined;\n};\n\n/**\n * The per-connection protocol handler — transport-agnostic glue between a single\n * client socket and the {@link SyncEngine}. It owns that connection's\n * subscriptions: a `subscribe` frame authorizes + hydrates and replies with a\n * `snapshot`, then streams `diff` frames; `unsubscribe`/`close` release views.\n *\n * Pure (no WebSocket import) so it can be unit-tested with a fake `send`; the\n * Elysia `syncSocket` plugin is the thin adapter that feeds it socket events.\n */\nexport const createSyncConnection = ({\n\tengine,\n\tctx,\n\tsend\n}: SyncConnectionOptions): SyncConnection => {\n\tconst subscriptions = new Map<string, Subscription<unknown>>();\n\n\tconst handle = async (raw: unknown) => {\n\t\tconst frame = parseFrame(raw);\n\t\tif (frame === undefined) {\n\t\t\tsend({ type: 'error', message: 'Malformed sync frame' });\n\t\t\treturn;\n\t\t}\n\n\t\tif (frame.type === 'mutate') {\n\t\t\ttry {\n\t\t\t\tconst result = await engine.runMutation(\n\t\t\t\t\tframe.name,\n\t\t\t\t\tframe.args,\n\t\t\t\t\tctx\n\t\t\t\t);\n\t\t\t\t// The mutation's diffs were sent during runMutation (over the same\n\t\t\t\t// ordered socket), so the ack arrives after them.\n\t\t\t\tsend({ type: 'ack', mutationId: frame.mutationId, result });\n\t\t\t} catch (error) {\n\t\t\t\tsend({\n\t\t\t\t\ttype: 'reject',\n\t\t\t\t\tmutationId: frame.mutationId,\n\t\t\t\t\tmessage:\n\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (frame.type === 'unsubscribe') {\n\t\t\tsubscriptions.get(frame.id)?.unsubscribe();\n\t\t\tsubscriptions.delete(frame.id);\n\t\t\treturn;\n\t\t}\n\n\t\tif (subscriptions.has(frame.id)) {\n\t\t\tsend({\n\t\t\t\ttype: 'error',\n\t\t\t\tid: frame.id,\n\t\t\t\tmessage: `Subscription id \"${frame.id}\" already in use`\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst subscription = await engine.subscribe({\n\t\t\t\tcollection: frame.collection,\n\t\t\t\tparams: frame.params,\n\t\t\t\tctx,\n\t\t\t\tsince: frame.since,\n\t\t\t\tonDiff: (diff, diffVersion) => {\n\t\t\t\t\tsend({\n\t\t\t\t\t\ttype: 'diff',\n\t\t\t\t\t\tid: frame.id,\n\t\t\t\t\t\tadded: diff.added,\n\t\t\t\t\t\tremoved: diff.removed,\n\t\t\t\t\t\tchanged: diff.changed,\n\t\t\t\t\t\tversion: diffVersion\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t\tsubscriptions.set(frame.id, subscription);\n\t\t\t// No await between subscribe resolving and this send, so the initial\n\t\t\t// reply always precedes any diff for this subscription.\n\t\t\tif (subscription.catchup !== undefined) {\n\t\t\t\t// Resumed: a catch-up diff applied on top of the client's set.\n\t\t\t\tsend({\n\t\t\t\t\ttype: 'diff',\n\t\t\t\t\tid: frame.id,\n\t\t\t\t\tadded: subscription.catchup.added,\n\t\t\t\t\tremoved: subscription.catchup.removed,\n\t\t\t\t\tchanged: subscription.catchup.changed,\n\t\t\t\t\tversion: subscription.version\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tsend({\n\t\t\t\t\ttype: 'snapshot',\n\t\t\t\t\tid: frame.id,\n\t\t\t\t\trows: subscription.initial,\n\t\t\t\t\tversion: subscription.version\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tsend({\n\t\t\t\ttype: 'error',\n\t\t\t\tid: frame.id,\n\t\t\t\tmessage: error instanceof Error ? error.message : String(error)\n\t\t\t});\n\t\t}\n\t};\n\n\tconst close = () => {\n\t\tfor (const subscription of subscriptions.values()) {\n\t\t\tsubscription.unsubscribe();\n\t\t}\n\t\tsubscriptions.clear();\n\t};\n\n\treturn { handle, close };\n};\n"
|
|
8
10
|
],
|
|
9
|
-
"mappings": ";;AA2DO,IAAM,yBAAyB,CACrC,YAC4B;AAAA,EAC5B,MAAM,aAAa,QAAQ,cAAc;AAAA,EACzC,MAAM,QAAQ,IAAI;AAAA,EAClB,MAAM,SAAS,IAAI;AAAA,EAEnB,MAAM,UAAU,OAAO,QAAW;AAAA,IACjC,OAAO,OAAO,GAAG;AAAA,IACjB,MAAM,QAAQ,MAAM,IAAI,GAAG;AAAA,IAC3B,IAAI,UAAU,WAAW;AAAA,MACxB;AAAA,IACD;AAAA,IACA,IAAI;AAAA,MACH,MAAM,QAAQ,QAAQ,KAAK,KAAK;AAAA,MAChC,IAAI,QAAQ,QAAQ,OAAO,GAAG,GAAG;AAAA,QAChC,MAAM,OAAO,GAAG;AAAA,MACjB;AAAA,MACC,OAAO,OAAO;AAAA,MACf,QAAQ,iBAAiB,OAAO,GAAG;AAAA;AAAA;AAAA,EAIrC,MAAM,kBAAkB,CAAC,QAAW;AAAA,IACnC,IAAI,OAAO,IAAI,GAAG,GAAG;AAAA,MACpB;AAAA,IACD;AAAA,IACA,OAAO,IACN,KACA,WAAW,MAAM;AAAA,MACX,QAAQ,GAAG;AAAA,OACd,UAAU,CACd;AAAA;AAAA,EAGD,OAAO;AAAA,IACN,KAAK,OAAO,QAAQ;AAAA,MACnB,MAAM,SAAS,MAAM,IAAI,GAAG;AAAA,MAC5B,IAAI,WAAW,WAAW;AAAA,QACzB,OAAO;AAAA,MACR;AAAA,MACA,MAAM,SAAS,MAAM,QAAQ,KAAK,GAAG;AAAA,MACrC,IAAI,WAAW,WAAW;AAAA,QACzB,MAAM,IAAI,KAAK,MAAM;AAAA,MACtB;AAAA,MACA,OAAO;AAAA;AAAA,IAER,MAAM,CAAC,QAAQ,MAAM,IAAI,GAAG;AAAA,IAC5B,KAAK,CAAC,QAAQ,MAAM,IAAI,GAAG;AAAA,IAC3B,KAAK,CAAC,KAAK,UAAU;AAAA,MACpB,MAAM,IAAI,KAAK,KAAK;AAAA,MACpB,gBAAgB,GAAG;AAAA;AAAA,IAEpB,QAAQ,OAAO,QAAQ;AAAA,MACtB,MAAM,QAAQ,OAAO,IAAI,GAAG;AAAA,MAC5B,IAAI,OAAO;AAAA,QACV,aAAa,KAAK;AAAA,QAClB,OAAO,OAAO,GAAG;AAAA,MAClB;AAAA,MACA,MAAM,OAAO,GAAG;AAAA,MAChB,MAAM,QAAQ,SAAS,GAAG;AAAA;AAAA,IAE3B,MAAM,MAAM,MAAM,KAAK;AAAA,IACvB,QAAQ,MAAM,MAAM,OAAO;AAAA,IAC3B,MAAM,MAAM,MAAM;AAAA,IAClB,OAAO,YAAY;AAAA,MAClB,WAAW,SAAS,OAAO,OAAO,GAAG;AAAA,QACpC,aAAa,KAAK;AAAA,MACnB;AAAA,MACA,OAAO,MAAM;AAAA,MACb,MAAM,QAAQ,IAAI,CAAC,GAAG,MAAM,KAAK,CAAC,EAAE,IAAI,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC;AAAA;AAAA,EAEhE;AAAA;;
|
|
10
|
-
"debugId": "
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "45E96CAA075E408764756E2164756E21",
|
|
11
13
|
"names": []
|
|
12
14
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useSyncCollection } from './useSyncCollection';
|