@korajs/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 ADDED
@@ -0,0 +1,1201 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ClientSession: () => ClientSession,
34
+ HttpServerTransport: () => HttpServerTransport,
35
+ KoraSyncServer: () => KoraSyncServer,
36
+ MemoryServerStore: () => MemoryServerStore,
37
+ NoAuthProvider: () => NoAuthProvider,
38
+ PostgresServerStore: () => PostgresServerStore,
39
+ SqliteServerStore: () => SqliteServerStore,
40
+ TokenAuthProvider: () => TokenAuthProvider,
41
+ WsServerTransport: () => WsServerTransport,
42
+ createKoraServer: () => createKoraServer,
43
+ createPostgresServerStore: () => createPostgresServerStore,
44
+ createSqliteServerStore: () => createSqliteServerStore
45
+ });
46
+ module.exports = __toCommonJS(index_exports);
47
+
48
+ // src/store/memory-server-store.ts
49
+ var import_core = require("@korajs/core");
50
+ var MemoryServerStore = class {
51
+ nodeId;
52
+ operations = [];
53
+ operationIndex = /* @__PURE__ */ new Map();
54
+ versionVector = /* @__PURE__ */ new Map();
55
+ closed = false;
56
+ constructor(nodeId) {
57
+ this.nodeId = nodeId ?? (0, import_core.generateUUIDv7)();
58
+ }
59
+ getVersionVector() {
60
+ return new Map(this.versionVector);
61
+ }
62
+ getNodeId() {
63
+ return this.nodeId;
64
+ }
65
+ async applyRemoteOperation(op) {
66
+ this.assertOpen();
67
+ if (this.operationIndex.has(op.id)) {
68
+ return "duplicate";
69
+ }
70
+ this.operations.push(op);
71
+ this.operationIndex.set(op.id, op);
72
+ const currentSeq = this.versionVector.get(op.nodeId) ?? 0;
73
+ if (op.sequenceNumber > currentSeq) {
74
+ this.versionVector.set(op.nodeId, op.sequenceNumber);
75
+ }
76
+ return "applied";
77
+ }
78
+ async getOperationRange(nodeId, fromSeq, toSeq) {
79
+ this.assertOpen();
80
+ return this.operations.filter(
81
+ (op) => op.nodeId === nodeId && op.sequenceNumber >= fromSeq && op.sequenceNumber <= toSeq
82
+ ).sort((a, b) => a.sequenceNumber - b.sequenceNumber);
83
+ }
84
+ async getOperationCount() {
85
+ this.assertOpen();
86
+ return this.operations.length;
87
+ }
88
+ async close() {
89
+ this.closed = true;
90
+ }
91
+ // --- Testing helpers (not on interface) ---
92
+ /**
93
+ * Get all stored operations (for test assertions).
94
+ */
95
+ getAllOperations() {
96
+ return [...this.operations];
97
+ }
98
+ assertOpen() {
99
+ if (this.closed) {
100
+ throw new Error("MemoryServerStore is closed");
101
+ }
102
+ }
103
+ };
104
+
105
+ // src/store/postgres-server-store.ts
106
+ var import_core2 = require("@korajs/core");
107
+ var import_drizzle_orm = require("drizzle-orm");
108
+
109
+ // src/store/drizzle-pg-schema.ts
110
+ var import_pg_core = require("drizzle-orm/pg-core");
111
+ var pgOperations = (0, import_pg_core.pgTable)(
112
+ "operations",
113
+ {
114
+ id: (0, import_pg_core.text)("id").primaryKey(),
115
+ nodeId: (0, import_pg_core.text)("node_id").notNull(),
116
+ type: (0, import_pg_core.text)("type").notNull(),
117
+ collection: (0, import_pg_core.text)("collection").notNull(),
118
+ recordId: (0, import_pg_core.text)("record_id").notNull(),
119
+ data: (0, import_pg_core.text)("data"),
120
+ // JSON-serialized, null for deletes
121
+ previousData: (0, import_pg_core.text)("previous_data"),
122
+ // JSON-serialized, null for insert/delete
123
+ wallTime: (0, import_pg_core.integer)("wall_time").notNull(),
124
+ logical: (0, import_pg_core.integer)("logical").notNull(),
125
+ timestampNodeId: (0, import_pg_core.text)("timestamp_node_id").notNull(),
126
+ sequenceNumber: (0, import_pg_core.integer)("sequence_number").notNull(),
127
+ causalDeps: (0, import_pg_core.text)("causal_deps").notNull().default("[]"),
128
+ // JSON array of op IDs
129
+ schemaVersion: (0, import_pg_core.integer)("schema_version").notNull(),
130
+ receivedAt: (0, import_pg_core.integer)("received_at").notNull()
131
+ },
132
+ (table) => ({
133
+ nodeSeqIdx: (0, import_pg_core.index)("idx_pg_node_seq").on(table.nodeId, table.sequenceNumber),
134
+ collectionIdx: (0, import_pg_core.index)("idx_pg_collection").on(table.collection),
135
+ receivedIdx: (0, import_pg_core.index)("idx_pg_received").on(table.receivedAt)
136
+ })
137
+ );
138
+ var pgSyncState = (0, import_pg_core.pgTable)("sync_state", {
139
+ nodeId: (0, import_pg_core.text)("node_id").primaryKey(),
140
+ maxSequenceNumber: (0, import_pg_core.integer)("max_sequence_number").notNull(),
141
+ lastSeenAt: (0, import_pg_core.integer)("last_seen_at").notNull()
142
+ });
143
+
144
+ // src/store/postgres-server-store.ts
145
+ var PostgresServerStore = class {
146
+ nodeId;
147
+ db;
148
+ versionVector = /* @__PURE__ */ new Map();
149
+ ready;
150
+ closed = false;
151
+ constructor(db, nodeId) {
152
+ this.db = db;
153
+ this.nodeId = nodeId ?? (0, import_core2.generateUUIDv7)();
154
+ this.ready = this.initialize();
155
+ }
156
+ getVersionVector() {
157
+ this.assertOpen();
158
+ return new Map(this.versionVector);
159
+ }
160
+ getNodeId() {
161
+ return this.nodeId;
162
+ }
163
+ async applyRemoteOperation(op) {
164
+ this.assertOpen();
165
+ await this.ready;
166
+ const existing = await this.db.select({ id: pgOperations.id }).from(pgOperations).where((0, import_drizzle_orm.eq)(pgOperations.id, op.id)).limit(1);
167
+ if (existing.length > 0) {
168
+ return "duplicate";
169
+ }
170
+ const now = Date.now();
171
+ const row = this.serializeOperation(op, now);
172
+ await this.db.transaction(async (tx) => {
173
+ await tx.insert(pgOperations).values(row).onConflictDoNothing({ target: pgOperations.id });
174
+ await tx.insert(pgSyncState).values({
175
+ nodeId: op.nodeId,
176
+ maxSequenceNumber: op.sequenceNumber,
177
+ lastSeenAt: now
178
+ }).onConflictDoUpdate({
179
+ target: pgSyncState.nodeId,
180
+ set: {
181
+ maxSequenceNumber: import_drizzle_orm.sql`GREATEST(${pgSyncState.maxSequenceNumber}, ${op.sequenceNumber})`,
182
+ lastSeenAt: import_drizzle_orm.sql`${now}`
183
+ }
184
+ });
185
+ });
186
+ const currentMax = this.versionVector.get(op.nodeId) ?? 0;
187
+ if (op.sequenceNumber > currentMax) {
188
+ this.versionVector.set(op.nodeId, op.sequenceNumber);
189
+ }
190
+ return "applied";
191
+ }
192
+ async getOperationRange(nodeId, fromSeq, toSeq) {
193
+ this.assertOpen();
194
+ await this.ready;
195
+ const rows = await this.db.select().from(pgOperations).where(
196
+ (0, import_drizzle_orm.and)(
197
+ (0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId),
198
+ (0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq)
199
+ )
200
+ ).orderBy((0, import_drizzle_orm.asc)(pgOperations.sequenceNumber));
201
+ return rows.map((row) => this.deserializeOperation(row));
202
+ }
203
+ async getOperationCount() {
204
+ this.assertOpen();
205
+ await this.ready;
206
+ const result = await this.db.select({ value: (0, import_drizzle_orm.count)() }).from(pgOperations);
207
+ return result[0]?.value ?? 0;
208
+ }
209
+ async close() {
210
+ this.closed = true;
211
+ }
212
+ async initialize() {
213
+ await this.ensureTables();
214
+ const rows = await this.db.select({
215
+ nodeId: pgSyncState.nodeId,
216
+ maxSequenceNumber: pgSyncState.maxSequenceNumber
217
+ }).from(pgSyncState);
218
+ for (const row of rows) {
219
+ this.versionVector.set(row.nodeId, row.maxSequenceNumber);
220
+ }
221
+ }
222
+ /**
223
+ * Create tables if they don't exist.
224
+ * Uses raw SQL via Drizzle's sql template — standard DDL practice without drizzle-kit.
225
+ */
226
+ async ensureTables() {
227
+ await this.db.execute(import_drizzle_orm.sql`
228
+ CREATE TABLE IF NOT EXISTS operations (
229
+ id TEXT PRIMARY KEY,
230
+ node_id TEXT NOT NULL,
231
+ type TEXT NOT NULL,
232
+ collection TEXT NOT NULL,
233
+ record_id TEXT NOT NULL,
234
+ data TEXT,
235
+ previous_data TEXT,
236
+ wall_time INTEGER NOT NULL,
237
+ logical INTEGER NOT NULL,
238
+ timestamp_node_id TEXT NOT NULL,
239
+ sequence_number INTEGER NOT NULL,
240
+ causal_deps TEXT NOT NULL DEFAULT '[]',
241
+ schema_version INTEGER NOT NULL,
242
+ received_at INTEGER NOT NULL
243
+ )
244
+ `);
245
+ await this.db.execute(
246
+ import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
247
+ );
248
+ await this.db.execute(
249
+ import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`
250
+ );
251
+ await this.db.execute(
252
+ import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`
253
+ );
254
+ await this.db.execute(import_drizzle_orm.sql`
255
+ CREATE TABLE IF NOT EXISTS sync_state (
256
+ node_id TEXT PRIMARY KEY,
257
+ max_sequence_number INTEGER NOT NULL,
258
+ last_seen_at INTEGER NOT NULL
259
+ )
260
+ `);
261
+ }
262
+ serializeOperation(op, receivedAt) {
263
+ return {
264
+ id: op.id,
265
+ nodeId: op.nodeId,
266
+ type: op.type,
267
+ collection: op.collection,
268
+ recordId: op.recordId,
269
+ data: op.data !== null ? JSON.stringify(op.data) : null,
270
+ previousData: op.previousData !== null ? JSON.stringify(op.previousData) : null,
271
+ wallTime: op.timestamp.wallTime,
272
+ logical: op.timestamp.logical,
273
+ timestampNodeId: op.timestamp.nodeId,
274
+ sequenceNumber: op.sequenceNumber,
275
+ causalDeps: JSON.stringify(op.causalDeps),
276
+ schemaVersion: op.schemaVersion,
277
+ receivedAt
278
+ };
279
+ }
280
+ deserializeOperation(row) {
281
+ return {
282
+ id: row.id,
283
+ nodeId: row.nodeId,
284
+ type: row.type,
285
+ collection: row.collection,
286
+ recordId: row.recordId,
287
+ data: row.data !== null ? JSON.parse(row.data) : null,
288
+ previousData: row.previousData !== null ? JSON.parse(row.previousData) : null,
289
+ timestamp: {
290
+ wallTime: row.wallTime,
291
+ logical: row.logical,
292
+ nodeId: row.timestampNodeId
293
+ },
294
+ sequenceNumber: row.sequenceNumber,
295
+ causalDeps: JSON.parse(row.causalDeps),
296
+ schemaVersion: row.schemaVersion
297
+ };
298
+ }
299
+ assertOpen() {
300
+ if (this.closed) {
301
+ throw new Error("PostgresServerStore is closed");
302
+ }
303
+ }
304
+ };
305
+ async function createPostgresServerStore(options) {
306
+ const { postgresClient, drizzleFn } = await loadPostgresDeps();
307
+ const client = postgresClient(options.connectionString);
308
+ const db = drizzleFn(client);
309
+ return new PostgresServerStore(db, options.nodeId);
310
+ }
311
+ async function loadPostgresDeps() {
312
+ try {
313
+ const dynamicImport = new Function("specifier", "return import(specifier)");
314
+ const postgresMod = await dynamicImport("postgres");
315
+ const drizzleMod = await dynamicImport("drizzle-orm/postgres-js");
316
+ return {
317
+ postgresClient: postgresMod.default,
318
+ drizzleFn: drizzleMod.drizzle
319
+ };
320
+ } catch {
321
+ throw new Error(
322
+ 'PostgreSQL backend requires the "postgres" package. Install it in your project dependencies.'
323
+ );
324
+ }
325
+ }
326
+
327
+ // src/store/sqlite-server-store.ts
328
+ var import_core3 = require("@korajs/core");
329
+ var import_drizzle_orm2 = require("drizzle-orm");
330
+
331
+ // src/store/drizzle-schema.ts
332
+ var import_sqlite_core = require("drizzle-orm/sqlite-core");
333
+ var operations = (0, import_sqlite_core.sqliteTable)(
334
+ "operations",
335
+ {
336
+ id: (0, import_sqlite_core.text)("id").primaryKey(),
337
+ nodeId: (0, import_sqlite_core.text)("node_id").notNull(),
338
+ type: (0, import_sqlite_core.text)("type").notNull(),
339
+ collection: (0, import_sqlite_core.text)("collection").notNull(),
340
+ recordId: (0, import_sqlite_core.text)("record_id").notNull(),
341
+ data: (0, import_sqlite_core.text)("data"),
342
+ // JSON-serialized, null for deletes
343
+ previousData: (0, import_sqlite_core.text)("previous_data"),
344
+ // JSON-serialized, null for insert/delete
345
+ wallTime: (0, import_sqlite_core.integer)("wall_time").notNull(),
346
+ logical: (0, import_sqlite_core.integer)("logical").notNull(),
347
+ timestampNodeId: (0, import_sqlite_core.text)("timestamp_node_id").notNull(),
348
+ sequenceNumber: (0, import_sqlite_core.integer)("sequence_number").notNull(),
349
+ causalDeps: (0, import_sqlite_core.text)("causal_deps").notNull().default("[]"),
350
+ // JSON array of op IDs
351
+ schemaVersion: (0, import_sqlite_core.integer)("schema_version").notNull(),
352
+ receivedAt: (0, import_sqlite_core.integer)("received_at").notNull()
353
+ },
354
+ (table) => ({
355
+ nodeSeqIdx: (0, import_sqlite_core.index)("idx_node_seq").on(table.nodeId, table.sequenceNumber),
356
+ collectionIdx: (0, import_sqlite_core.index)("idx_collection").on(table.collection),
357
+ receivedIdx: (0, import_sqlite_core.index)("idx_received").on(table.receivedAt)
358
+ })
359
+ );
360
+ var syncState = (0, import_sqlite_core.sqliteTable)("sync_state", {
361
+ nodeId: (0, import_sqlite_core.text)("node_id").primaryKey(),
362
+ maxSequenceNumber: (0, import_sqlite_core.integer)("max_sequence_number").notNull(),
363
+ lastSeenAt: (0, import_sqlite_core.integer)("last_seen_at").notNull()
364
+ });
365
+
366
+ // src/store/sqlite-server-store.ts
367
+ var SqliteServerStore = class {
368
+ nodeId;
369
+ db;
370
+ closed = false;
371
+ constructor(db, nodeId) {
372
+ this.db = db;
373
+ this.nodeId = nodeId ?? (0, import_core3.generateUUIDv7)();
374
+ this.ensureTables();
375
+ }
376
+ getVersionVector() {
377
+ this.assertOpen();
378
+ const rows = this.db.select().from(syncState).all();
379
+ const vv = /* @__PURE__ */ new Map();
380
+ for (const row of rows) {
381
+ vv.set(row.nodeId, row.maxSequenceNumber);
382
+ }
383
+ return vv;
384
+ }
385
+ getNodeId() {
386
+ return this.nodeId;
387
+ }
388
+ async applyRemoteOperation(op) {
389
+ this.assertOpen();
390
+ const now = Date.now();
391
+ const row = this.serializeOperation(op, now);
392
+ const result = this.db.transaction((tx) => {
393
+ const insertResult = tx.insert(operations).values(row).onConflictDoNothing({ target: operations.id }).run();
394
+ if (insertResult.changes === 0) {
395
+ return "duplicate";
396
+ }
397
+ tx.insert(syncState).values({
398
+ nodeId: op.nodeId,
399
+ maxSequenceNumber: op.sequenceNumber,
400
+ lastSeenAt: now
401
+ }).onConflictDoUpdate({
402
+ target: syncState.nodeId,
403
+ set: {
404
+ maxSequenceNumber: import_drizzle_orm2.sql`MAX(${syncState.maxSequenceNumber}, ${op.sequenceNumber})`,
405
+ lastSeenAt: import_drizzle_orm2.sql`${now}`
406
+ }
407
+ }).run();
408
+ return "applied";
409
+ });
410
+ return result;
411
+ }
412
+ async getOperationRange(nodeId, fromSeq, toSeq) {
413
+ this.assertOpen();
414
+ const rows = this.db.select().from(operations).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(operations.nodeId, nodeId), (0, import_drizzle_orm2.between)(operations.sequenceNumber, fromSeq, toSeq))).orderBy((0, import_drizzle_orm2.asc)(operations.sequenceNumber)).all();
415
+ return rows.map((row) => this.deserializeOperation(row));
416
+ }
417
+ async getOperationCount() {
418
+ this.assertOpen();
419
+ const result = this.db.select({ value: (0, import_drizzle_orm2.count)() }).from(operations).all();
420
+ return result[0]?.value ?? 0;
421
+ }
422
+ async close() {
423
+ this.closed = true;
424
+ }
425
+ /**
426
+ * Create the operations and sync_state tables if they don't exist.
427
+ * Uses raw SQL via Drizzle's sql template — standard practice for DDL without drizzle-kit.
428
+ */
429
+ ensureTables() {
430
+ this.db.run(import_drizzle_orm2.sql`
431
+ CREATE TABLE IF NOT EXISTS operations (
432
+ id TEXT PRIMARY KEY,
433
+ node_id TEXT NOT NULL,
434
+ type TEXT NOT NULL,
435
+ collection TEXT NOT NULL,
436
+ record_id TEXT NOT NULL,
437
+ data TEXT,
438
+ previous_data TEXT,
439
+ wall_time INTEGER NOT NULL,
440
+ logical INTEGER NOT NULL,
441
+ timestamp_node_id TEXT NOT NULL,
442
+ sequence_number INTEGER NOT NULL,
443
+ causal_deps TEXT NOT NULL DEFAULT '[]',
444
+ schema_version INTEGER NOT NULL,
445
+ received_at INTEGER NOT NULL
446
+ )
447
+ `);
448
+ this.db.run(import_drizzle_orm2.sql`
449
+ CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)
450
+ `);
451
+ this.db.run(import_drizzle_orm2.sql`
452
+ CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)
453
+ `);
454
+ this.db.run(import_drizzle_orm2.sql`
455
+ CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)
456
+ `);
457
+ this.db.run(import_drizzle_orm2.sql`
458
+ CREATE TABLE IF NOT EXISTS sync_state (
459
+ node_id TEXT PRIMARY KEY,
460
+ max_sequence_number INTEGER NOT NULL,
461
+ last_seen_at INTEGER NOT NULL
462
+ )
463
+ `);
464
+ }
465
+ serializeOperation(op, receivedAt) {
466
+ return {
467
+ id: op.id,
468
+ nodeId: op.nodeId,
469
+ type: op.type,
470
+ collection: op.collection,
471
+ recordId: op.recordId,
472
+ data: op.data !== null ? JSON.stringify(op.data) : null,
473
+ previousData: op.previousData !== null ? JSON.stringify(op.previousData) : null,
474
+ wallTime: op.timestamp.wallTime,
475
+ logical: op.timestamp.logical,
476
+ timestampNodeId: op.timestamp.nodeId,
477
+ sequenceNumber: op.sequenceNumber,
478
+ causalDeps: JSON.stringify(op.causalDeps),
479
+ schemaVersion: op.schemaVersion,
480
+ receivedAt
481
+ };
482
+ }
483
+ deserializeOperation(row) {
484
+ return {
485
+ id: row.id,
486
+ nodeId: row.nodeId,
487
+ type: row.type,
488
+ collection: row.collection,
489
+ recordId: row.recordId,
490
+ data: row.data !== null ? JSON.parse(row.data) : null,
491
+ previousData: row.previousData !== null ? JSON.parse(row.previousData) : null,
492
+ timestamp: {
493
+ wallTime: row.wallTime,
494
+ logical: row.logical,
495
+ nodeId: row.timestampNodeId
496
+ },
497
+ sequenceNumber: row.sequenceNumber,
498
+ causalDeps: JSON.parse(row.causalDeps),
499
+ schemaVersion: row.schemaVersion
500
+ };
501
+ }
502
+ assertOpen() {
503
+ if (this.closed) {
504
+ throw new Error("SqliteServerStore is closed");
505
+ }
506
+ }
507
+ };
508
+ function createSqliteServerStore(options) {
509
+ const Database = require("better-sqlite3");
510
+ const { drizzle } = require("drizzle-orm/better-sqlite3");
511
+ const filename = options.filename ?? ":memory:";
512
+ const sqlite = new Database(filename);
513
+ sqlite.pragma("journal_mode = WAL");
514
+ const db = drizzle(sqlite);
515
+ return new SqliteServerStore(db, options.nodeId);
516
+ }
517
+
518
+ // src/transport/http-server-transport.ts
519
+ var import_core4 = require("@korajs/core");
520
+ var HttpServerTransport = class {
521
+ serializer;
522
+ messageHandler = null;
523
+ closeHandler = null;
524
+ errorHandler = null;
525
+ connected = true;
526
+ nextSequence = 1;
527
+ queue = [];
528
+ constructor(serializer) {
529
+ this.serializer = serializer;
530
+ }
531
+ send(message) {
532
+ if (!this.connected) return;
533
+ const encoded = this.serializer.encode(message);
534
+ const isBinary = encoded instanceof Uint8Array;
535
+ this.queue.push({
536
+ etag: this.makeEtag(this.nextSequence++),
537
+ contentType: isBinary ? "application/x-protobuf" : "application/json",
538
+ payload: encoded
539
+ });
540
+ }
541
+ onMessage(handler) {
542
+ this.messageHandler = handler;
543
+ }
544
+ onClose(handler) {
545
+ this.closeHandler = handler;
546
+ }
547
+ onError(handler) {
548
+ this.errorHandler = handler;
549
+ }
550
+ isConnected() {
551
+ return this.connected;
552
+ }
553
+ close(code = 1e3, reason = "transport closed") {
554
+ if (!this.connected) return;
555
+ this.connected = false;
556
+ this.queue.length = 0;
557
+ this.closeHandler?.(code, reason);
558
+ }
559
+ receive(payload) {
560
+ if (!this.connected) {
561
+ throw new import_core4.SyncError("HTTP server transport is closed");
562
+ }
563
+ try {
564
+ const message = this.serializer.decode(payload);
565
+ this.messageHandler?.(message);
566
+ } catch (error) {
567
+ this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
568
+ }
569
+ }
570
+ poll(ifNoneMatch) {
571
+ if (!this.connected) {
572
+ return { status: 410 };
573
+ }
574
+ const next = this.queue[0];
575
+ if (!next) {
576
+ return { status: 204 };
577
+ }
578
+ if (ifNoneMatch && ifNoneMatch === next.etag) {
579
+ return {
580
+ status: 304,
581
+ headers: { etag: next.etag }
582
+ };
583
+ }
584
+ this.queue.shift();
585
+ return {
586
+ status: 200,
587
+ body: next.payload,
588
+ headers: {
589
+ "content-type": next.contentType,
590
+ etag: next.etag
591
+ }
592
+ };
593
+ }
594
+ makeEtag(sequence) {
595
+ return `W/"${sequence}"`;
596
+ }
597
+ };
598
+
599
+ // src/transport/ws-server-transport.ts
600
+ var import_core5 = require("@korajs/core");
601
+ var import_sync = require("@korajs/sync");
602
+ var WS_OPEN = 1;
603
+ var WsServerTransport = class {
604
+ ws;
605
+ serializer;
606
+ messageHandler = null;
607
+ closeHandler = null;
608
+ errorHandler = null;
609
+ constructor(ws, options) {
610
+ this.ws = ws;
611
+ this.serializer = options?.serializer ?? new import_sync.JsonMessageSerializer();
612
+ this.setupListeners();
613
+ }
614
+ send(message) {
615
+ if (this.ws.readyState !== WS_OPEN) {
616
+ throw new import_core5.SyncError("Cannot send message: WebSocket is not open", {
617
+ readyState: this.ws.readyState,
618
+ messageType: message.type
619
+ });
620
+ }
621
+ const encoded = this.serializer.encode(message);
622
+ this.ws.send(encoded);
623
+ }
624
+ onMessage(handler) {
625
+ this.messageHandler = handler;
626
+ }
627
+ onClose(handler) {
628
+ this.closeHandler = handler;
629
+ }
630
+ onError(handler) {
631
+ this.errorHandler = handler;
632
+ }
633
+ isConnected() {
634
+ return this.ws.readyState === WS_OPEN;
635
+ }
636
+ close(code, reason) {
637
+ this.ws.close(code ?? 1e3, reason ?? "server closing");
638
+ }
639
+ setupListeners() {
640
+ this.ws.on("message", (data) => {
641
+ try {
642
+ if (typeof data !== "string" && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer)) {
643
+ throw new import_core5.SyncError("Unsupported WebSocket payload type", {
644
+ payloadType: typeof data
645
+ });
646
+ }
647
+ const decoded = this.serializer.decode(data);
648
+ this.messageHandler?.(decoded);
649
+ } catch (err) {
650
+ this.errorHandler?.(err instanceof Error ? err : new Error(String(err)));
651
+ }
652
+ });
653
+ this.ws.on("close", (code, reason) => {
654
+ this.closeHandler?.(Number(code) || 1006, String(reason || "connection closed"));
655
+ });
656
+ this.ws.on("error", (err) => {
657
+ this.errorHandler?.(err instanceof Error ? err : new Error(String(err)));
658
+ });
659
+ }
660
+ };
661
+
662
+ // src/session/client-session.ts
663
+ var import_core6 = require("@korajs/core");
664
+ var import_internal = require("@korajs/core/internal");
665
+ var import_sync2 = require("@korajs/sync");
666
+
667
+ // src/scopes/server-scope-filter.ts
668
+ function operationMatchesScopes(op, scopes) {
669
+ if (!scopes) return true;
670
+ const collectionScope = scopes[op.collection];
671
+ if (!collectionScope) return false;
672
+ const snapshot = buildSnapshot(op);
673
+ if (!snapshot) return false;
674
+ for (const [field, expected] of Object.entries(collectionScope)) {
675
+ if (snapshot[field] !== expected) {
676
+ return false;
677
+ }
678
+ }
679
+ return true;
680
+ }
681
+ function buildSnapshot(op) {
682
+ const previous = asRecord(op.previousData);
683
+ const next = asRecord(op.data);
684
+ if (!previous && !next) return null;
685
+ return {
686
+ ...previous ?? {},
687
+ ...next ?? {}
688
+ };
689
+ }
690
+ function asRecord(value) {
691
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
692
+ return null;
693
+ }
694
+ return value;
695
+ }
696
+
697
+ // src/session/client-session.ts
698
+ var DEFAULT_BATCH_SIZE = 100;
699
+ var DEFAULT_SCHEMA_VERSION = 1;
700
+ var ClientSession = class {
701
+ state = "connected";
702
+ clientNodeId = null;
703
+ authContext = null;
704
+ sessionId;
705
+ transport;
706
+ store;
707
+ auth;
708
+ serializer;
709
+ emitter;
710
+ batchSize;
711
+ schemaVersion;
712
+ onRelay;
713
+ onClose;
714
+ constructor(options) {
715
+ this.sessionId = options.sessionId;
716
+ this.transport = options.transport;
717
+ this.store = options.store;
718
+ this.auth = options.auth ?? null;
719
+ this.serializer = options.serializer ?? new import_sync2.NegotiatedMessageSerializer("json");
720
+ this.emitter = options.emitter ?? null;
721
+ this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
722
+ this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
723
+ this.onRelay = options.onRelay ?? null;
724
+ this.onClose = options.onClose ?? null;
725
+ }
726
+ /**
727
+ * Start handling messages from the client transport.
728
+ */
729
+ start() {
730
+ this.transport.onMessage((msg) => this.handleMessage(msg));
731
+ this.transport.onClose((_code, _reason) => this.handleTransportClose());
732
+ this.transport.onError((_err) => {
733
+ if (this.state !== "closed") {
734
+ this.handleTransportClose();
735
+ }
736
+ });
737
+ }
738
+ /**
739
+ * Relay operations from another session to this client.
740
+ * Only relays if the session is in streaming state and transport is connected.
741
+ */
742
+ relayOperations(operations2) {
743
+ if (this.state !== "streaming" || !this.transport.isConnected()) return;
744
+ if (operations2.length === 0) return;
745
+ const visibleOperations = operations2.filter(
746
+ (op) => operationMatchesScopes(op, this.authContext?.scopes)
747
+ );
748
+ if (visibleOperations.length === 0) return;
749
+ const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
750
+ const msg = {
751
+ type: "operation-batch",
752
+ messageId: (0, import_core6.generateUUIDv7)(),
753
+ operations: serializedOps,
754
+ isFinal: true,
755
+ batchIndex: 0
756
+ };
757
+ this.transport.send(msg);
758
+ }
759
+ /**
760
+ * Close this session.
761
+ */
762
+ close(reason) {
763
+ if (this.state === "closed") return;
764
+ this.state = "closed";
765
+ if (this.transport.isConnected()) {
766
+ this.transport.close(1e3, reason ?? "session closed");
767
+ }
768
+ this.onClose?.(this.sessionId);
769
+ }
770
+ // --- Getters ---
771
+ getState() {
772
+ return this.state;
773
+ }
774
+ getSessionId() {
775
+ return this.sessionId;
776
+ }
777
+ getClientNodeId() {
778
+ return this.clientNodeId;
779
+ }
780
+ getAuthContext() {
781
+ return this.authContext;
782
+ }
783
+ isStreaming() {
784
+ return this.state === "streaming";
785
+ }
786
+ // --- Private protocol handlers ---
787
+ handleMessage(message) {
788
+ switch (message.type) {
789
+ case "handshake":
790
+ this.handleHandshake(message);
791
+ break;
792
+ case "operation-batch":
793
+ this.handleOperationBatch(message);
794
+ break;
795
+ // Acknowledgments from clients are noted but no action needed on server
796
+ case "acknowledgment":
797
+ break;
798
+ case "error":
799
+ break;
800
+ }
801
+ }
802
+ async handleHandshake(msg) {
803
+ if (this.state !== "connected") {
804
+ this.sendError("DUPLICATE_HANDSHAKE", "Handshake already completed", false);
805
+ return;
806
+ }
807
+ this.clientNodeId = msg.nodeId;
808
+ if (this.auth) {
809
+ const token = msg.authToken ?? "";
810
+ const context = await this.auth.authenticate(token);
811
+ if (!context) {
812
+ this.sendError("AUTH_FAILED", "Authentication failed", false);
813
+ this.close("authentication failed");
814
+ return;
815
+ }
816
+ this.authContext = context;
817
+ this.state = "authenticated";
818
+ }
819
+ const serverVector = this.store.getVersionVector();
820
+ const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
821
+ this.setSerializerWireFormat(selectedWireFormat);
822
+ const response = {
823
+ type: "handshake-response",
824
+ messageId: (0, import_core6.generateUUIDv7)(),
825
+ nodeId: this.store.getNodeId(),
826
+ versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
827
+ schemaVersion: this.schemaVersion,
828
+ accepted: true,
829
+ selectedWireFormat
830
+ };
831
+ this.transport.send(response);
832
+ this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
833
+ this.state = "syncing";
834
+ const clientVector = (0, import_sync2.wireToVersionVector)(msg.versionVector);
835
+ await this.sendDelta(clientVector);
836
+ this.state = "streaming";
837
+ }
838
+ async handleOperationBatch(msg) {
839
+ const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
840
+ const applied = [];
841
+ for (const op of operations2) {
842
+ if (!operationMatchesScopes(op, this.authContext?.scopes)) {
843
+ continue;
844
+ }
845
+ const result = await this.store.applyRemoteOperation(op);
846
+ if (result === "applied") {
847
+ applied.push(op);
848
+ }
849
+ }
850
+ if (operations2.length > 0) {
851
+ this.emitter?.emit({
852
+ type: "sync:received",
853
+ operations: operations2,
854
+ batchSize: operations2.length
855
+ });
856
+ }
857
+ const lastOp = operations2[operations2.length - 1];
858
+ const ack = {
859
+ type: "acknowledgment",
860
+ messageId: (0, import_core6.generateUUIDv7)(),
861
+ acknowledgedMessageId: msg.messageId,
862
+ lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
863
+ };
864
+ this.transport.send(ack);
865
+ if (applied.length > 0) {
866
+ this.onRelay?.(this.sessionId, applied);
867
+ }
868
+ }
869
+ async sendDelta(clientVector) {
870
+ const serverVector = this.store.getVersionVector();
871
+ const missing = [];
872
+ for (const [nodeId, serverSeq] of serverVector) {
873
+ const clientSeq = clientVector.get(nodeId) ?? 0;
874
+ if (serverSeq > clientSeq) {
875
+ const ops = await this.store.getOperationRange(nodeId, clientSeq + 1, serverSeq);
876
+ const visible = ops.filter((op) => operationMatchesScopes(op, this.authContext?.scopes));
877
+ missing.push(...visible);
878
+ }
879
+ }
880
+ if (missing.length === 0) {
881
+ const emptyBatch = {
882
+ type: "operation-batch",
883
+ messageId: (0, import_core6.generateUUIDv7)(),
884
+ operations: [],
885
+ isFinal: true,
886
+ batchIndex: 0
887
+ };
888
+ this.transport.send(emptyBatch);
889
+ return;
890
+ }
891
+ const sorted = (0, import_internal.topologicalSort)(missing);
892
+ const totalBatches = Math.ceil(sorted.length / this.batchSize);
893
+ for (let i = 0; i < totalBatches; i++) {
894
+ const start = i * this.batchSize;
895
+ const batchOps = sorted.slice(start, start + this.batchSize);
896
+ const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
897
+ const batchMsg = {
898
+ type: "operation-batch",
899
+ messageId: (0, import_core6.generateUUIDv7)(),
900
+ operations: serializedOps,
901
+ isFinal: i === totalBatches - 1,
902
+ batchIndex: i
903
+ };
904
+ this.transport.send(batchMsg);
905
+ this.emitter?.emit({
906
+ type: "sync:sent",
907
+ operations: batchOps,
908
+ batchSize: batchOps.length
909
+ });
910
+ }
911
+ }
912
+ sendError(code, message, retriable) {
913
+ const errorMsg = {
914
+ type: "error",
915
+ messageId: (0, import_core6.generateUUIDv7)(),
916
+ code,
917
+ message,
918
+ retriable
919
+ };
920
+ this.transport.send(errorMsg);
921
+ }
922
+ setSerializerWireFormat(format) {
923
+ if (typeof this.serializer.setWireFormat === "function") {
924
+ this.serializer.setWireFormat(format);
925
+ }
926
+ }
927
+ handleTransportClose() {
928
+ if (this.state === "closed") return;
929
+ this.state = "closed";
930
+ this.emitter?.emit({ type: "sync:disconnected", reason: "transport closed" });
931
+ this.onClose?.(this.sessionId);
932
+ }
933
+ };
934
+ function selectWireFormat(supportedWireFormats) {
935
+ if (supportedWireFormats?.includes("protobuf")) {
936
+ return "protobuf";
937
+ }
938
+ return "json";
939
+ }
940
+
941
+ // src/server/kora-sync-server.ts
942
+ var import_core7 = require("@korajs/core");
943
+ var import_sync3 = require("@korajs/sync");
944
+ var DEFAULT_MAX_CONNECTIONS = 0;
945
+ var DEFAULT_BATCH_SIZE2 = 100;
946
+ var DEFAULT_SCHEMA_VERSION2 = 1;
947
+ var DEFAULT_HOST = "0.0.0.0";
948
+ var DEFAULT_PATH = "/";
949
+ var KoraSyncServer = class {
950
+ store;
951
+ auth;
952
+ serializer;
953
+ emitter;
954
+ maxConnections;
955
+ batchSize;
956
+ schemaVersion;
957
+ port;
958
+ host;
959
+ path;
960
+ sessions = /* @__PURE__ */ new Map();
961
+ httpClients = /* @__PURE__ */ new Map();
962
+ httpSessionToClient = /* @__PURE__ */ new Map();
963
+ wsServer = null;
964
+ running = false;
965
+ constructor(config) {
966
+ this.store = config.store;
967
+ this.auth = config.auth ?? null;
968
+ this.serializer = config.serializer ?? new import_sync3.JsonMessageSerializer();
969
+ this.emitter = config.emitter ?? null;
970
+ this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
971
+ this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
972
+ this.schemaVersion = config.schemaVersion ?? DEFAULT_SCHEMA_VERSION2;
973
+ this.port = config.port;
974
+ this.host = config.host ?? DEFAULT_HOST;
975
+ this.path = config.path ?? DEFAULT_PATH;
976
+ }
977
+ /**
978
+ * Start the WebSocket server in standalone mode.
979
+ *
980
+ * @param wsServerImpl - Optional WebSocket server constructor for testing
981
+ */
982
+ async start(wsServerImpl) {
983
+ if (this.running) {
984
+ throw new import_core7.SyncError("Server is already running", { port: this.port });
985
+ }
986
+ if (!wsServerImpl && this.port === void 0) {
987
+ throw new import_core7.SyncError(
988
+ "Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
989
+ {}
990
+ );
991
+ }
992
+ if (wsServerImpl) {
993
+ this.wsServer = new wsServerImpl({
994
+ port: this.port,
995
+ host: this.host,
996
+ path: this.path
997
+ });
998
+ } else {
999
+ const { WebSocketServer } = await import("ws");
1000
+ this.wsServer = new WebSocketServer({
1001
+ port: this.port,
1002
+ host: this.host,
1003
+ path: this.path
1004
+ });
1005
+ }
1006
+ this.wsServer.on("connection", (ws) => {
1007
+ const transport = new WsServerTransport(
1008
+ ws,
1009
+ {
1010
+ serializer: this.serializer
1011
+ }
1012
+ );
1013
+ this.handleConnection(transport);
1014
+ });
1015
+ this.running = true;
1016
+ }
1017
+ /**
1018
+ * Stop the server. Closes all sessions and the WebSocket server.
1019
+ */
1020
+ async stop() {
1021
+ for (const session of this.sessions.values()) {
1022
+ session.close("server shutting down");
1023
+ }
1024
+ this.sessions.clear();
1025
+ this.httpClients.clear();
1026
+ this.httpSessionToClient.clear();
1027
+ if (this.wsServer) {
1028
+ await new Promise((resolve) => {
1029
+ this.wsServer?.close(() => resolve());
1030
+ });
1031
+ this.wsServer = null;
1032
+ }
1033
+ this.running = false;
1034
+ }
1035
+ /**
1036
+ * Handle one HTTP sync request for a long-polling client.
1037
+ *
1038
+ * A stable `clientId` identifies the logical connection across requests.
1039
+ */
1040
+ async handleHttpRequest(request) {
1041
+ if (!request.clientId || request.clientId.trim().length === 0) {
1042
+ return { status: 400 };
1043
+ }
1044
+ const client = this.getOrCreateHttpClient(request.clientId);
1045
+ if (request.method === "POST") {
1046
+ if (request.body === void 0) {
1047
+ return { status: 400 };
1048
+ }
1049
+ const payload = normalizeHttpBody(request.body, request.contentType);
1050
+ client.transport.receive(payload);
1051
+ return { status: 202 };
1052
+ }
1053
+ if (request.method === "GET") {
1054
+ const polled = client.transport.poll(request.ifNoneMatch);
1055
+ return {
1056
+ status: polled.status,
1057
+ body: polled.body,
1058
+ headers: polled.headers
1059
+ };
1060
+ }
1061
+ return {
1062
+ status: 405,
1063
+ headers: { allow: "GET, POST" }
1064
+ };
1065
+ }
1066
+ /**
1067
+ * Handle an incoming client connection (attach mode).
1068
+ * Creates a new ClientSession for the transport.
1069
+ *
1070
+ * @param transport - The server transport for the new connection
1071
+ * @returns The session ID
1072
+ */
1073
+ handleConnection(transport) {
1074
+ if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
1075
+ transport.send({
1076
+ type: "error",
1077
+ messageId: (0, import_core7.generateUUIDv7)(),
1078
+ code: "MAX_CONNECTIONS",
1079
+ message: `Server has reached maximum connections (${this.maxConnections})`,
1080
+ retriable: true
1081
+ });
1082
+ transport.close(4029, "max connections reached");
1083
+ throw new import_core7.SyncError("Maximum connections reached", {
1084
+ current: this.sessions.size,
1085
+ max: this.maxConnections
1086
+ });
1087
+ }
1088
+ const sessionId = (0, import_core7.generateUUIDv7)();
1089
+ const session = new ClientSession({
1090
+ sessionId,
1091
+ transport,
1092
+ store: this.store,
1093
+ auth: this.auth ?? void 0,
1094
+ serializer: this.serializer,
1095
+ emitter: this.emitter ?? void 0,
1096
+ batchSize: this.batchSize,
1097
+ schemaVersion: this.schemaVersion,
1098
+ onRelay: (sourceSessionId, operations2) => {
1099
+ this.handleRelay(sourceSessionId, operations2);
1100
+ },
1101
+ onClose: (sid) => {
1102
+ this.handleSessionClose(sid);
1103
+ }
1104
+ });
1105
+ this.sessions.set(sessionId, session);
1106
+ session.start();
1107
+ return sessionId;
1108
+ }
1109
+ /**
1110
+ * Get the current server status.
1111
+ */
1112
+ async getStatus() {
1113
+ return {
1114
+ running: this.running,
1115
+ connectedClients: this.sessions.size,
1116
+ port: this.port ?? null,
1117
+ totalOperations: await this.store.getOperationCount()
1118
+ };
1119
+ }
1120
+ /**
1121
+ * Get the number of currently connected clients.
1122
+ */
1123
+ getConnectionCount() {
1124
+ return this.sessions.size;
1125
+ }
1126
+ // --- Private ---
1127
+ handleRelay(sourceSessionId, operations2) {
1128
+ for (const [sessionId, session] of this.sessions) {
1129
+ if (sessionId === sourceSessionId) continue;
1130
+ session.relayOperations(operations2);
1131
+ }
1132
+ }
1133
+ handleSessionClose(sessionId) {
1134
+ this.sessions.delete(sessionId);
1135
+ const clientId = this.httpSessionToClient.get(sessionId);
1136
+ if (clientId) {
1137
+ this.httpSessionToClient.delete(sessionId);
1138
+ this.httpClients.delete(clientId);
1139
+ }
1140
+ }
1141
+ getOrCreateHttpClient(clientId) {
1142
+ const existing = this.httpClients.get(clientId);
1143
+ if (existing) {
1144
+ return existing;
1145
+ }
1146
+ const transport = new HttpServerTransport(this.serializer);
1147
+ const sessionId = this.handleConnection(transport);
1148
+ const client = { sessionId, transport };
1149
+ this.httpClients.set(clientId, client);
1150
+ this.httpSessionToClient.set(sessionId, clientId);
1151
+ return client;
1152
+ }
1153
+ };
1154
+ function normalizeHttpBody(body, contentType) {
1155
+ if (body instanceof Uint8Array) {
1156
+ return body;
1157
+ }
1158
+ if (contentType?.includes("application/x-protobuf")) {
1159
+ return new TextEncoder().encode(body);
1160
+ }
1161
+ return body;
1162
+ }
1163
+
1164
+ // src/auth/no-auth.ts
1165
+ var NoAuthProvider = class {
1166
+ async authenticate(_token) {
1167
+ return { userId: "anonymous" };
1168
+ }
1169
+ };
1170
+
1171
+ // src/auth/token-auth.ts
1172
+ var TokenAuthProvider = class {
1173
+ validate;
1174
+ constructor(options) {
1175
+ this.validate = options.validate;
1176
+ }
1177
+ async authenticate(token) {
1178
+ return this.validate(token);
1179
+ }
1180
+ };
1181
+
1182
+ // src/server/create-server.ts
1183
+ function createKoraServer(config) {
1184
+ return new KoraSyncServer(config);
1185
+ }
1186
+ // Annotate the CommonJS export names for ESM import in node:
1187
+ 0 && (module.exports = {
1188
+ ClientSession,
1189
+ HttpServerTransport,
1190
+ KoraSyncServer,
1191
+ MemoryServerStore,
1192
+ NoAuthProvider,
1193
+ PostgresServerStore,
1194
+ SqliteServerStore,
1195
+ TokenAuthProvider,
1196
+ WsServerTransport,
1197
+ createKoraServer,
1198
+ createPostgresServerStore,
1199
+ createSqliteServerStore
1200
+ });
1201
+ //# sourceMappingURL=index.cjs.map