@absolutejs/sync 0.1.0 → 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 +51 -34
- package/dist/adapters/drizzle/collection.d.ts +27 -0
- package/dist/adapters/drizzle/index.d.ts +3 -0
- package/dist/adapters/drizzle/index.js +139 -2
- package/dist/adapters/drizzle/index.js.map +5 -3
- package/dist/adapters/drizzle/predicate.d.ts +20 -0
- package/dist/angular/index.js +2 -2
- package/dist/angular/index.js.map +3 -3
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +357 -2
- package/dist/client/index.js.map +6 -4
- package/dist/client/presence.d.ts +37 -0
- package/dist/client/syncClient.d.ts +53 -0
- package/dist/engine/cluster.d.ts +41 -0
- package/dist/engine/connection.d.ts +33 -1
- package/dist/engine/index.d.ts +8 -2
- package/dist/engine/index.js +542 -37
- package/dist/engine/index.js.map +9 -6
- package/dist/engine/mutation.d.ts +39 -3
- package/dist/engine/presence.d.ts +46 -0
- package/dist/engine/reactive.d.ts +67 -0
- package/dist/engine/socket.d.ts +4 -1
- package/dist/engine/syncEngine.d.ts +33 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +175 -9
- package/dist/index.js.map +6 -5
- package/dist/react/index.js +2 -2
- package/dist/react/index.js.map +3 -3
- package/dist/svelte/index.js +2 -2
- package/dist/svelte/index.js.map +3 -3
- package/dist/vue/index.js +2 -2
- package/dist/vue/index.js.map +3 -3
- package/package.json +3 -1
|
@@ -1,11 +1,47 @@
|
|
|
1
1
|
import type { CollectionContext } from './collection';
|
|
2
2
|
import type { RowChange } from './types';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* How to persist a table — register one with {@link SyncEngine.registerWriter} so
|
|
5
|
+
* `insert`/`update`/`delete` on the mutation actions write to your store (any
|
|
6
|
+
* ORM) and emit the live change in one step. Each function returns the stored
|
|
7
|
+
* row so the emitted diff carries DB-generated fields (ids, timestamps).
|
|
8
|
+
*
|
|
9
|
+
* The third argument is the transaction handle from the engine's
|
|
10
|
+
* {@link TransactionRunner} (or `undefined` if none is configured) — write
|
|
11
|
+
* through it so a mutation's writes commit all-or-nothing.
|
|
12
|
+
*/
|
|
13
|
+
export type TableWriter<Row = any, Ctx = unknown, Tx = unknown> = {
|
|
14
|
+
insert: (data: any, ctx: Ctx, tx: Tx) => Promise<Row> | Row;
|
|
15
|
+
update: (data: any, ctx: Ctx, tx: Tx) => Promise<Row> | Row;
|
|
16
|
+
/** Persist the delete; receives the row/identifier passed to `actions.delete`. */
|
|
17
|
+
delete: (row: any, ctx: Ctx, tx: Tx) => Promise<unknown> | unknown;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Runs a function inside your database's transaction, threading the transaction
|
|
21
|
+
* handle to each {@link TableWriter}, so a mutation's writes commit
|
|
22
|
+
* all-or-nothing and the engine emits its diff only after the commit. Configure
|
|
23
|
+
* it on {@link createSyncEngine}. Examples:
|
|
24
|
+
*
|
|
25
|
+
* `(run) => db.transaction(run)` // Drizzle
|
|
26
|
+
* `(run) => prisma.$transaction(run)` // Prisma
|
|
27
|
+
*/
|
|
28
|
+
export type TransactionRunner = <R>(run: (tx: unknown) => Promise<R>) => Promise<R>;
|
|
29
|
+
/**
|
|
30
|
+
* Actions a mutation handler uses to write and publish changes.
|
|
31
|
+
*
|
|
32
|
+
* Prefer `insert`/`update`/`delete`: they persist via the table's registered
|
|
33
|
+
* {@link TableWriter} and emit the live change in one fused call, so you can't
|
|
34
|
+
* forget to go live (and the change always reflects the stored row). `change` is
|
|
35
|
+
* the lower-level escape hatch for when you wrote some other way.
|
|
7
36
|
*/
|
|
8
37
|
export type MutationActions = {
|
|
38
|
+
/** Persist a new row to `table` and emit it. Returns the stored row. */
|
|
39
|
+
insert: <Row = unknown>(table: string, data: unknown) => Promise<Row>;
|
|
40
|
+
/** Persist an update to `table` and emit it. Returns the stored row. */
|
|
41
|
+
update: <Row = unknown>(table: string, data: unknown) => Promise<Row>;
|
|
42
|
+
/** Persist a delete to `table` (pass the row or its key) and emit it. */
|
|
43
|
+
delete: (table: string, row: unknown) => Promise<void>;
|
|
44
|
+
/** Escape hatch: emit a change you persisted yourself (no writer call). */
|
|
9
45
|
change: <T>(collection: string, change: RowChange<T>) => Promise<void>;
|
|
10
46
|
};
|
|
11
47
|
export type MutationHandler<Args, Ctx, Result> = (args: Args, ctx: Ctx, actions: MutationActions) => Promise<Result> | Result;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence — ephemeral, room-scoped state shared over the live socket (who's
|
|
3
|
+
* online, who's typing, cursor positions). Unlike collections it is **not**
|
|
4
|
+
* persisted: it lives only while a member is joined, and a member's state is
|
|
5
|
+
* removed (and peers notified) the moment it leaves or its connection drops.
|
|
6
|
+
*
|
|
7
|
+
* A `room` is any string (a document id, a channel). Each `member` is one
|
|
8
|
+
* participant (typically one connection) with a `state` it owns and updates;
|
|
9
|
+
* everyone in the room sees the member set and its changes.
|
|
10
|
+
*/
|
|
11
|
+
export type PresenceMember<S = unknown> = {
|
|
12
|
+
id: string;
|
|
13
|
+
state: S;
|
|
14
|
+
};
|
|
15
|
+
/** What changed in a room: members that joined, updated state, or left. */
|
|
16
|
+
export type PresenceDiff<S = unknown> = {
|
|
17
|
+
joined: PresenceMember<S>[];
|
|
18
|
+
updated: PresenceMember<S>[];
|
|
19
|
+
left: string[];
|
|
20
|
+
};
|
|
21
|
+
export type PresenceHandle<S> = {
|
|
22
|
+
/** The room's members at join time (including this one). */
|
|
23
|
+
members: PresenceMember<S>[];
|
|
24
|
+
/** Replace this member's state and notify the rest of the room. */
|
|
25
|
+
set: (state: S) => void;
|
|
26
|
+
/** Leave the room (remove this member; notify peers). */
|
|
27
|
+
leave: () => void;
|
|
28
|
+
};
|
|
29
|
+
export type PresenceHub = {
|
|
30
|
+
/**
|
|
31
|
+
* Join `room` as `memberId` with `state`; `onDiff` receives every later change
|
|
32
|
+
* to the room (not this member's own join). Returns the current members and
|
|
33
|
+
* handles to update/leave.
|
|
34
|
+
*/
|
|
35
|
+
join: <S>(room: string, memberId: string, state: S, onDiff: (diff: PresenceDiff<S>) => void) => PresenceHandle<S>;
|
|
36
|
+
/** Snapshot a room's members without joining. */
|
|
37
|
+
members: <S = unknown>(room: string) => PresenceMember<S>[];
|
|
38
|
+
/** Number of members in a room (0 if none). */
|
|
39
|
+
count: (room: string) => number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Create an in-process presence hub. Transport-agnostic (no socket import): the
|
|
43
|
+
* sync connection wires client `presence-*` frames to it and tears down a
|
|
44
|
+
* connection's memberships on close.
|
|
45
|
+
*/
|
|
46
|
+
export declare const createPresenceHub: () => PresenceHub;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CollectionContext } from './collection';
|
|
2
|
+
import type { RowKey } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Read-set tracking — the reactor. You write a plain query function that reads
|
|
5
|
+
* through an instrumented `ctx.db`; the engine records which tables it touched
|
|
6
|
+
* and re-runs it (diffing old vs new) whenever any of those tables change. No
|
|
7
|
+
* hand-written `match`, no operator graph, no manual change emission for reads:
|
|
8
|
+
* write a query, it stays live. This is the BYO-database analogue of Convex's
|
|
9
|
+
* automatic read-set tracking — it works on your own DB because your reads go
|
|
10
|
+
* through the registered {@link TableReader}s.
|
|
11
|
+
*
|
|
12
|
+
* Granularity is table-level for now (a query that read a table re-runs on any
|
|
13
|
+
* change to it) — the full developer experience, and always correct; key/range
|
|
14
|
+
* precision is a later optimization.
|
|
15
|
+
*/
|
|
16
|
+
/** How to read a table for reactive queries — register with `registerReader`. */
|
|
17
|
+
export type TableReader<Ctx = unknown> = {
|
|
18
|
+
/** All rows of the table (the common case; filter in JS in your query). */
|
|
19
|
+
all: (ctx: Ctx) => Promise<Iterable<unknown>> | Iterable<unknown>;
|
|
20
|
+
/** Optional point lookup by key. */
|
|
21
|
+
get?: (key: RowKey, ctx: Ctx) => Promise<unknown> | unknown;
|
|
22
|
+
/**
|
|
23
|
+
* Row identity. Provide it to unlock **key-level** read tracking: a query that
|
|
24
|
+
* only `db.get`s specific rows re-runs solely when one of *those* rows changes
|
|
25
|
+
* (matched via this `key`), not on every change to the table. Omit and `get`
|
|
26
|
+
* falls back to a table-level dependency (coarser, still correct).
|
|
27
|
+
*/
|
|
28
|
+
key?: (row: unknown) => RowKey;
|
|
29
|
+
};
|
|
30
|
+
/** The instrumented data handle passed to a reactive query — reads are tracked. */
|
|
31
|
+
export type ReadHandle = {
|
|
32
|
+
/** Read all rows of `table` (records a full-table dependency). */
|
|
33
|
+
all: <T = unknown>(table: string) => Promise<T[]>;
|
|
34
|
+
/** Read one row of `table` by key (records a row-key dependency). */
|
|
35
|
+
get: <T = unknown>(table: string, key: RowKey) => Promise<T | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* Read the rows of `table` matching `predicate` (records a **range**
|
|
38
|
+
* dependency): the query re-runs only when a change matches the predicate now
|
|
39
|
+
* or was in the matched set before — not on every change to the table. Needs
|
|
40
|
+
* the table's reader to declare a `key`; without one it falls back to a
|
|
41
|
+
* full-table dependency. Prefer this over `all().filter(...)` for precision.
|
|
42
|
+
*/
|
|
43
|
+
where: <T = unknown>(table: string, predicate: (row: T) => boolean) => Promise<T[]>;
|
|
44
|
+
};
|
|
45
|
+
export type ReactiveQueryContext<P, Ctx> = {
|
|
46
|
+
/** Tracked reads — anything you read here becomes a live dependency. */
|
|
47
|
+
db: ReadHandle;
|
|
48
|
+
params: P;
|
|
49
|
+
ctx: Ctx;
|
|
50
|
+
};
|
|
51
|
+
export type ReactiveQueryDefinition<T, P = void, Ctx = CollectionContext> = {
|
|
52
|
+
name: string;
|
|
53
|
+
kind: 'reactive';
|
|
54
|
+
/** Compute the result set by reading through `ctx.db`; re-run on change. */
|
|
55
|
+
run: (context: ReactiveQueryContext<P, Ctx>) => Promise<T[]> | T[];
|
|
56
|
+
/** Result-row identity (used to diff re-runs). */
|
|
57
|
+
key: (row: T) => RowKey;
|
|
58
|
+
/** Access control; return false (or throw) to deny the subscription. */
|
|
59
|
+
authorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Define a reactive query: a function that reads through `ctx.db` and is kept
|
|
63
|
+
* live automatically by read-set tracking. Register it with
|
|
64
|
+
* {@link SyncEngine.registerReactive} (and the tables it reads with
|
|
65
|
+
* `registerReader`).
|
|
66
|
+
*/
|
|
67
|
+
export declare const defineReactiveQuery: <T, P = void, Ctx = CollectionContext>(definition: Omit<ReactiveQueryDefinition<T, P, Ctx>, "kind">) => ReactiveQueryDefinition<T, P, Ctx>;
|
package/dist/engine/socket.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Elysia } from 'elysia';
|
|
2
|
+
import type { PresenceHub } from './presence';
|
|
2
3
|
import type { SyncEngine } from './syncEngine';
|
|
3
4
|
export type SyncSocketOptions = {
|
|
4
5
|
/** The sync engine whose collections this socket serves. */
|
|
5
6
|
engine: SyncEngine;
|
|
6
7
|
/** WebSocket route. Defaults to `/sync/ws`. */
|
|
7
8
|
path?: string;
|
|
9
|
+
/** Optional presence hub; enables `presence-*` frames on this socket. */
|
|
10
|
+
presence?: PresenceHub;
|
|
8
11
|
/**
|
|
9
12
|
* Build the per-connection auth context from the upgrade request data
|
|
10
13
|
* (`ws.data`: query, headers, cookies, and anything you `derive`d/`resolve`d
|
|
@@ -24,7 +27,7 @@ export type SyncSocketOptions = {
|
|
|
24
27
|
* bidirectional channel carries both subscriptions and (later) mutations, and
|
|
25
28
|
* `ws.send` serializes frames for us.
|
|
26
29
|
*/
|
|
27
|
-
export declare const syncSocket: ({ engine, path, resolveContext }: SyncSocketOptions) => Elysia<"", {
|
|
30
|
+
export declare const syncSocket: ({ engine, path, resolveContext, presence }: SyncSocketOptions) => Elysia<"", {
|
|
28
31
|
decorator: {};
|
|
29
32
|
store: {};
|
|
30
33
|
derive: {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CollectionContext, CollectionDefinition, JoinCollectionDefinition } from './collection';
|
|
2
2
|
import type { GraphCollectionDefinition } from './graph';
|
|
3
|
-
import type { MutationDefinition } from './mutation';
|
|
3
|
+
import type { MutationDefinition, TableWriter, TransactionRunner } from './mutation';
|
|
4
|
+
import type { ReactiveQueryDefinition, TableReader } from './reactive';
|
|
5
|
+
import type { ClusterBus } from './cluster';
|
|
4
6
|
import type { ChangeSource, RowChange, ViewDiff } from './types';
|
|
5
7
|
/**
|
|
6
8
|
* Thrown when `authorize` denies a subscribe or a mutation. The message names
|
|
@@ -67,10 +69,33 @@ export type SyncEngine = {
|
|
|
67
69
|
* `applyChange`. Resolves to a disconnect function that stops the source.
|
|
68
70
|
*/
|
|
69
71
|
connectSource: (source: ChangeSource) => Promise<() => Promise<void>>;
|
|
72
|
+
/**
|
|
73
|
+
* Join a cluster (see {@link ClusterBus}): broadcast this instance's committed
|
|
74
|
+
* changes to peers and apply theirs locally, so subscribers on every instance
|
|
75
|
+
* stay live. Resolves to a disconnect function. Run once per instance.
|
|
76
|
+
*/
|
|
77
|
+
connectCluster: (bus: ClusterBus) => Promise<() => Promise<void>>;
|
|
70
78
|
/** Active subscription count, optionally for one collection. */
|
|
71
79
|
subscriptionCount: (collection?: string) => number;
|
|
72
80
|
/** Register a mutation definition (see {@link defineMutation}). */
|
|
73
81
|
registerMutation: <Args, Ctx = CollectionContext, Result = unknown>(mutation: MutationDefinition<Args, Ctx, Result>) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Register how to persist a `table` (any ORM), so a mutation's
|
|
84
|
+
* `actions.insert/update/delete` write to your store and emit the live change
|
|
85
|
+
* in one step — you can't write without going live. See {@link TableWriter}.
|
|
86
|
+
*/
|
|
87
|
+
registerWriter: <Row = unknown, Ctx = CollectionContext, Tx = unknown>(table: string, writer: TableWriter<Row, Ctx, Tx>) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Register a read-set-tracked reactive query (see {@link defineReactiveQuery}):
|
|
90
|
+
* it re-runs and re-pushes whenever any table it read changes — no `match`, no
|
|
91
|
+
* operator graph, no manual change emission.
|
|
92
|
+
*/
|
|
93
|
+
registerReactive: <T, P = void, Ctx = CollectionContext>(query: ReactiveQueryDefinition<T, P, Ctx>) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Teach the engine how to read a table for reactive queries' `ctx.db` (any
|
|
96
|
+
* ORM). Required for every table a reactive query reads.
|
|
97
|
+
*/
|
|
98
|
+
registerReader: <Ctx = CollectionContext>(table: string, reader: TableReader<Ctx>) => void;
|
|
74
99
|
/**
|
|
75
100
|
* Run a registered mutation: authorize, invoke its handler (which writes and
|
|
76
101
|
* emits changes via `applyChange`), and resolve with the handler's result.
|
|
@@ -86,6 +111,13 @@ export type SyncEngineOptions = {
|
|
|
86
111
|
* snapshot. Defaults to 1024.
|
|
87
112
|
*/
|
|
88
113
|
changeLogSize?: number;
|
|
114
|
+
/**
|
|
115
|
+
* Run every mutation inside your database's transaction (see
|
|
116
|
+
* {@link TransactionRunner}): the handler's writes commit all-or-nothing, and
|
|
117
|
+
* the engine emits the resulting diff only after the commit. Omit to run
|
|
118
|
+
* mutations without a transaction (each writer call is its own DB op).
|
|
119
|
+
*/
|
|
120
|
+
transaction?: TransactionRunner;
|
|
89
121
|
};
|
|
90
122
|
/**
|
|
91
123
|
* The Tier 3 sync engine: a registry of collections plus the view syncer. It is
|
package/dist/index.d.ts
CHANGED
|
@@ -6,3 +6,5 @@ export { sync } from './plugin';
|
|
|
6
6
|
export type { SyncPluginOptions, SyncRequestContext } from './plugin';
|
|
7
7
|
export { syncSocket } from './engine/socket';
|
|
8
8
|
export type { SyncSocketOptions } from './engine/socket';
|
|
9
|
+
export { createPresenceHub } from './engine/presence';
|
|
10
|
+
export type { PresenceDiff, PresenceHandle, PresenceHub, PresenceMember } from './engine/presence';
|
package/dist/index.js
CHANGED
|
@@ -204,14 +204,73 @@ var parseFrame = (raw) => {
|
|
|
204
204
|
args: frame.args
|
|
205
205
|
} : undefined;
|
|
206
206
|
}
|
|
207
|
+
if (frame.type === "presence-join") {
|
|
208
|
+
return typeof frame.room === "string" && typeof frame.memberId === "string" ? {
|
|
209
|
+
type: "presence-join",
|
|
210
|
+
room: frame.room,
|
|
211
|
+
memberId: frame.memberId,
|
|
212
|
+
state: frame.state
|
|
213
|
+
} : undefined;
|
|
214
|
+
}
|
|
215
|
+
if (frame.type === "presence-set") {
|
|
216
|
+
return typeof frame.room === "string" ? { type: "presence-set", room: frame.room, state: frame.state } : undefined;
|
|
217
|
+
}
|
|
218
|
+
if (frame.type === "presence-leave") {
|
|
219
|
+
return typeof frame.room === "string" ? { type: "presence-leave", room: frame.room } : undefined;
|
|
220
|
+
}
|
|
207
221
|
return;
|
|
208
222
|
};
|
|
209
223
|
var createSyncConnection = ({
|
|
210
224
|
engine,
|
|
211
225
|
ctx,
|
|
212
|
-
send
|
|
226
|
+
send,
|
|
227
|
+
presence
|
|
213
228
|
}) => {
|
|
214
229
|
const subscriptions = new Map;
|
|
230
|
+
const presenceRooms = new Map;
|
|
231
|
+
let pending = [];
|
|
232
|
+
let pendingVersion;
|
|
233
|
+
let flushScheduled = false;
|
|
234
|
+
const flush = () => {
|
|
235
|
+
if (pending.length === 0) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const diffs = pending;
|
|
239
|
+
const version = pendingVersion;
|
|
240
|
+
pending = [];
|
|
241
|
+
pendingVersion = undefined;
|
|
242
|
+
if (diffs.length === 1) {
|
|
243
|
+
const only = diffs[0];
|
|
244
|
+
send({
|
|
245
|
+
type: "diff",
|
|
246
|
+
id: only.id,
|
|
247
|
+
added: only.added,
|
|
248
|
+
removed: only.removed,
|
|
249
|
+
changed: only.changed,
|
|
250
|
+
version
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
send({ type: "frame", diffs, version });
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const scheduleFlush = () => {
|
|
257
|
+
if (flushScheduled) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
flushScheduled = true;
|
|
261
|
+
queueMicrotask(() => {
|
|
262
|
+
flushScheduled = false;
|
|
263
|
+
flush();
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
const bufferDiff = (diff, diffVersion) => {
|
|
267
|
+
if (pending.length > 0 && pendingVersion !== diffVersion) {
|
|
268
|
+
flush();
|
|
269
|
+
}
|
|
270
|
+
pending.push(diff);
|
|
271
|
+
pendingVersion = diffVersion;
|
|
272
|
+
scheduleFlush();
|
|
273
|
+
};
|
|
215
274
|
const handle = async (raw) => {
|
|
216
275
|
const frame = parseFrame(raw);
|
|
217
276
|
if (frame === undefined) {
|
|
@@ -221,6 +280,7 @@ var createSyncConnection = ({
|
|
|
221
280
|
if (frame.type === "mutate") {
|
|
222
281
|
try {
|
|
223
282
|
const result = await engine.runMutation(frame.name, frame.args, ctx);
|
|
283
|
+
flush();
|
|
224
284
|
send({ type: "ack", mutationId: frame.mutationId, result });
|
|
225
285
|
} catch (error) {
|
|
226
286
|
send({
|
|
@@ -236,6 +296,40 @@ var createSyncConnection = ({
|
|
|
236
296
|
subscriptions.delete(frame.id);
|
|
237
297
|
return;
|
|
238
298
|
}
|
|
299
|
+
if (frame.type === "presence-join") {
|
|
300
|
+
if (presence === undefined) {
|
|
301
|
+
send({ type: "error", message: "Presence is not enabled" });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
presenceRooms.get(frame.room)?.leave();
|
|
305
|
+
const handle2 = presence.join(frame.room, frame.memberId, frame.state, (diff) => {
|
|
306
|
+
send({
|
|
307
|
+
type: "presence",
|
|
308
|
+
room: frame.room,
|
|
309
|
+
joined: diff.joined,
|
|
310
|
+
updated: diff.updated,
|
|
311
|
+
left: diff.left
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
presenceRooms.set(frame.room, handle2);
|
|
315
|
+
send({
|
|
316
|
+
type: "presence",
|
|
317
|
+
room: frame.room,
|
|
318
|
+
joined: handle2.members,
|
|
319
|
+
updated: [],
|
|
320
|
+
left: []
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (frame.type === "presence-set") {
|
|
325
|
+
presenceRooms.get(frame.room)?.set(frame.state);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (frame.type === "presence-leave") {
|
|
329
|
+
presenceRooms.get(frame.room)?.leave();
|
|
330
|
+
presenceRooms.delete(frame.room);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
239
333
|
if (subscriptions.has(frame.id)) {
|
|
240
334
|
send({
|
|
241
335
|
type: "error",
|
|
@@ -251,14 +345,12 @@ var createSyncConnection = ({
|
|
|
251
345
|
ctx,
|
|
252
346
|
since: frame.since,
|
|
253
347
|
onDiff: (diff, diffVersion) => {
|
|
254
|
-
|
|
255
|
-
type: "diff",
|
|
348
|
+
bufferDiff({
|
|
256
349
|
id: frame.id,
|
|
257
350
|
added: diff.added,
|
|
258
351
|
removed: diff.removed,
|
|
259
|
-
changed: diff.changed
|
|
260
|
-
|
|
261
|
-
});
|
|
352
|
+
changed: diff.changed
|
|
353
|
+
}, diffVersion);
|
|
262
354
|
}
|
|
263
355
|
});
|
|
264
356
|
subscriptions.set(frame.id, subscription);
|
|
@@ -292,6 +384,10 @@ var createSyncConnection = ({
|
|
|
292
384
|
subscription.unsubscribe();
|
|
293
385
|
}
|
|
294
386
|
subscriptions.clear();
|
|
387
|
+
for (const handle2 of presenceRooms.values()) {
|
|
388
|
+
handle2.leave();
|
|
389
|
+
}
|
|
390
|
+
presenceRooms.clear();
|
|
295
391
|
};
|
|
296
392
|
return { handle, close };
|
|
297
393
|
};
|
|
@@ -300,7 +396,8 @@ var createSyncConnection = ({
|
|
|
300
396
|
var syncSocket = ({
|
|
301
397
|
engine,
|
|
302
398
|
path = "/sync/ws",
|
|
303
|
-
resolveContext
|
|
399
|
+
resolveContext,
|
|
400
|
+
presence
|
|
304
401
|
}) => {
|
|
305
402
|
const connections = new Map;
|
|
306
403
|
return new Elysia2({ name: "@absolutejs/sync/socket" }).ws(path, {
|
|
@@ -309,6 +406,7 @@ var syncSocket = ({
|
|
|
309
406
|
connections.set(ws.id, createSyncConnection({
|
|
310
407
|
engine,
|
|
311
408
|
ctx,
|
|
409
|
+
presence,
|
|
312
410
|
send: (frame) => {
|
|
313
411
|
ws.send(frame);
|
|
314
412
|
}
|
|
@@ -323,12 +421,80 @@ var syncSocket = ({
|
|
|
323
421
|
}
|
|
324
422
|
});
|
|
325
423
|
};
|
|
424
|
+
// src/engine/presence.ts
|
|
425
|
+
var createPresenceHub = () => {
|
|
426
|
+
const rooms = new Map;
|
|
427
|
+
const roomMembers = (room) => {
|
|
428
|
+
const members = rooms.get(room);
|
|
429
|
+
if (members === undefined) {
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
return [...members].map(([id, member]) => ({
|
|
433
|
+
id,
|
|
434
|
+
state: member.state
|
|
435
|
+
}));
|
|
436
|
+
};
|
|
437
|
+
const notify = (room, diff, exceptId) => {
|
|
438
|
+
const members = rooms.get(room);
|
|
439
|
+
if (members === undefined) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
for (const [id, member] of members) {
|
|
443
|
+
if (id !== exceptId) {
|
|
444
|
+
member.onDiff(diff);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
return {
|
|
449
|
+
join: (room, memberId, state, onDiff) => {
|
|
450
|
+
let members = rooms.get(room);
|
|
451
|
+
if (members === undefined) {
|
|
452
|
+
members = new Map;
|
|
453
|
+
rooms.set(room, members);
|
|
454
|
+
}
|
|
455
|
+
members.set(memberId, {
|
|
456
|
+
state,
|
|
457
|
+
onDiff
|
|
458
|
+
});
|
|
459
|
+
notify(room, { joined: [{ id: memberId, state }], updated: [], left: [] }, memberId);
|
|
460
|
+
const snapshot = roomMembers(room);
|
|
461
|
+
return {
|
|
462
|
+
members: snapshot,
|
|
463
|
+
set: (next) => {
|
|
464
|
+
const current = rooms.get(room)?.get(memberId);
|
|
465
|
+
if (current === undefined) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
current.state = next;
|
|
469
|
+
notify(room, {
|
|
470
|
+
joined: [],
|
|
471
|
+
updated: [{ id: memberId, state: next }],
|
|
472
|
+
left: []
|
|
473
|
+
}, memberId);
|
|
474
|
+
},
|
|
475
|
+
leave: () => {
|
|
476
|
+
const roomNow = rooms.get(room);
|
|
477
|
+
if (roomNow?.delete(memberId) !== true) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
notify(room, { joined: [], updated: [], left: [memberId] }, memberId);
|
|
481
|
+
if (roomNow.size === 0) {
|
|
482
|
+
rooms.delete(room);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
},
|
|
487
|
+
members: (room) => roomMembers(room),
|
|
488
|
+
count: (room) => rooms.get(room)?.size ?? 0
|
|
489
|
+
};
|
|
490
|
+
};
|
|
326
491
|
export {
|
|
327
492
|
syncSocket,
|
|
328
493
|
sync,
|
|
329
494
|
createWriteBehindCache,
|
|
330
|
-
createReactiveHub
|
|
495
|
+
createReactiveHub,
|
|
496
|
+
createPresenceHub
|
|
331
497
|
};
|
|
332
498
|
|
|
333
|
-
//# debugId=
|
|
499
|
+
//# debugId=0AE0815066390A8564756E2164756E21
|
|
334
500
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/writeBehindCache.ts", "../src/reactiveHub.ts", "../src/plugin.ts", "../src/engine/socket.ts", "../src/engine/connection.ts"],
|
|
3
|
+
"sources": ["../src/writeBehindCache.ts", "../src/reactiveHub.ts", "../src/plugin.ts", "../src/engine/socket.ts", "../src/engine/connection.ts", "../src/engine/presence.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
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
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
|
+
"import { Elysia } from 'elysia';\nimport { createSyncConnection } from './connection';\nimport type { SyncConnection } from './connection';\nimport type { PresenceHub } from './presence';\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/** Optional presence hub; enables `presence-*` frames on this socket. */\n\tpresence?: PresenceHub;\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\tpresence\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\tpresence,\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 { PresenceHandle, PresenceHub, PresenceMember } from './presence';\nimport 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\t| { type: 'presence-join'; room: string; memberId: string; state: unknown }\n\t| { type: 'presence-set'; room: string; state: unknown }\n\t| { type: 'presence-leave'; room: string };\n\n/** One subscription's delta within a {@link ServerFrame} `frame`. */\nexport type FrameDiff<T = unknown> = {\n\tid: string;\n\tadded: T[];\n\tremoved: T[];\n\tchanged: T[];\n};\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| {\n\t\t\t// One atomic batch (e.g. a transactional mutation) that touched several\n\t\t\t// subscriptions — bundled into one message so the client applies them in\n\t\t\t// a single frame, never showing a torn cross-collection intermediate.\n\t\t\ttype: 'frame';\n\t\t\tversion?: number;\n\t\t\tdiffs: FrameDiff<T>[];\n\t }\n\t| {\n\t\t\t// A presence room changed: members joined, updated state, or left.\n\t\t\ttype: 'presence';\n\t\t\troom: string;\n\t\t\tjoined: PresenceMember<T>[];\n\t\t\tupdated: PresenceMember<T>[];\n\t\t\tleft: string[];\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\t/** Optional presence hub; enables the `presence-*` frames (see createPresenceHub). */\n\tpresence?: PresenceHub;\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\troom?: unknown;\n\t\tmemberId?: unknown;\n\t\tstate?: 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\tif (frame.type === 'presence-join') {\n\t\treturn typeof frame.room === 'string' &&\n\t\t\ttypeof frame.memberId === 'string'\n\t\t\t? {\n\t\t\t\t\ttype: 'presence-join',\n\t\t\t\t\troom: frame.room,\n\t\t\t\t\tmemberId: frame.memberId,\n\t\t\t\t\tstate: frame.state\n\t\t\t\t}\n\t\t\t: undefined;\n\t}\n\tif (frame.type === 'presence-set') {\n\t\treturn typeof frame.room === 'string'\n\t\t\t? { type: 'presence-set', room: frame.room, state: frame.state }\n\t\t\t: undefined;\n\t}\n\tif (frame.type === 'presence-leave') {\n\t\treturn typeof frame.room === 'string'\n\t\t\t? { type: 'presence-leave', room: frame.room }\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\tpresence\n}: SyncConnectionOptions): SyncConnection => {\n\tconst subscriptions = new Map<string, Subscription<unknown>>();\n\t// This connection's presence memberships (one per room), torn down on close.\n\tconst presenceRooms = new Map<string, PresenceHandle<unknown>>();\n\n\t// Diffs from one atomic batch (a mutation, or a single applyChange) arrive via\n\t// onDiff synchronously and share a version. Buffer them and flush as one\n\t// message: a lone diff stays a plain `diff` (so single-collection clients are\n\t// unchanged); several become one `frame` the client applies atomically.\n\tlet pending: FrameDiff[] = [];\n\tlet pendingVersion: number | undefined;\n\tlet flushScheduled = false;\n\n\tconst flush = () => {\n\t\tif (pending.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst diffs = pending;\n\t\tconst version = pendingVersion;\n\t\tpending = [];\n\t\tpendingVersion = undefined;\n\t\tif (diffs.length === 1) {\n\t\t\tconst only = diffs[0]!;\n\t\t\tsend({\n\t\t\t\ttype: 'diff',\n\t\t\t\tid: only.id,\n\t\t\t\tadded: only.added,\n\t\t\t\tremoved: only.removed,\n\t\t\t\tchanged: only.changed,\n\t\t\t\tversion\n\t\t\t});\n\t\t} else {\n\t\t\tsend({ type: 'frame', diffs, version });\n\t\t}\n\t};\n\n\tconst scheduleFlush = () => {\n\t\tif (flushScheduled) {\n\t\t\treturn;\n\t\t}\n\t\tflushScheduled = true;\n\t\tqueueMicrotask(() => {\n\t\t\tflushScheduled = false;\n\t\t\tflush();\n\t\t});\n\t};\n\n\tconst bufferDiff = (diff: FrameDiff, diffVersion: number) => {\n\t\t// A new version means a new batch — flush the previous one first.\n\t\tif (pending.length > 0 && pendingVersion !== diffVersion) {\n\t\t\tflush();\n\t\t}\n\t\tpending.push(diff);\n\t\tpendingVersion = diffVersion;\n\t\tscheduleFlush();\n\t};\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 buffered during runMutation; flush them\n\t\t\t\t// (as one frame) before the ack so the ack always arrives after.\n\t\t\t\tflush();\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 (frame.type === 'presence-join') {\n\t\t\tif (presence === undefined) {\n\t\t\t\tsend({ type: 'error', message: 'Presence is not enabled' });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// A re-join replaces the prior membership for this room.\n\t\t\tpresenceRooms.get(frame.room)?.leave();\n\t\t\tconst handle = presence.join(\n\t\t\t\tframe.room,\n\t\t\t\tframe.memberId,\n\t\t\t\tframe.state,\n\t\t\t\t(diff) => {\n\t\t\t\t\tsend({\n\t\t\t\t\t\ttype: 'presence',\n\t\t\t\t\t\troom: frame.room,\n\t\t\t\t\t\tjoined: diff.joined,\n\t\t\t\t\t\tupdated: diff.updated,\n\t\t\t\t\t\tleft: diff.left\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t);\n\t\t\tpresenceRooms.set(frame.room, handle);\n\t\t\t// Initial snapshot to the joiner (peers got a `joined` diff instead).\n\t\t\tsend({\n\t\t\t\ttype: 'presence',\n\t\t\t\troom: frame.room,\n\t\t\t\tjoined: handle.members,\n\t\t\t\tupdated: [],\n\t\t\t\tleft: []\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tif (frame.type === 'presence-set') {\n\t\t\tpresenceRooms.get(frame.room)?.set(frame.state);\n\t\t\treturn;\n\t\t}\n\n\t\tif (frame.type === 'presence-leave') {\n\t\t\tpresenceRooms.get(frame.room)?.leave();\n\t\t\tpresenceRooms.delete(frame.room);\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\tbufferDiff(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: frame.id,\n\t\t\t\t\t\t\tadded: diff.added,\n\t\t\t\t\t\t\tremoved: diff.removed,\n\t\t\t\t\t\t\tchanged: diff.changed\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdiffVersion\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\t// Drop this connection's presence so peers see it leave (auto-cleanup).\n\t\tfor (const handle of presenceRooms.values()) {\n\t\t\thandle.leave();\n\t\t}\n\t\tpresenceRooms.clear();\n\t};\n\n\treturn { handle, close };\n};\n",
|
|
10
|
+
"/**\n * Presence — ephemeral, room-scoped state shared over the live socket (who's\n * online, who's typing, cursor positions). Unlike collections it is **not**\n * persisted: it lives only while a member is joined, and a member's state is\n * removed (and peers notified) the moment it leaves or its connection drops.\n *\n * A `room` is any string (a document id, a channel). Each `member` is one\n * participant (typically one connection) with a `state` it owns and updates;\n * everyone in the room sees the member set and its changes.\n */\n\nexport type PresenceMember<S = unknown> = { id: string; state: S };\n\n/** What changed in a room: members that joined, updated state, or left. */\nexport type PresenceDiff<S = unknown> = {\n\tjoined: PresenceMember<S>[];\n\tupdated: PresenceMember<S>[];\n\tleft: string[];\n};\n\nexport type PresenceHandle<S> = {\n\t/** The room's members at join time (including this one). */\n\tmembers: PresenceMember<S>[];\n\t/** Replace this member's state and notify the rest of the room. */\n\tset: (state: S) => void;\n\t/** Leave the room (remove this member; notify peers). */\n\tleave: () => void;\n};\n\nexport type PresenceHub = {\n\t/**\n\t * Join `room` as `memberId` with `state`; `onDiff` receives every later change\n\t * to the room (not this member's own join). Returns the current members and\n\t * handles to update/leave.\n\t */\n\tjoin: <S>(\n\t\troom: string,\n\t\tmemberId: string,\n\t\tstate: S,\n\t\tonDiff: (diff: PresenceDiff<S>) => void\n\t) => PresenceHandle<S>;\n\t/** Snapshot a room's members without joining. */\n\tmembers: <S = unknown>(room: string) => PresenceMember<S>[];\n\t/** Number of members in a room (0 if none). */\n\tcount: (room: string) => number;\n};\n\ntype RoomMember = {\n\tstate: unknown;\n\tonDiff: (diff: PresenceDiff<unknown>) => void;\n};\n\n/**\n * Create an in-process presence hub. Transport-agnostic (no socket import): the\n * sync connection wires client `presence-*` frames to it and tears down a\n * connection's memberships on close.\n */\nexport const createPresenceHub = (): PresenceHub => {\n\tconst rooms = new Map<string, Map<string, RoomMember>>();\n\n\tconst roomMembers = (room: string): PresenceMember<unknown>[] => {\n\t\tconst members = rooms.get(room);\n\t\tif (members === undefined) {\n\t\t\treturn [];\n\t\t}\n\t\treturn [...members].map(([id, member]) => ({\n\t\t\tid,\n\t\t\tstate: member.state\n\t\t}));\n\t};\n\n\t/** Notify everyone in `room` except the actor that caused the change. */\n\tconst notify = (\n\t\troom: string,\n\t\tdiff: PresenceDiff<unknown>,\n\t\texceptId: string\n\t) => {\n\t\tconst members = rooms.get(room);\n\t\tif (members === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const [id, member] of members) {\n\t\t\tif (id !== exceptId) {\n\t\t\t\tmember.onDiff(diff);\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tjoin: (room, memberId, state, onDiff) => {\n\t\t\tlet members = rooms.get(room);\n\t\t\tif (members === undefined) {\n\t\t\t\tmembers = new Map();\n\t\t\t\trooms.set(room, members);\n\t\t\t}\n\t\t\tmembers.set(memberId, {\n\t\t\t\tstate,\n\t\t\t\tonDiff: onDiff as (diff: PresenceDiff<unknown>) => void\n\t\t\t});\n\t\t\t// Peers learn this member joined; the joiner gets the snapshot instead.\n\t\t\tnotify(\n\t\t\t\troom,\n\t\t\t\t{ joined: [{ id: memberId, state }], updated: [], left: [] },\n\t\t\t\tmemberId\n\t\t\t);\n\t\t\tconst snapshot = roomMembers(room) as PresenceMember<\n\t\t\t\ttypeof state\n\t\t\t>[];\n\n\t\t\treturn {\n\t\t\t\tmembers: snapshot,\n\t\t\t\tset: (next) => {\n\t\t\t\t\tconst current = rooms.get(room)?.get(memberId);\n\t\t\t\t\tif (current === undefined) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tcurrent.state = next;\n\t\t\t\t\tnotify(\n\t\t\t\t\t\troom,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tjoined: [],\n\t\t\t\t\t\t\tupdated: [{ id: memberId, state: next }],\n\t\t\t\t\t\t\tleft: []\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmemberId\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tleave: () => {\n\t\t\t\t\tconst roomNow = rooms.get(room);\n\t\t\t\t\tif (roomNow?.delete(memberId) !== true) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tnotify(\n\t\t\t\t\t\troom,\n\t\t\t\t\t\t{ joined: [], updated: [], left: [memberId] },\n\t\t\t\t\t\tmemberId\n\t\t\t\t\t);\n\t\t\t\t\tif (roomNow.size === 0) {\n\t\t\t\t\t\trooms.delete(room);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\tmembers: (room) => roomMembers(room) as PresenceMember<never>[],\n\t\tcount: (room) => rooms.get(room)?.size ?? 0\n\t};\n};\n"
|
|
10
11
|
],
|
|
11
|
-
"mappings": "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",
|
|
12
|
-
"debugId": "
|
|
12
|
+
"mappings": "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",
|
|
13
|
+
"debugId": "0AE0815066390A8564756E2164756E21",
|
|
13
14
|
"names": []
|
|
14
15
|
}
|