meridian-server 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/dist/index.cjs +1680 -0
- package/dist/index.d.cts +658 -0
- package/dist/index.d.ts +658 -0
- package/dist/index.js +1659 -0
- package/package.json +34 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { ClientMessage, ServerMessage, SchemaDefinition, ConflictRecord, PermissionRules, CRDTOperation, ServerChange, StorageAdapterConfig, ConflictInfo } from 'meridian-shared';
|
|
2
|
+
export { CRDTOperation, ConflictRecord, SchemaDefinition, ServerChange, defineSchema, z } from 'meridian-shared';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Meridian Server — WebSocket Hub
|
|
7
|
+
*
|
|
8
|
+
* Manages WebSocket connections with:
|
|
9
|
+
* - Client authentication (pluggable JWT verifier)
|
|
10
|
+
* - Namespace isolation (multi-tenant)
|
|
11
|
+
* - Message routing (push/pull/subscribe/presence)
|
|
12
|
+
* - Heartbeat/ping-pong health checks
|
|
13
|
+
* - Auth token expiry tracking and refresh notifications
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface AuthResult {
|
|
17
|
+
userId: string;
|
|
18
|
+
namespace?: string;
|
|
19
|
+
expiresAt?: number;
|
|
20
|
+
}
|
|
21
|
+
interface WsHubConfig {
|
|
22
|
+
/** Port to listen on */
|
|
23
|
+
port: number;
|
|
24
|
+
/** Path for WebSocket endpoint */
|
|
25
|
+
path?: string;
|
|
26
|
+
/** Auth verifier — return user info or throw to reject */
|
|
27
|
+
auth?: (token: string) => Promise<AuthResult>;
|
|
28
|
+
/** Message handler */
|
|
29
|
+
onMessage: (clientId: string, message: ClientMessage, client: ConnectedClient) => void;
|
|
30
|
+
/** Disconnect handler */
|
|
31
|
+
onDisconnect?: (clientId: string) => void;
|
|
32
|
+
/** Subscribe handler — called when client subscribes with optional filter */
|
|
33
|
+
onSubscribe?: (clientId: string, collections: string[], filter?: Record<string, Record<string, unknown>>) => void;
|
|
34
|
+
/** Debug mode */
|
|
35
|
+
debug?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface ConnectedClient {
|
|
38
|
+
id: string;
|
|
39
|
+
ws: WebSocket;
|
|
40
|
+
userId: string | null;
|
|
41
|
+
namespace: string | null;
|
|
42
|
+
subscribedCollections: Set<string>;
|
|
43
|
+
authExpiresAt: number | null;
|
|
44
|
+
lastActivity: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* WebSocket connection hub for Meridian server.
|
|
48
|
+
*/
|
|
49
|
+
declare class WsHub {
|
|
50
|
+
private wss;
|
|
51
|
+
private readonly config;
|
|
52
|
+
private clients;
|
|
53
|
+
private heartbeatInterval;
|
|
54
|
+
private authCheckInterval;
|
|
55
|
+
constructor(config: WsHubConfig);
|
|
56
|
+
/**
|
|
57
|
+
* Start the WebSocket server.
|
|
58
|
+
*/
|
|
59
|
+
start(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Stop the WebSocket server.
|
|
62
|
+
*/
|
|
63
|
+
stop(): void;
|
|
64
|
+
private handleConnection;
|
|
65
|
+
private handleClientMessage;
|
|
66
|
+
/**
|
|
67
|
+
* Send a message to a specific client.
|
|
68
|
+
*/
|
|
69
|
+
sendTo(client: ConnectedClient, msg: ServerMessage): void;
|
|
70
|
+
/**
|
|
71
|
+
* Send a message to a client by ID.
|
|
72
|
+
*/
|
|
73
|
+
sendToId(clientId: string, msg: ServerMessage): void;
|
|
74
|
+
/**
|
|
75
|
+
* Broadcast a message to all clients subscribed to a collection.
|
|
76
|
+
* Excludes the sender.
|
|
77
|
+
*/
|
|
78
|
+
broadcastToCollection(collection: string, msg: ServerMessage, excludeClientId?: string, namespace?: string | null): void;
|
|
79
|
+
/**
|
|
80
|
+
* Broadcast a message to all connected clients.
|
|
81
|
+
*/
|
|
82
|
+
broadcastToAll(msg: ServerMessage, namespace?: string | null): void;
|
|
83
|
+
/**
|
|
84
|
+
* Get all connected client IDs.
|
|
85
|
+
*/
|
|
86
|
+
getClientIds(): string[];
|
|
87
|
+
/**
|
|
88
|
+
* Get a connected client by ID.
|
|
89
|
+
*/
|
|
90
|
+
getClient(clientId: string): ConnectedClient | undefined;
|
|
91
|
+
private startHeartbeat;
|
|
92
|
+
private startAuthCheck;
|
|
93
|
+
private log;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Meridian Server — Main Entry Point
|
|
98
|
+
*
|
|
99
|
+
* `createServer()` wires together all server components:
|
|
100
|
+
* - PostgreSQL store (auto-DDL, CRDT merge)
|
|
101
|
+
* - WebSocket hub (auth, connections)
|
|
102
|
+
* - Merge engine (push/pull processing)
|
|
103
|
+
* - Presence manager
|
|
104
|
+
* - Compaction scheduler
|
|
105
|
+
*
|
|
106
|
+
* Usage:
|
|
107
|
+
* ```ts
|
|
108
|
+
* import { createServer } from 'meridian-server';
|
|
109
|
+
* import { defineSchema, z } from 'meridian-shared';
|
|
110
|
+
*
|
|
111
|
+
* const schema = defineSchema({
|
|
112
|
+
* version: 1,
|
|
113
|
+
* collections: {
|
|
114
|
+
* todos: { id: z.string(), title: z.string(), done: z.boolean() },
|
|
115
|
+
* },
|
|
116
|
+
* });
|
|
117
|
+
*
|
|
118
|
+
* const server = createServer({
|
|
119
|
+
* port: 3000,
|
|
120
|
+
* database: 'postgresql://user:pass@localhost:5432/mydb',
|
|
121
|
+
* schema,
|
|
122
|
+
* });
|
|
123
|
+
*
|
|
124
|
+
* await server.start();
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
interface MeridianServerConfig {
|
|
129
|
+
/** Port for WebSocket server */
|
|
130
|
+
port: number;
|
|
131
|
+
/** PostgreSQL connection string */
|
|
132
|
+
database: string;
|
|
133
|
+
/** Schema definition (same as client) */
|
|
134
|
+
schema: SchemaDefinition;
|
|
135
|
+
/** WebSocket path (default: '/sync') */
|
|
136
|
+
path?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Authentication handler.
|
|
139
|
+
* Called with the token from client's auth message.
|
|
140
|
+
* Return user info or throw to reject.
|
|
141
|
+
*/
|
|
142
|
+
auth?: (token: string) => Promise<AuthResult>;
|
|
143
|
+
/**
|
|
144
|
+
* Compaction settings for tombstone cleanup.
|
|
145
|
+
*/
|
|
146
|
+
compaction?: {
|
|
147
|
+
/** Max age for tombstones in ms (default: 30 days) */
|
|
148
|
+
tombstoneMaxAge?: number;
|
|
149
|
+
/** Compaction interval in ms (default: 24 hours) */
|
|
150
|
+
interval?: number;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Conflict handler — called when a field-level conflict is resolved.
|
|
154
|
+
* Use this to implement custom merge logic for specific collections or fields.
|
|
155
|
+
*/
|
|
156
|
+
onConflict?: (conflict: ConflictRecord & {
|
|
157
|
+
collection: string;
|
|
158
|
+
docId: string;
|
|
159
|
+
}) => void;
|
|
160
|
+
/**
|
|
161
|
+
* Permission rules for row-level access control.
|
|
162
|
+
* When provided, only rows the user is authorized to read are returned.
|
|
163
|
+
*
|
|
164
|
+
* ```ts
|
|
165
|
+
* permissions: defineRules({
|
|
166
|
+
* todos: {
|
|
167
|
+
* read: (auth, doc) => auth?.userId === doc.existing?.ownerId,
|
|
168
|
+
* write: (auth, doc) => auth != null,
|
|
169
|
+
* }
|
|
170
|
+
* })
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
permissions?: PermissionRules;
|
|
174
|
+
/**
|
|
175
|
+
* Enable debug logging.
|
|
176
|
+
* @default false
|
|
177
|
+
*/
|
|
178
|
+
debug?: boolean;
|
|
179
|
+
}
|
|
180
|
+
interface MeridianServer {
|
|
181
|
+
/** Start the server */
|
|
182
|
+
start(): Promise<void>;
|
|
183
|
+
/** Stop the server gracefully */
|
|
184
|
+
stop(): Promise<void>;
|
|
185
|
+
/** Run compaction manually */
|
|
186
|
+
compact(): Promise<number>;
|
|
187
|
+
/** Get connected client count */
|
|
188
|
+
getClientCount(): number;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Create a Meridian sync server.
|
|
192
|
+
*/
|
|
193
|
+
declare function createServer(config: MeridianServerConfig): MeridianServer;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Meridian Server — PostgreSQL Store
|
|
197
|
+
*
|
|
198
|
+
* Handles:
|
|
199
|
+
* - Auto-creation of tables from client schema
|
|
200
|
+
* - CRDT metadata storage via _meridian_meta JSONB column
|
|
201
|
+
* - Server-assigned monotonic sequence numbers
|
|
202
|
+
* - LISTEN/NOTIFY for change detection
|
|
203
|
+
* - Tombstone compaction
|
|
204
|
+
* - Changes-since queries for pull protocol
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
interface PgStoreConfig {
|
|
208
|
+
/** PostgreSQL connection string */
|
|
209
|
+
connectionString: string;
|
|
210
|
+
/** Schema definition */
|
|
211
|
+
schema: SchemaDefinition;
|
|
212
|
+
/** Optional namespace prefix for multi-tenant isolation */
|
|
213
|
+
namespace?: string;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* PostgreSQL storage adapter for Meridian server.
|
|
217
|
+
*/
|
|
218
|
+
declare class PgStore {
|
|
219
|
+
private pool;
|
|
220
|
+
private readonly config;
|
|
221
|
+
private changeCallbacks;
|
|
222
|
+
private listenClient;
|
|
223
|
+
private minSeq;
|
|
224
|
+
constructor(config: PgStoreConfig);
|
|
225
|
+
/**
|
|
226
|
+
* Initialize the database — create tables, sequences, triggers.
|
|
227
|
+
*/
|
|
228
|
+
init(): Promise<void>;
|
|
229
|
+
/**
|
|
230
|
+
* Get the table name with optional namespace prefix.
|
|
231
|
+
*/
|
|
232
|
+
private tableName;
|
|
233
|
+
/**
|
|
234
|
+
* Create a table for a collection with Meridian system columns.
|
|
235
|
+
*/
|
|
236
|
+
private createTable;
|
|
237
|
+
/**
|
|
238
|
+
* Apply CRDT operations from a client.
|
|
239
|
+
* Performs field-level LWW merge with existing data.
|
|
240
|
+
* @returns Array of server changes with assigned sequence numbers and any conflicts
|
|
241
|
+
*/
|
|
242
|
+
applyOperations(ops: CRDTOperation[]): Promise<{
|
|
243
|
+
changes: ServerChange[];
|
|
244
|
+
conflicts: ConflictRecord[];
|
|
245
|
+
}>;
|
|
246
|
+
/**
|
|
247
|
+
* Get all changes since a given sequence number.
|
|
248
|
+
* Used for pull protocol.
|
|
249
|
+
*
|
|
250
|
+
* @returns null if seqNum is below minSeq (compaction gap), otherwise changes
|
|
251
|
+
*/
|
|
252
|
+
getChangesSince(since: number): Promise<ServerChange[] | null>;
|
|
253
|
+
/**
|
|
254
|
+
* Get the current minimum available sequence number.
|
|
255
|
+
*/
|
|
256
|
+
getMinSeq(): number;
|
|
257
|
+
/**
|
|
258
|
+
* Delete tombstoned rows older than maxAge.
|
|
259
|
+
* @returns Number of rows deleted
|
|
260
|
+
*/
|
|
261
|
+
compact(maxAgeMs: number): Promise<number>;
|
|
262
|
+
private updateMinSeqWithClient;
|
|
263
|
+
private updateMinSeq;
|
|
264
|
+
private startListening;
|
|
265
|
+
/**
|
|
266
|
+
* Register a callback for database changes.
|
|
267
|
+
*/
|
|
268
|
+
onChange(callback: (tableName: string, docId: string) => void): () => void;
|
|
269
|
+
/**
|
|
270
|
+
* Close the database connection pool.
|
|
271
|
+
*/
|
|
272
|
+
close(): Promise<void>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Meridian Server — CRDT Merge Engine
|
|
277
|
+
*
|
|
278
|
+
* Handles server-side merge logic:
|
|
279
|
+
* - Receives client operations
|
|
280
|
+
* - Merges with existing PostgreSQL state
|
|
281
|
+
* - Assigns sequence numbers
|
|
282
|
+
* - Broadcasts results to other clients
|
|
283
|
+
* - Logs conflicts for debugging
|
|
284
|
+
*/
|
|
285
|
+
|
|
286
|
+
interface MergeEngineConfig {
|
|
287
|
+
pgStore: PgStore;
|
|
288
|
+
wsHub: WsHub;
|
|
289
|
+
debug?: boolean;
|
|
290
|
+
/** Custom conflict handler — devs define their own merge logic */
|
|
291
|
+
onConflict?: (conflict: ConflictRecord & {
|
|
292
|
+
collection: string;
|
|
293
|
+
docId: string;
|
|
294
|
+
}) => void;
|
|
295
|
+
/** Permission rules for row-level access control */
|
|
296
|
+
permissions?: PermissionRules;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Server-side CRDT merge engine with partial sync and row-level permissions.
|
|
300
|
+
*/
|
|
301
|
+
declare class MergeEngine {
|
|
302
|
+
private readonly config;
|
|
303
|
+
private conflictLog;
|
|
304
|
+
private readonly maxConflictLog;
|
|
305
|
+
/** Per-client subscribe filters: clientId → collection → filter */
|
|
306
|
+
private clientFilters;
|
|
307
|
+
private ruleEvaluator;
|
|
308
|
+
constructor(config: MergeEngineConfig);
|
|
309
|
+
/** Store a client's subscribe filter for partial sync */
|
|
310
|
+
setClientFilter(clientId: string, collections: string[], filter?: Record<string, Record<string, unknown>>): void;
|
|
311
|
+
/** Remove client filters on disconnect */
|
|
312
|
+
removeClientFilter(clientId: string): void;
|
|
313
|
+
/**
|
|
314
|
+
* Process a push from a client.
|
|
315
|
+
* Merges operations with existing state, assigns seqNums, and broadcasts.
|
|
316
|
+
*
|
|
317
|
+
* @param clientId - The sending client's ID
|
|
318
|
+
* @param ops - CRDT operations from the client
|
|
319
|
+
* @param client - The connected client object
|
|
320
|
+
*/
|
|
321
|
+
processPush(clientId: string, ops: CRDTOperation[], client: ConnectedClient): Promise<void>;
|
|
322
|
+
/**
|
|
323
|
+
* Process a pull request from a client.
|
|
324
|
+
* Returns changes since the given sequence number.
|
|
325
|
+
*/
|
|
326
|
+
processPull(clientId: string, since: number, client: ConnectedClient): Promise<void>;
|
|
327
|
+
/**
|
|
328
|
+
* Get the conflict log.
|
|
329
|
+
*/
|
|
330
|
+
getConflictLog(): (ConflictRecord & {
|
|
331
|
+
collection: string;
|
|
332
|
+
docId: string;
|
|
333
|
+
timestamp: number;
|
|
334
|
+
})[];
|
|
335
|
+
private log;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Meridian Server — Presence Manager (Server-side)
|
|
340
|
+
*
|
|
341
|
+
* In-memory presence state for connected clients.
|
|
342
|
+
* - Stores presence data per client
|
|
343
|
+
* - Broadcasts updates to all peers
|
|
344
|
+
* - Auto-cleanup on disconnect (no TTL needed — instant)
|
|
345
|
+
*/
|
|
346
|
+
|
|
347
|
+
type PresenceData = Record<string, unknown>;
|
|
348
|
+
declare class ServerPresenceManager {
|
|
349
|
+
private presence;
|
|
350
|
+
private wsHub;
|
|
351
|
+
private debug;
|
|
352
|
+
constructor(wsHub: WsHub, debug?: boolean);
|
|
353
|
+
/**
|
|
354
|
+
* Update presence for a client and broadcast to peers.
|
|
355
|
+
*/
|
|
356
|
+
update(clientId: string, data: PresenceData, client: ConnectedClient): void;
|
|
357
|
+
/**
|
|
358
|
+
* Remove presence for a disconnected client.
|
|
359
|
+
*/
|
|
360
|
+
remove(clientId: string): void;
|
|
361
|
+
/**
|
|
362
|
+
* Get all current presence data.
|
|
363
|
+
*/
|
|
364
|
+
getAll(): Record<string, PresenceData>;
|
|
365
|
+
/**
|
|
366
|
+
* Send current presence state to a specific client (e.g., on reconnect).
|
|
367
|
+
*/
|
|
368
|
+
sendCurrentState(client: ConnectedClient): void;
|
|
369
|
+
private broadcastAll;
|
|
370
|
+
/**
|
|
371
|
+
* Clear all presence data.
|
|
372
|
+
*/
|
|
373
|
+
clear(): void;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Meridian Server — Tombstone Compaction
|
|
378
|
+
*
|
|
379
|
+
* Periodically removes soft-deleted rows older than a configurable max age.
|
|
380
|
+
* After compaction, notifies connected clients so they can clean up local data.
|
|
381
|
+
*/
|
|
382
|
+
|
|
383
|
+
interface CompactionConfig {
|
|
384
|
+
/** Maximum age for tombstones in ms (default: 30 days) */
|
|
385
|
+
tombstoneMaxAge: number;
|
|
386
|
+
/** Compaction check interval in ms (default: 24 hours) */
|
|
387
|
+
interval: number;
|
|
388
|
+
/** Debug mode */
|
|
389
|
+
debug?: boolean;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Tombstone compaction scheduler.
|
|
393
|
+
*/
|
|
394
|
+
declare class CompactionManager {
|
|
395
|
+
private readonly pgStore;
|
|
396
|
+
private readonly wsHub;
|
|
397
|
+
private readonly config;
|
|
398
|
+
private timer;
|
|
399
|
+
constructor(pgStore: PgStore, wsHub: WsHub, config?: Partial<CompactionConfig>);
|
|
400
|
+
/**
|
|
401
|
+
* Start the compaction scheduler.
|
|
402
|
+
*/
|
|
403
|
+
start(): void;
|
|
404
|
+
/**
|
|
405
|
+
* Stop the compaction scheduler.
|
|
406
|
+
*/
|
|
407
|
+
stop(): void;
|
|
408
|
+
/**
|
|
409
|
+
* Run compaction now.
|
|
410
|
+
*/
|
|
411
|
+
runCompaction(): Promise<number>;
|
|
412
|
+
private log;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Meridian — WAL Streaming (PostgreSQL Logical Replication)
|
|
417
|
+
*
|
|
418
|
+
* Production-grade change streaming using PostgreSQL's
|
|
419
|
+
* LISTEN/NOTIFY + logical replication protocol.
|
|
420
|
+
*
|
|
421
|
+
* Two modes:
|
|
422
|
+
* 1. NOTIFY mode (default) — Fast, simple, uses pg_notify()
|
|
423
|
+
* triggers. Good for up to ~10K concurrent clients.
|
|
424
|
+
* 2. WAL mode — Uses wal2json logical decoding plugin for
|
|
425
|
+
* massive scale (100K+ clients). Requires:
|
|
426
|
+
* - `wal_level = logical` in postgresql.conf
|
|
427
|
+
* - `CREATE EXTENSION wal2json;` (if not already installed)
|
|
428
|
+
*/
|
|
429
|
+
interface WALStreamConfig {
|
|
430
|
+
/** PostgreSQL connection string */
|
|
431
|
+
connectionString: string;
|
|
432
|
+
/** Mode: 'notify' (default) or 'wal' (logical replication) */
|
|
433
|
+
mode?: 'notify' | 'wal';
|
|
434
|
+
/** Channel name for NOTIFY mode */
|
|
435
|
+
channel?: string;
|
|
436
|
+
/** Publication name for WAL mode */
|
|
437
|
+
publication?: string;
|
|
438
|
+
/** Slot name for WAL mode */
|
|
439
|
+
slot?: string;
|
|
440
|
+
/** Called for each change received */
|
|
441
|
+
onChange: (change: WALChange) => void;
|
|
442
|
+
/** Debug logging */
|
|
443
|
+
debug?: boolean;
|
|
444
|
+
}
|
|
445
|
+
interface WALChange {
|
|
446
|
+
collection: string;
|
|
447
|
+
docId: string;
|
|
448
|
+
operation: 'INSERT' | 'UPDATE' | 'DELETE';
|
|
449
|
+
fields?: Record<string, unknown>;
|
|
450
|
+
seq?: number;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Create a WAL stream based on the configured mode.
|
|
454
|
+
*
|
|
455
|
+
* ```ts
|
|
456
|
+
* const stream = createWALStream({
|
|
457
|
+
* connectionString: process.env.DATABASE_URL!,
|
|
458
|
+
* mode: 'notify', // or 'wal'
|
|
459
|
+
* onChange: (change) => {
|
|
460
|
+
* // Broadcast to WebSocket clients
|
|
461
|
+
* wsHub.broadcastToCollection(change.collection, change);
|
|
462
|
+
* },
|
|
463
|
+
* debug: true,
|
|
464
|
+
* });
|
|
465
|
+
* await stream.start();
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
declare function createWALStream(config: WALStreamConfig): {
|
|
469
|
+
start(): Promise<void>;
|
|
470
|
+
stop(): Promise<void>;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Meridian — SQLite Storage Adapter
|
|
475
|
+
*
|
|
476
|
+
* Implements StorageAdapter for SQLite databases.
|
|
477
|
+
* Supports:
|
|
478
|
+
* - better-sqlite3 (Node.js server)
|
|
479
|
+
* - sql.js (WASM — browser/React Native)
|
|
480
|
+
* - Turso/libsql (edge/distributed SQLite)
|
|
481
|
+
*
|
|
482
|
+
* Usage:
|
|
483
|
+
* ```ts
|
|
484
|
+
* const store = new SQLiteStore({
|
|
485
|
+
* databasePath: './meridian.db',
|
|
486
|
+
* schema,
|
|
487
|
+
* });
|
|
488
|
+
* await store.init();
|
|
489
|
+
* ```
|
|
490
|
+
*/
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Minimal SQL driver interface — compatible with better-sqlite3, sql.js, and libsql.
|
|
494
|
+
*/
|
|
495
|
+
interface SQLDriver {
|
|
496
|
+
exec(sql: string): void;
|
|
497
|
+
prepare(sql: string): SQLStatement;
|
|
498
|
+
close(): void;
|
|
499
|
+
}
|
|
500
|
+
interface SQLStatement {
|
|
501
|
+
run(...params: unknown[]): {
|
|
502
|
+
changes: number;
|
|
503
|
+
lastInsertRowid: number | bigint;
|
|
504
|
+
};
|
|
505
|
+
get(...params: unknown[]): Record<string, unknown> | undefined;
|
|
506
|
+
all(...params: unknown[]): Record<string, unknown>[];
|
|
507
|
+
}
|
|
508
|
+
interface SQLiteStoreConfig extends StorageAdapterConfig {
|
|
509
|
+
/** SQL driver instance (better-sqlite3 Database, sql.js Database, etc.) */
|
|
510
|
+
driver: SQLDriver;
|
|
511
|
+
}
|
|
512
|
+
declare class SQLiteStore {
|
|
513
|
+
private readonly driver;
|
|
514
|
+
private readonly config;
|
|
515
|
+
private lastSeq;
|
|
516
|
+
private minSeq;
|
|
517
|
+
constructor(config: SQLiteStoreConfig);
|
|
518
|
+
init(): Promise<void>;
|
|
519
|
+
private createTable;
|
|
520
|
+
applyOperations(ops: CRDTOperation[]): Promise<{
|
|
521
|
+
changes: ServerChange[];
|
|
522
|
+
conflicts: ConflictInfo[];
|
|
523
|
+
}>;
|
|
524
|
+
getChangesSince(since: number): Promise<ServerChange[] | null>;
|
|
525
|
+
getMinSeq(): number;
|
|
526
|
+
compact(maxAgeMs: number): Promise<number>;
|
|
527
|
+
close(): Promise<void>;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Meridian — MySQL Storage Adapter
|
|
532
|
+
*
|
|
533
|
+
* Implements StorageAdapter for MySQL databases.
|
|
534
|
+
* Uses mysql2 driver for Node.js.
|
|
535
|
+
*
|
|
536
|
+
* Usage:
|
|
537
|
+
* ```ts
|
|
538
|
+
* import mysql from 'mysql2/promise';
|
|
539
|
+
* const pool = mysql.createPool('mysql://localhost/meridian');
|
|
540
|
+
* const store = new MySQLStore({ pool, schema });
|
|
541
|
+
* await store.init();
|
|
542
|
+
* ```
|
|
543
|
+
*/
|
|
544
|
+
|
|
545
|
+
interface MySQLPool {
|
|
546
|
+
execute(sql: string, params?: unknown[]): Promise<[ResultSetHeader, any]>;
|
|
547
|
+
query(sql: string, params?: unknown[]): Promise<[RowDataPacket[], any]>;
|
|
548
|
+
end(): Promise<void>;
|
|
549
|
+
}
|
|
550
|
+
interface ResultSetHeader {
|
|
551
|
+
insertId: number;
|
|
552
|
+
affectedRows: number;
|
|
553
|
+
}
|
|
554
|
+
interface RowDataPacket {
|
|
555
|
+
[key: string]: unknown;
|
|
556
|
+
}
|
|
557
|
+
interface MySQLStoreConfig {
|
|
558
|
+
pool: MySQLPool;
|
|
559
|
+
schema: SchemaDefinition;
|
|
560
|
+
debug?: boolean;
|
|
561
|
+
}
|
|
562
|
+
declare class MySQLStore {
|
|
563
|
+
private pool;
|
|
564
|
+
private config;
|
|
565
|
+
private lastSeq;
|
|
566
|
+
private minSeq;
|
|
567
|
+
constructor(config: MySQLStoreConfig);
|
|
568
|
+
init(): Promise<void>;
|
|
569
|
+
applyOperations(ops: CRDTOperation[]): Promise<{
|
|
570
|
+
changes: ServerChange[];
|
|
571
|
+
conflicts: ConflictInfo[];
|
|
572
|
+
}>;
|
|
573
|
+
getChangesSince(since: number): Promise<ServerChange[] | null>;
|
|
574
|
+
getMinSeq(): number;
|
|
575
|
+
compact(maxAgeMs: number): Promise<number>;
|
|
576
|
+
close(): Promise<void>;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Meridian — Snapshot Recovery
|
|
581
|
+
*
|
|
582
|
+
* Optimizes full re-sync by creating periodic snapshots of collection state.
|
|
583
|
+
* Instead of replaying all operations from seq=0, clients can load a snapshot
|
|
584
|
+
* and only replay operations since the snapshot's sequence number.
|
|
585
|
+
*
|
|
586
|
+
* Benefits:
|
|
587
|
+
* - New clients sync in O(snapshot + delta) instead of O(all_ops)
|
|
588
|
+
* - Bandwidth reduction for clients that have been offline for days
|
|
589
|
+
* - Faster recovery after compaction gaps
|
|
590
|
+
*/
|
|
591
|
+
|
|
592
|
+
interface Snapshot {
|
|
593
|
+
/** Sequence number this snapshot was taken at */
|
|
594
|
+
seq: number;
|
|
595
|
+
/** ISO timestamp of snapshot creation */
|
|
596
|
+
createdAt: string;
|
|
597
|
+
/** Collection snapshots */
|
|
598
|
+
collections: Record<string, CollectionSnapshot>;
|
|
599
|
+
}
|
|
600
|
+
interface CollectionSnapshot {
|
|
601
|
+
/** Collection name */
|
|
602
|
+
name: string;
|
|
603
|
+
/** Number of documents */
|
|
604
|
+
count: number;
|
|
605
|
+
/** All non-deleted documents */
|
|
606
|
+
documents: Record<string, unknown>[];
|
|
607
|
+
}
|
|
608
|
+
interface SnapshotConfig {
|
|
609
|
+
/** Create a snapshot every N operations */
|
|
610
|
+
interval: number;
|
|
611
|
+
/** Maximum number of snapshots to keep */
|
|
612
|
+
maxSnapshots: number;
|
|
613
|
+
/** Database (pgStore or mysqlStore) */
|
|
614
|
+
store: PgStore | {
|
|
615
|
+
getChangesSince(since: number): Promise<ServerChange[] | null>;
|
|
616
|
+
getMinSeq(): number;
|
|
617
|
+
};
|
|
618
|
+
/** Schema definition */
|
|
619
|
+
schema: SchemaDefinition;
|
|
620
|
+
/** Debug logging */
|
|
621
|
+
debug?: boolean;
|
|
622
|
+
}
|
|
623
|
+
declare class SnapshotManager {
|
|
624
|
+
private config;
|
|
625
|
+
private snapshots;
|
|
626
|
+
private opCounter;
|
|
627
|
+
constructor(config: SnapshotConfig);
|
|
628
|
+
/**
|
|
629
|
+
* Track an operation. Creates a snapshot when the interval is reached.
|
|
630
|
+
*/
|
|
631
|
+
trackOp(): Promise<void>;
|
|
632
|
+
/**
|
|
633
|
+
* Create a snapshot of all collections at the current sequence number.
|
|
634
|
+
*/
|
|
635
|
+
createSnapshot(): Promise<Snapshot>;
|
|
636
|
+
/**
|
|
637
|
+
* Get the most recent snapshot at or before the given sequence number.
|
|
638
|
+
*/
|
|
639
|
+
getSnapshotForSeq(seq: number): Snapshot | null;
|
|
640
|
+
/**
|
|
641
|
+
* Estimate bandwidth savings from using a snapshot vs full replay.
|
|
642
|
+
*
|
|
643
|
+
* @param totalOps - Total operations since seq 0
|
|
644
|
+
* @param snapshotSeq - Sequence number of the snapshot
|
|
645
|
+
* @returns Percentage of operations saved
|
|
646
|
+
*/
|
|
647
|
+
estimateSavings(totalOps: number, snapshotSeq: number): number;
|
|
648
|
+
/**
|
|
649
|
+
* Get all stored snapshots (for debugging/management).
|
|
650
|
+
*/
|
|
651
|
+
getSnapshots(): Snapshot[];
|
|
652
|
+
/**
|
|
653
|
+
* Clear all snapshots (e.g., after schema change).
|
|
654
|
+
*/
|
|
655
|
+
clear(): void;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export { type AuthResult, type CollectionSnapshot, type CompactionConfig, CompactionManager, type ConnectedClient, MergeEngine, type MergeEngineConfig, type MeridianServer, type MeridianServerConfig, type MySQLPool, MySQLStore, type MySQLStoreConfig, PgStore, type PgStoreConfig, type SQLDriver, type SQLStatement, SQLiteStore, type SQLiteStoreConfig, ServerPresenceManager, type Snapshot, type SnapshotConfig, SnapshotManager, type WALChange, type WALStreamConfig, WsHub, type WsHubConfig, createServer, createWALStream };
|