@korajs/server 0.3.3 → 0.5.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.js CHANGED
@@ -1,3 +1,59 @@
1
+ // src/logging/structured-logger.ts
2
+ function defaultJsonSerializer(entry) {
3
+ return JSON.stringify(entry);
4
+ }
5
+ function defaultPrettySerializer(entry) {
6
+ const time = new Date(entry.timestamp).toISOString().slice(11, 23);
7
+ const levelTag = { info: " INFO", warn: " WARN", error: "ERROR" }[entry.level];
8
+ const node = entry.nodeId ? ` [${entry.nodeId.slice(0, 8)}]` : "";
9
+ const session = entry.sessionId ? ` [session:${entry.sessionId.slice(0, 8)}]` : "";
10
+ const dur = entry.duration !== void 0 ? ` +${entry.duration}ms` : "";
11
+ const count3 = entry.count !== void 0 ? ` (${entry.count})` : "";
12
+ const err = entry.error ? ` \u2014 ${entry.error}` : "";
13
+ return `${time} ${levelTag}${node}${session} ${entry.event}${count3}${dur}${err}`;
14
+ }
15
+ function createJsonLogger(writer) {
16
+ const out = writer ?? {
17
+ info: (s) => process.stdout.write(`${s}
18
+ `),
19
+ error: (s) => process.stderr.write(`${s}
20
+ `)
21
+ };
22
+ return {
23
+ log(entry) {
24
+ const line = defaultJsonSerializer(entry);
25
+ if (entry.level === "error") {
26
+ out.error(line);
27
+ } else {
28
+ out.info(line);
29
+ }
30
+ }
31
+ };
32
+ }
33
+ function createPrettyLogger(writer) {
34
+ const out = writer ?? { write: (s) => process.stdout.write(`${s}
35
+ `) };
36
+ return {
37
+ log(entry) {
38
+ out.write(defaultPrettySerializer(entry));
39
+ }
40
+ };
41
+ }
42
+ function createSilentLogger() {
43
+ return { log() {
44
+ } };
45
+ }
46
+ function createDefaultLogger() {
47
+ switch (process.env.NODE_ENV) {
48
+ case "production":
49
+ return createJsonLogger();
50
+ case "test":
51
+ return createSilentLogger();
52
+ default:
53
+ return createPrettyLogger();
54
+ }
55
+ }
56
+
1
57
  // src/store/memory-server-store.ts
2
58
  import { generateUUIDv7 } from "@korajs/core";
3
59
 
@@ -63,9 +119,7 @@ ALTER TABLE ${name} ADD COLUMN ${colDef}`);
63
119
  `CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${name} (${indexField})`
64
120
  );
65
121
  }
66
- statements.push(
67
- `CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`
68
- );
122
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`);
69
123
  return statements;
70
124
  }
71
125
  function generateAllCollectionDDL(schema, dialect) {
@@ -159,6 +213,9 @@ var MemoryServerStore = class {
159
213
  getNodeId() {
160
214
  return this.nodeId;
161
215
  }
216
+ getSchema() {
217
+ return this.schema;
218
+ }
162
219
  async setSchema(schema) {
163
220
  this.assertOpen();
164
221
  this.schema = schema;
@@ -180,7 +237,7 @@ var MemoryServerStore = class {
180
237
  if (op.sequenceNumber > currentSeq) {
181
238
  this.versionVector.set(op.nodeId, op.sequenceNumber);
182
239
  }
183
- if (this.schema && this.schema.collections[op.collection]) {
240
+ if (this.schema?.collections[op.collection]) {
184
241
  this.rebuildMaterializedRecord(op.collection, op.recordId);
185
242
  }
186
243
  return "applied";
@@ -197,7 +254,7 @@ var MemoryServerStore = class {
197
254
  }
198
255
  async materializeCollection(collection) {
199
256
  this.assertOpen();
200
- if (this.schema && this.schema.collections[collection]) {
257
+ if (this.schema?.collections[collection]) {
201
258
  return this.queryCollection(collection);
202
259
  }
203
260
  return this.materializeFromOps(collection);
@@ -206,13 +263,14 @@ var MemoryServerStore = class {
206
263
  this.assertOpen();
207
264
  this.assertSchema();
208
265
  this.assertCollection(collection);
266
+ const schema = this.schema;
209
267
  if (options?.where) {
210
268
  for (const key of Object.keys(options.where)) {
211
- validateFieldName(collection, key, this.schema);
269
+ validateFieldName(collection, key, schema);
212
270
  }
213
271
  }
214
272
  if (options?.orderBy) {
215
- validateFieldName(collection, options.orderBy, this.schema);
273
+ validateFieldName(collection, options.orderBy, schema);
216
274
  }
217
275
  const collectionMap = this.materializedRecords.get(collection);
218
276
  if (!collectionMap) return [];
@@ -279,9 +337,10 @@ var MemoryServerStore = class {
279
337
  this.assertOpen();
280
338
  this.assertSchema();
281
339
  this.assertCollection(collection);
340
+ const schema = this.schema;
282
341
  if (where) {
283
342
  for (const key of Object.keys(where)) {
284
- validateFieldName(collection, key, this.schema);
343
+ validateFieldName(collection, key, schema);
285
344
  }
286
345
  }
287
346
  const collectionMap = this.materializedRecords.get(collection);
@@ -306,6 +365,49 @@ var MemoryServerStore = class {
306
365
  async close() {
307
366
  this.closed = true;
308
367
  }
368
+ /**
369
+ * Wipes all in-memory state. For tests and E2E isolation only.
370
+ */
371
+ resetForTests() {
372
+ this.assertOpen();
373
+ this.operations.length = 0;
374
+ this.operationIndex.clear();
375
+ this.versionVector.clear();
376
+ this.materializedRecords.clear();
377
+ this.schema = null;
378
+ }
379
+ async exportBackup() {
380
+ this.assertOpen();
381
+ const { buildServerBackup } = await import("./server-backup-MOICK6MI.js");
382
+ return buildServerBackup(this.nodeId, this.operations, this.versionVector);
383
+ }
384
+ async importBackup(data, merge) {
385
+ this.assertOpen();
386
+ const { parseServerBackup } = await import("./server-backup-MOICK6MI.js");
387
+ const { operations: operations2, versionVector } = parseServerBackup(data);
388
+ if (merge) {
389
+ let restored = 0;
390
+ for (const op of operations2) {
391
+ const result = await this.applyRemoteOperation(op);
392
+ if (result === "applied") restored++;
393
+ }
394
+ return { operationsRestored: restored, success: true };
395
+ }
396
+ this.operations.length = 0;
397
+ this.operationIndex.clear();
398
+ this.versionVector.clear();
399
+ for (const [nid, seq] of versionVector) {
400
+ this.versionVector.set(nid, seq);
401
+ }
402
+ for (const op of operations2) {
403
+ this.operations.push(op);
404
+ this.operationIndex.set(op.id, op);
405
+ if (this.schema?.collections[op.collection]) {
406
+ this.rebuildMaterializedRecord(op.collection, op.recordId);
407
+ }
408
+ }
409
+ return { operationsRestored: operations2.length, success: true };
410
+ }
309
411
  // --- Testing helpers (not on interface) ---
310
412
  /**
311
413
  * Get all stored operations (for test assertions).
@@ -317,7 +419,7 @@ var MemoryServerStore = class {
317
419
  // Materialization internals
318
420
  // ---------------------------------------------------------------------------
319
421
  rebuildMaterializedRecord(collection, recordId) {
320
- const collectionDef = this.schema.collections[collection];
422
+ const collectionDef = this.schema?.collections[collection];
321
423
  if (!collectionDef) return;
322
424
  let collectionMap = this.materializedRecords.get(collection);
323
425
  if (!collectionMap) {
@@ -325,8 +427,10 @@ var MemoryServerStore = class {
325
427
  this.materializedRecords.set(collection, collectionMap);
326
428
  }
327
429
  const recordOps = this.operations.filter((op) => op.collection === collection && op.recordId === recordId).sort((a, b) => {
328
- if (a.timestamp.wallTime !== b.timestamp.wallTime) return a.timestamp.wallTime - b.timestamp.wallTime;
329
- if (a.timestamp.logical !== b.timestamp.logical) return a.timestamp.logical - b.timestamp.logical;
430
+ if (a.timestamp.wallTime !== b.timestamp.wallTime)
431
+ return a.timestamp.wallTime - b.timestamp.wallTime;
432
+ if (a.timestamp.logical !== b.timestamp.logical)
433
+ return a.timestamp.logical - b.timestamp.logical;
330
434
  return a.sequenceNumber - b.sequenceNumber;
331
435
  });
332
436
  const parsedOps = recordOps.map((op) => ({
@@ -378,8 +482,10 @@ var MemoryServerStore = class {
378
482
  // ---------------------------------------------------------------------------
379
483
  materializeFromOps(collection) {
380
484
  const collectionOps = this.operations.filter((op) => op.collection === collection).sort((a, b) => {
381
- if (a.timestamp.wallTime !== b.timestamp.wallTime) return a.timestamp.wallTime - b.timestamp.wallTime;
382
- if (a.timestamp.logical !== b.timestamp.logical) return a.timestamp.logical - b.timestamp.logical;
485
+ if (a.timestamp.wallTime !== b.timestamp.wallTime)
486
+ return a.timestamp.wallTime - b.timestamp.wallTime;
487
+ if (a.timestamp.logical !== b.timestamp.logical)
488
+ return a.timestamp.logical - b.timestamp.logical;
383
489
  return a.sequenceNumber - b.sequenceNumber;
384
490
  });
385
491
  const records = /* @__PURE__ */ new Map();
@@ -425,9 +531,10 @@ var MemoryServerStore = class {
425
531
  }
426
532
  }
427
533
  assertCollection(collection) {
428
- if (!this.schema.collections[collection]) {
534
+ const schema = this.schema;
535
+ if (!schema.collections[collection]) {
429
536
  throw new Error(
430
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
537
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
431
538
  );
432
539
  }
433
540
  }
@@ -492,6 +599,9 @@ var PostgresServerStore = class {
492
599
  getNodeId() {
493
600
  return this.nodeId;
494
601
  }
602
+ getSchema() {
603
+ return this.schema;
604
+ }
495
605
  async setSchema(schema) {
496
606
  this.assertOpen();
497
607
  await this.ready;
@@ -537,7 +647,7 @@ var PostgresServerStore = class {
537
647
  lastSeenAt: sql`${now}`
538
648
  }
539
649
  });
540
- if (this.schema && this.schema.collections[op.collection]) {
650
+ if (this.schema?.collections[op.collection]) {
541
651
  await this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
542
652
  }
543
653
  });
@@ -551,10 +661,7 @@ var PostgresServerStore = class {
551
661
  this.assertOpen();
552
662
  await this.ready;
553
663
  const rows = await this.db.select().from(pgOperations).where(
554
- and(
555
- eq(pgOperations.nodeId, nodeId),
556
- between(pgOperations.sequenceNumber, fromSeq, toSeq)
557
- )
664
+ and(eq(pgOperations.nodeId, nodeId), between(pgOperations.sequenceNumber, fromSeq, toSeq))
558
665
  ).orderBy(asc(pgOperations.sequenceNumber));
559
666
  return rows.map((row) => this.deserializeOperation(row));
560
667
  }
@@ -567,7 +674,7 @@ var PostgresServerStore = class {
567
674
  async materializeCollection(collection) {
568
675
  this.assertOpen();
569
676
  await this.ready;
570
- if (this.schema && this.schema.collections[collection]) {
677
+ if (this.schema?.collections[collection]) {
571
678
  return this.queryCollection(collection);
572
679
  }
573
680
  return this.materializeFromOpsLog(collection);
@@ -577,14 +684,15 @@ var PostgresServerStore = class {
577
684
  await this.ready;
578
685
  this.assertSchema();
579
686
  this.assertCollection(collection);
580
- const collectionDef = this.schema.collections[collection];
687
+ const schema = this.schema;
688
+ const collectionDef = schema.collections[collection];
581
689
  if (options?.where) {
582
690
  for (const key of Object.keys(options.where)) {
583
- validateFieldName(collection, key, this.schema);
691
+ validateFieldName(collection, key, schema);
584
692
  }
585
693
  }
586
694
  if (options?.orderBy) {
587
- validateFieldName(collection, options.orderBy, this.schema);
695
+ validateFieldName(collection, options.orderBy, schema);
588
696
  }
589
697
  const query = this.buildSelectQuery(collection, options);
590
698
  const rows = await this.db.execute(query);
@@ -595,7 +703,8 @@ var PostgresServerStore = class {
595
703
  await this.ready;
596
704
  this.assertSchema();
597
705
  this.assertCollection(collection);
598
- const collectionDef = this.schema.collections[collection];
706
+ const schema = this.schema;
707
+ const collectionDef = schema.collections[collection];
599
708
  const query = sql`SELECT * FROM ${sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
600
709
  const rows = await this.db.execute(query);
601
710
  if (rows.length === 0) return null;
@@ -606,9 +715,10 @@ var PostgresServerStore = class {
606
715
  await this.ready;
607
716
  this.assertSchema();
608
717
  this.assertCollection(collection);
718
+ const schema = this.schema;
609
719
  if (where) {
610
720
  for (const key of Object.keys(where)) {
611
- validateFieldName(collection, key, this.schema);
721
+ validateFieldName(collection, key, schema);
612
722
  }
613
723
  }
614
724
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -620,6 +730,45 @@ var PostgresServerStore = class {
620
730
  async close() {
621
731
  this.closed = true;
622
732
  }
733
+ async exportBackup() {
734
+ this.assertOpen();
735
+ await this.ready;
736
+ const { buildServerBackup } = await import("./server-backup-MOICK6MI.js");
737
+ const rows = await this.db.select().from(pgOperations).orderBy(asc(pgOperations.sequenceNumber));
738
+ const operations2 = rows.map((row) => this.deserializeOperation(row));
739
+ return buildServerBackup(this.nodeId, operations2, this.versionVector);
740
+ }
741
+ async importBackup(data, merge) {
742
+ this.assertOpen();
743
+ await this.ready;
744
+ const { parseServerBackup } = await import("./server-backup-MOICK6MI.js");
745
+ const { operations: operations2, versionVector } = parseServerBackup(data);
746
+ if (merge) {
747
+ let restored = 0;
748
+ for (const op of operations2) {
749
+ const result = await this.applyRemoteOperation(op);
750
+ if (result === "applied") restored++;
751
+ }
752
+ return { operationsRestored: restored, success: true };
753
+ }
754
+ const now = Date.now();
755
+ await this.db.transaction(async (tx) => {
756
+ await tx.delete(pgOperations);
757
+ await tx.delete(pgSyncState);
758
+ for (const [nid, seq] of versionVector) {
759
+ await tx.insert(pgSyncState).values({ nodeId: nid, maxSequenceNumber: seq, lastSeenAt: now }).onConflictDoNothing({ target: pgSyncState.nodeId });
760
+ }
761
+ for (const op of operations2) {
762
+ const row = this.serializeOperation(op, now);
763
+ await tx.insert(pgOperations).values(row).onConflictDoNothing({ target: pgOperations.id });
764
+ }
765
+ });
766
+ this.versionVector.clear();
767
+ for (const [nid, seq] of versionVector) {
768
+ this.versionVector.set(nid, seq);
769
+ }
770
+ return { operationsRestored: operations2.length, success: true };
771
+ }
623
772
  // ---------------------------------------------------------------------------
624
773
  // Materialization internals
625
774
  // ---------------------------------------------------------------------------
@@ -628,18 +777,13 @@ var PostgresServerStore = class {
628
777
  * all operations for that record.
629
778
  */
630
779
  async rebuildMaterializedRecord(txOrDb, collection, recordId) {
631
- const collectionDef = this.schema.collections[collection];
780
+ const collectionDef = this.schema?.collections[collection];
632
781
  if (!collectionDef) return;
633
782
  const ops = await txOrDb.select({
634
783
  type: pgOperations.type,
635
784
  data: pgOperations.data,
636
785
  wallTime: pgOperations.wallTime
637
- }).from(pgOperations).where(
638
- and(
639
- eq(pgOperations.collection, collection),
640
- eq(pgOperations.recordId, recordId)
641
- )
642
- ).orderBy(
786
+ }).from(pgOperations).where(and(eq(pgOperations.collection, collection), eq(pgOperations.recordId, recordId))).orderBy(
643
787
  asc(pgOperations.wallTime),
644
788
  asc(pgOperations.logical),
645
789
  asc(pgOperations.sequenceNumber)
@@ -709,7 +853,7 @@ var PostgresServerStore = class {
709
853
  * Backfill a single collection's materialized table from operations.
710
854
  */
711
855
  async backfillCollection(collectionName) {
712
- const collectionDef = this.schema.collections[collectionName];
856
+ const collectionDef = this.schema?.collections[collectionName];
713
857
  if (!collectionDef) return;
714
858
  const allOps = await this.db.select({
715
859
  recordId: pgOperations.recordId,
@@ -766,9 +910,7 @@ var PostgresServerStore = class {
766
910
  options?.where ?? {},
767
911
  options?.includeDeleted ?? false
768
912
  );
769
- const parts = [
770
- sql`SELECT * FROM ${sql.raw(collection)} WHERE ${whereClause}`
771
- ];
913
+ const parts = [sql`SELECT * FROM ${sql.raw(collection)} WHERE ${whereClause}`];
772
914
  if (options?.orderBy) {
773
915
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
774
916
  parts.push(sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -881,12 +1023,8 @@ var PostgresServerStore = class {
881
1023
  await this.db.execute(
882
1024
  sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
883
1025
  );
884
- await this.db.execute(
885
- sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`
886
- );
887
- await this.db.execute(
888
- sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`
889
- );
1026
+ await this.db.execute(sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`);
1027
+ await this.db.execute(sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`);
890
1028
  await this.db.execute(
891
1029
  sql`CREATE INDEX IF NOT EXISTS idx_collection_record ON operations (collection, record_id)`
892
1030
  );
@@ -954,9 +1092,10 @@ var PostgresServerStore = class {
954
1092
  }
955
1093
  }
956
1094
  assertCollection(collection) {
957
- if (!this.schema.collections[collection]) {
1095
+ const schema = this.schema;
1096
+ if (!schema.collections[collection]) {
958
1097
  throw new Error(
959
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
1098
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
960
1099
  );
961
1100
  }
962
1101
  }
@@ -1047,6 +1186,9 @@ var SqliteServerStore = class {
1047
1186
  getNodeId() {
1048
1187
  return this.nodeId;
1049
1188
  }
1189
+ getSchema() {
1190
+ return this.schema;
1191
+ }
1050
1192
  async setSchema(schema) {
1051
1193
  this.assertOpen();
1052
1194
  this.schema = schema;
@@ -1089,7 +1231,7 @@ var SqliteServerStore = class {
1089
1231
  lastSeenAt: sql2`${now}`
1090
1232
  }
1091
1233
  }).run();
1092
- if (this.schema && this.schema.collections[op.collection]) {
1234
+ if (this.schema?.collections[op.collection]) {
1093
1235
  this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
1094
1236
  }
1095
1237
  return "applied";
@@ -1108,7 +1250,7 @@ var SqliteServerStore = class {
1108
1250
  }
1109
1251
  async materializeCollection(collection) {
1110
1252
  this.assertOpen();
1111
- if (this.schema && this.schema.collections[collection]) {
1253
+ if (this.schema?.collections[collection]) {
1112
1254
  return this.queryCollection(collection);
1113
1255
  }
1114
1256
  return this.materializeFromOpsLog(collection);
@@ -1117,14 +1259,15 @@ var SqliteServerStore = class {
1117
1259
  this.assertOpen();
1118
1260
  this.assertSchema();
1119
1261
  this.assertCollection(collection);
1120
- const collectionDef = this.schema.collections[collection];
1262
+ const schema = this.schema;
1263
+ const collectionDef = schema.collections[collection];
1121
1264
  if (options?.where) {
1122
1265
  for (const key of Object.keys(options.where)) {
1123
- validateFieldName(collection, key, this.schema);
1266
+ validateFieldName(collection, key, schema);
1124
1267
  }
1125
1268
  }
1126
1269
  if (options?.orderBy) {
1127
- validateFieldName(collection, options.orderBy, this.schema);
1270
+ validateFieldName(collection, options.orderBy, schema);
1128
1271
  }
1129
1272
  const query = this.buildSelectQuery(collection, options);
1130
1273
  const rows = this.db.all(query);
@@ -1134,7 +1277,8 @@ var SqliteServerStore = class {
1134
1277
  this.assertOpen();
1135
1278
  this.assertSchema();
1136
1279
  this.assertCollection(collection);
1137
- const collectionDef = this.schema.collections[collection];
1280
+ const schema = this.schema;
1281
+ const collectionDef = schema.collections[collection];
1138
1282
  const query = sql2`SELECT * FROM ${sql2.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
1139
1283
  const rows = this.db.all(query);
1140
1284
  if (rows.length === 0) return null;
@@ -1144,9 +1288,10 @@ var SqliteServerStore = class {
1144
1288
  this.assertOpen();
1145
1289
  this.assertSchema();
1146
1290
  this.assertCollection(collection);
1291
+ const schema = this.schema;
1147
1292
  if (where) {
1148
1293
  for (const key of Object.keys(where)) {
1149
- validateFieldName(collection, key, this.schema);
1294
+ validateFieldName(collection, key, schema);
1150
1295
  }
1151
1296
  }
1152
1297
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -1157,6 +1302,39 @@ var SqliteServerStore = class {
1157
1302
  async close() {
1158
1303
  this.closed = true;
1159
1304
  }
1305
+ async exportBackup() {
1306
+ this.assertOpen();
1307
+ const { buildServerBackup } = await import("./server-backup-MOICK6MI.js");
1308
+ const rows = this.db.select().from(operations).all();
1309
+ const deserialized = rows.map((row) => this.deserializeOperation(row));
1310
+ const vv = this.getVersionVector();
1311
+ return buildServerBackup(this.nodeId, deserialized, vv);
1312
+ }
1313
+ async importBackup(data, merge) {
1314
+ this.assertOpen();
1315
+ const { parseServerBackup } = await import("./server-backup-MOICK6MI.js");
1316
+ const { operations: ops, versionVector } = parseServerBackup(data);
1317
+ if (merge) {
1318
+ let restored = 0;
1319
+ for (const op of ops) {
1320
+ const result = await this.applyRemoteOperation(op);
1321
+ if (result === "applied") restored++;
1322
+ }
1323
+ return { operationsRestored: restored, success: true };
1324
+ }
1325
+ this.db.transaction((tx) => {
1326
+ tx.run(sql2.raw("DELETE FROM operations"));
1327
+ tx.run(sql2.raw("DELETE FROM sync_state"));
1328
+ for (const [nid, seq] of versionVector) {
1329
+ tx.insert(syncState).values({ nodeId: nid, maxSequenceNumber: seq, lastSeenAt: Date.now() }).run();
1330
+ }
1331
+ for (const op of ops) {
1332
+ const row = this.serializeOperation(op, Date.now());
1333
+ tx.insert(operations).values(row).run();
1334
+ }
1335
+ });
1336
+ return { operationsRestored: ops.length, success: true };
1337
+ }
1160
1338
  // ---------------------------------------------------------------------------
1161
1339
  // Materialization internals
1162
1340
  // ---------------------------------------------------------------------------
@@ -1166,18 +1344,13 @@ var SqliteServerStore = class {
1166
1344
  * transaction for atomic dual-write.
1167
1345
  */
1168
1346
  rebuildMaterializedRecord(txOrDb, collection, recordId) {
1169
- const collectionDef = this.schema.collections[collection];
1347
+ const collectionDef = this.schema?.collections[collection];
1170
1348
  if (!collectionDef) return;
1171
1349
  const ops = txOrDb.select({
1172
1350
  type: operations.type,
1173
1351
  data: operations.data,
1174
1352
  wallTime: operations.wallTime
1175
- }).from(operations).where(
1176
- and2(
1177
- eq2(operations.collection, collection),
1178
- eq2(operations.recordId, recordId)
1179
- )
1180
- ).orderBy(asc2(operations.wallTime), asc2(operations.logical), asc2(operations.sequenceNumber)).all();
1353
+ }).from(operations).where(and2(eq2(operations.collection, collection), eq2(operations.recordId, recordId))).orderBy(asc2(operations.wallTime), asc2(operations.logical), asc2(operations.sequenceNumber)).all();
1181
1354
  const parsedOps = ops.map((op) => ({
1182
1355
  type: op.type,
1183
1356
  data: op.data !== null ? JSON.parse(op.data) : null
@@ -1246,7 +1419,7 @@ var SqliteServerStore = class {
1246
1419
  * Backfill a single collection's materialized table from operations.
1247
1420
  */
1248
1421
  backfillCollection(collectionName) {
1249
- const collectionDef = this.schema.collections[collectionName];
1422
+ const collectionDef = this.schema?.collections[collectionName];
1250
1423
  if (!collectionDef) return;
1251
1424
  const allOps = this.db.select({
1252
1425
  recordId: operations.recordId,
@@ -1301,9 +1474,7 @@ var SqliteServerStore = class {
1301
1474
  options?.where ?? {},
1302
1475
  options?.includeDeleted ?? false
1303
1476
  );
1304
- const parts = [
1305
- sql2`SELECT * FROM ${sql2.raw(collection)} WHERE ${whereClause}`
1306
- ];
1477
+ const parts = [sql2`SELECT * FROM ${sql2.raw(collection)} WHERE ${whereClause}`];
1307
1478
  if (options?.orderBy) {
1308
1479
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
1309
1480
  parts.push(sql2.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -1478,9 +1649,10 @@ var SqliteServerStore = class {
1478
1649
  }
1479
1650
  }
1480
1651
  assertCollection(collection) {
1481
- if (!this.schema.collections[collection]) {
1652
+ const schema = this.schema;
1653
+ if (!schema.collections[collection]) {
1482
1654
  throw new Error(
1483
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
1655
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
1484
1656
  );
1485
1657
  }
1486
1658
  }
@@ -1644,10 +1816,219 @@ import { generateUUIDv7 as generateUUIDv74 } from "@korajs/core";
1644
1816
  import { topologicalSort } from "@korajs/core/internal";
1645
1817
  import {
1646
1818
  NegotiatedMessageSerializer,
1819
+ SCHEMA_MISMATCH_PREFIX,
1820
+ createDeltaCursorFromBatch,
1821
+ decodeDeltaCursor,
1822
+ dedupeQuerySubsets,
1823
+ encodeDeltaCursor,
1824
+ isClientSchemaVersionSupported,
1825
+ operationMatchesQuerySubsets,
1826
+ sliceOperationsAfterCursor,
1647
1827
  versionVectorToWire,
1648
1828
  wireToVersionVector
1649
1829
  } from "@korajs/sync";
1650
1830
 
1831
+ // src/apply/apply-server-operation.ts
1832
+ import { buildMergeRelationLookup, checkReferentialIntegrityOnDelete } from "@korajs/merge";
1833
+
1834
+ // src/constraints/operation-constraint-validator.ts
1835
+ import { checkConstraints } from "@korajs/merge";
1836
+
1837
+ // src/constraints/server-constraint-context.ts
1838
+ function createServerConstraintContext(store) {
1839
+ return {
1840
+ async queryRecords(collection, where) {
1841
+ const rows = await store.queryCollection(collection, { where });
1842
+ return rows.map((row) => ({ ...row }));
1843
+ },
1844
+ async countRecords(collection, where) {
1845
+ return store.countCollection(collection, where);
1846
+ }
1847
+ };
1848
+ }
1849
+
1850
+ // src/constraints/operation-constraint-validator.ts
1851
+ async function validateIncomingOperationConstraints(store, op, schema) {
1852
+ if (!schema) {
1853
+ return { valid: true };
1854
+ }
1855
+ if (op.type === "delete") {
1856
+ return { valid: true };
1857
+ }
1858
+ const collectionDef = schema.collections[op.collection];
1859
+ if (!collectionDef || collectionDef.constraints.length === 0) {
1860
+ return { valid: true };
1861
+ }
1862
+ const candidate = await projectCandidateRecord(store, op);
1863
+ if (candidate === null) {
1864
+ return { valid: true };
1865
+ }
1866
+ const ctx = createServerConstraintContext(store);
1867
+ const violations = await checkConstraints(
1868
+ candidate,
1869
+ op.recordId,
1870
+ op.collection,
1871
+ collectionDef,
1872
+ ctx
1873
+ );
1874
+ if (violations.length === 0) {
1875
+ return { valid: true };
1876
+ }
1877
+ const first = violations[0];
1878
+ if (first === void 0) {
1879
+ return { valid: true };
1880
+ }
1881
+ return {
1882
+ valid: false,
1883
+ code: "CONSTRAINT_VIOLATION",
1884
+ message: first.message
1885
+ };
1886
+ }
1887
+ async function projectCandidateRecord(store, op) {
1888
+ if (op.data === null) {
1889
+ return null;
1890
+ }
1891
+ const existing = await store.findRecord(op.collection, op.recordId);
1892
+ const base = existing ? { ...existing } : {};
1893
+ if (op.type === "insert") {
1894
+ return { ...op.data, id: op.recordId };
1895
+ }
1896
+ return { ...base, ...op.data, id: op.recordId };
1897
+ }
1898
+
1899
+ // src/constraints/server-referential-context.ts
1900
+ function createServerReferentialContext(store) {
1901
+ return {
1902
+ async queryRecords(collection, where) {
1903
+ const rows = await store.queryCollection(collection, { where });
1904
+ return rows.map((row) => ({ ...row }));
1905
+ },
1906
+ async recordExists(collection, recordId) {
1907
+ const row = await store.findRecord(collection, recordId);
1908
+ return row !== null && row._deleted !== 1;
1909
+ }
1910
+ };
1911
+ }
1912
+
1913
+ // src/apply/server-side-effect-operation.ts
1914
+ import { HybridLogicalClock, createOperation } from "@korajs/core";
1915
+ async function createServerSideEffectOperation(store, parentOp, effect, schemaVersion, sequenceNumber) {
1916
+ const nodeId = store.getNodeId();
1917
+ const clock = new HybridLogicalClock(nodeId);
1918
+ clock.receive(parentOp.timestamp);
1919
+ return createOperation(
1920
+ {
1921
+ nodeId,
1922
+ type: effect.type === "delete" ? "delete" : "update",
1923
+ collection: effect.collection,
1924
+ recordId: effect.recordId,
1925
+ data: effect.data,
1926
+ previousData: effect.previousData,
1927
+ sequenceNumber,
1928
+ causalDeps: [parentOp.id],
1929
+ schemaVersion
1930
+ },
1931
+ clock
1932
+ );
1933
+ }
1934
+ function nextServerSequenceNumber(store) {
1935
+ const nodeId = store.getNodeId();
1936
+ const current = store.getVersionVector().get(nodeId) ?? 0;
1937
+ return current + 1;
1938
+ }
1939
+
1940
+ // src/apply/apply-server-operation.ts
1941
+ async function applyServerOperation(store, op, relationLookup) {
1942
+ const schema = store.getSchema();
1943
+ const lookup = relationLookup ?? (schema ? buildMergeRelationLookup(schema) : /* @__PURE__ */ new Map());
1944
+ const constraintCheck = await validateIncomingOperationConstraints(store, op, schema);
1945
+ if (!constraintCheck.valid) {
1946
+ return {
1947
+ result: "skipped",
1948
+ appliedOperations: [],
1949
+ rejection: {
1950
+ code: constraintCheck.code ?? "CONSTRAINT_VIOLATION",
1951
+ message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint`
1952
+ }
1953
+ };
1954
+ }
1955
+ if (op.type === "delete" && schema) {
1956
+ const refCtx = createServerReferentialContext(store);
1957
+ const referential = await checkReferentialIntegrityOnDelete(op, schema, refCtx, lookup);
1958
+ if (!referential.allowed) {
1959
+ return {
1960
+ result: "skipped",
1961
+ appliedOperations: [],
1962
+ rejection: {
1963
+ code: "REFERENTIAL_INTEGRITY",
1964
+ message: `Operation "${op.id}" violates referential integrity on "${op.collection}"`
1965
+ }
1966
+ };
1967
+ }
1968
+ const primaryResult = await store.applyRemoteOperation(op);
1969
+ if (primaryResult !== "applied") {
1970
+ return { result: primaryResult, appliedOperations: [] };
1971
+ }
1972
+ const appliedOperations = [op];
1973
+ let serverSeq = nextServerSequenceNumber(store);
1974
+ for (const effect of referential.sideEffectOps) {
1975
+ const sideOp = await createServerSideEffectOperation(
1976
+ store,
1977
+ op,
1978
+ effect,
1979
+ op.schemaVersion,
1980
+ serverSeq
1981
+ );
1982
+ serverSeq += 1;
1983
+ const sideResult = await store.applyRemoteOperation(sideOp);
1984
+ if (sideResult === "applied") {
1985
+ appliedOperations.push(sideOp);
1986
+ }
1987
+ }
1988
+ return { result: "applied", appliedOperations };
1989
+ }
1990
+ const result = await store.applyRemoteOperation(op);
1991
+ return {
1992
+ result,
1993
+ appliedOperations: result === "applied" ? [op] : []
1994
+ };
1995
+ }
1996
+
1997
+ // src/scopes/resolve-session-scopes.ts
1998
+ import { buildScopeMap } from "@korajs/core";
1999
+ function resolveSessionScopes(schema, options) {
2000
+ const { handshakeScope, authScopes, scopeValues } = options;
2001
+ let resolved;
2002
+ if (schema && scopeValues && Object.keys(scopeValues).length > 0) {
2003
+ resolved = buildScopeMap(schema, scopeValues);
2004
+ } else if (handshakeScope) {
2005
+ resolved = { ...handshakeScope };
2006
+ } else if (authScopes) {
2007
+ resolved = { ...authScopes };
2008
+ }
2009
+ if (schema && !resolved && scopeValues) {
2010
+ resolved = buildScopeMap(schema, scopeValues);
2011
+ }
2012
+ if (authScopes) {
2013
+ resolved = mergeScopeMaps(resolved, authScopes, "auth");
2014
+ }
2015
+ if (handshakeScope) {
2016
+ resolved = mergeScopeMaps(resolved, handshakeScope, "handshake");
2017
+ }
2018
+ return resolved && Object.keys(resolved).length > 0 ? resolved : void 0;
2019
+ }
2020
+ function mergeScopeMaps(base, overlay, source) {
2021
+ const merged = { ...base ?? {} };
2022
+ for (const [collection, overlayScope] of Object.entries(overlay)) {
2023
+ if (source === "auth") {
2024
+ merged[collection] = { ...merged[collection] ?? {}, ...overlayScope };
2025
+ } else {
2026
+ merged[collection] = { ...overlayScope ?? {}, ...merged[collection] ?? {} };
2027
+ }
2028
+ }
2029
+ return merged;
2030
+ }
2031
+
1651
2032
  // src/scopes/server-scope-filter.ts
1652
2033
  function operationMatchesScopes(op, scopes) {
1653
2034
  if (!scopes) return true;
@@ -1678,6 +2059,55 @@ function asRecord(value) {
1678
2059
  return value;
1679
2060
  }
1680
2061
 
2062
+ // src/session/operation-validation.ts
2063
+ var SERVER_MAX_TIMESTAMP_FUTURE_MS = 6e4;
2064
+ function isOperationTimestampValid(op, now = Date.now()) {
2065
+ return op.timestamp.wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS;
2066
+ }
2067
+
2068
+ // src/session/session-operation-limits.ts
2069
+ var DEFAULT_MAX_OPERATION_BYTES = 256 * 1024;
2070
+ var DEFAULT_MAX_OPS_PER_MINUTE = 600;
2071
+ function measureOperationBytes(op) {
2072
+ return Buffer.byteLength(JSON.stringify(op), "utf8");
2073
+ }
2074
+ function validateOperationSize(op, maxBytes = DEFAULT_MAX_OPERATION_BYTES) {
2075
+ const bytes = measureOperationBytes(op);
2076
+ if (bytes <= maxBytes) {
2077
+ return { valid: true, bytes };
2078
+ }
2079
+ return {
2080
+ valid: false,
2081
+ bytes,
2082
+ message: `Operation "${op.id}" exceeds max size (${String(bytes)} > ${String(maxBytes)} bytes)`
2083
+ };
2084
+ }
2085
+ var SessionRateLimiter = class {
2086
+ constructor(maxOpsPerMinute = DEFAULT_MAX_OPS_PER_MINUTE) {
2087
+ this.maxOpsPerMinute = maxOpsPerMinute;
2088
+ }
2089
+ maxOpsPerMinute;
2090
+ windowStartMs = Date.now();
2091
+ count = 0;
2092
+ get limit() {
2093
+ return this.maxOpsPerMinute;
2094
+ }
2095
+ /** Record N operations and return false when the limit is exceeded. */
2096
+ allow(count3 = 1) {
2097
+ const now = Date.now();
2098
+ if (now - this.windowStartMs >= 6e4) {
2099
+ this.windowStartMs = now;
2100
+ this.count = 0;
2101
+ }
2102
+ this.count += count3;
2103
+ return this.count <= this.maxOpsPerMinute;
2104
+ }
2105
+ reset() {
2106
+ this.windowStartMs = Date.now();
2107
+ this.count = 0;
2108
+ }
2109
+ };
2110
+
1681
2111
  // src/session/client-session.ts
1682
2112
  var DEFAULT_BATCH_SIZE = 100;
1683
2113
  var DEFAULT_SCHEMA_VERSION = 1;
@@ -1685,6 +2115,8 @@ var ClientSession = class {
1685
2115
  state = "connected";
1686
2116
  clientNodeId = null;
1687
2117
  authContext = null;
2118
+ syncQuerySubsets = [];
2119
+ resumeDeltaCursor = null;
1688
2120
  sessionId;
1689
2121
  transport;
1690
2122
  store;
@@ -1693,8 +2125,14 @@ var ClientSession = class {
1693
2125
  emitter;
1694
2126
  batchSize;
1695
2127
  schemaVersion;
2128
+ supportedSchemaVersions;
1696
2129
  onRelay;
2130
+ onAwarenessUpdate;
2131
+ onYjsDocUpdate;
1697
2132
  onClose;
2133
+ maxOperationBytes;
2134
+ maxOpsPerMinute;
2135
+ rateLimiter;
1698
2136
  constructor(options) {
1699
2137
  this.sessionId = options.sessionId;
1700
2138
  this.transport = options.transport;
@@ -1704,14 +2142,24 @@ var ClientSession = class {
1704
2142
  this.emitter = options.emitter ?? null;
1705
2143
  this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
1706
2144
  this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
2145
+ const supported = options.supportedSchemaVersions;
2146
+ this.supportedSchemaVersions = supported ?? {
2147
+ min: this.schemaVersion,
2148
+ max: this.schemaVersion
2149
+ };
1707
2150
  this.onRelay = options.onRelay ?? null;
2151
+ this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
2152
+ this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
1708
2153
  this.onClose = options.onClose ?? null;
2154
+ this.maxOperationBytes = options.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
2155
+ this.maxOpsPerMinute = options.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE;
2156
+ this.rateLimiter = new SessionRateLimiter(this.maxOpsPerMinute);
1709
2157
  }
1710
2158
  /**
1711
2159
  * Start handling messages from the client transport.
1712
2160
  */
1713
2161
  start() {
1714
- this.transport.onMessage((msg) => this.handleMessage(msg));
2162
+ this.transport.onMessage((msg) => this.enqueueMessage(msg));
1715
2163
  this.transport.onClose((_code, _reason) => this.handleTransportClose());
1716
2164
  this.transport.onError((_err) => {
1717
2165
  if (this.state !== "closed") {
@@ -1726,9 +2174,7 @@ var ClientSession = class {
1726
2174
  relayOperations(operations2) {
1727
2175
  if (this.state !== "streaming" || !this.transport.isConnected()) return;
1728
2176
  if (operations2.length === 0) return;
1729
- const visibleOperations = operations2.filter(
1730
- (op) => operationMatchesScopes(op, this.authContext?.scopes)
1731
- );
2177
+ const visibleOperations = operations2.filter((op) => this.operationVisibleToClient(op));
1732
2178
  if (visibleOperations.length === 0) return;
1733
2179
  const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
1734
2180
  const msg = {
@@ -1738,7 +2184,7 @@ var ClientSession = class {
1738
2184
  isFinal: true,
1739
2185
  batchIndex: 0
1740
2186
  };
1741
- this.transport.send(msg);
2187
+ this.sendToClient(msg);
1742
2188
  }
1743
2189
  /**
1744
2190
  * Close this session.
@@ -1767,22 +2213,55 @@ var ClientSession = class {
1767
2213
  isStreaming() {
1768
2214
  return this.state === "streaming";
1769
2215
  }
2216
+ /**
2217
+ * Get the transport for this session.
2218
+ * Used by the awareness relay to send messages to this client.
2219
+ */
2220
+ getTransport() {
2221
+ return this.transport;
2222
+ }
1770
2223
  // --- Private protocol handlers ---
1771
- handleMessage(message) {
2224
+ messageChain = Promise.resolve();
2225
+ /** Send to the client when the transport is still connected; no-op otherwise. */
2226
+ sendToClient(message) {
2227
+ if (!this.transport.isConnected()) {
2228
+ return false;
2229
+ }
2230
+ try {
2231
+ this.transport.send(message);
2232
+ return true;
2233
+ } catch {
2234
+ return false;
2235
+ }
2236
+ }
2237
+ enqueueMessage(message) {
2238
+ this.messageChain = this.messageChain.then(() => this.handleMessageAsync(message)).catch((error) => this.handleMessageFailure(error));
2239
+ }
2240
+ async handleMessageAsync(message) {
1772
2241
  switch (message.type) {
1773
2242
  case "handshake":
1774
- this.handleHandshake(message);
2243
+ await this.handleHandshake(message);
1775
2244
  break;
1776
2245
  case "operation-batch":
1777
- this.handleOperationBatch(message);
2246
+ await this.handleOperationBatch(message);
1778
2247
  break;
1779
- // Acknowledgments from clients are noted but no action needed on server
1780
2248
  case "acknowledgment":
1781
2249
  break;
1782
2250
  case "error":
1783
2251
  break;
2252
+ case "awareness-update":
2253
+ this.handleAwarenessUpdate(message);
2254
+ break;
2255
+ case "yjs-doc-update":
2256
+ this.handleYjsDocUpdate(message);
2257
+ break;
1784
2258
  }
1785
2259
  }
2260
+ handleMessageFailure(error) {
2261
+ const reason = error instanceof Error ? error.message : "Message handling failed";
2262
+ this.sendError("SYNC_ERROR", reason, true);
2263
+ this.close(reason);
2264
+ }
1786
2265
  async handleHandshake(msg) {
1787
2266
  if (this.state !== "connected") {
1788
2267
  this.sendError("DUPLICATE_HANDSHAKE", "Handshake already completed", false);
@@ -1800,9 +2279,43 @@ var ClientSession = class {
1800
2279
  this.authContext = context;
1801
2280
  this.state = "authenticated";
1802
2281
  }
2282
+ const resolvedScopes = resolveSessionScopes(this.store.getSchema(), {
2283
+ handshakeScope: msg.syncScope,
2284
+ authScopes: this.authContext?.scopes
2285
+ });
2286
+ if (resolvedScopes) {
2287
+ if (this.authContext) {
2288
+ this.authContext = { ...this.authContext, scopes: resolvedScopes };
2289
+ } else {
2290
+ this.authContext = { userId: msg.nodeId, scopes: resolvedScopes };
2291
+ }
2292
+ }
2293
+ if (msg.syncQueries && msg.syncQueries.length > 0) {
2294
+ this.syncQuerySubsets = dedupeQuerySubsets(msg.syncQueries);
2295
+ } else {
2296
+ this.syncQuerySubsets = [];
2297
+ }
2298
+ this.resumeDeltaCursor = msg.deltaCursor ? decodeDeltaCursor(msg.deltaCursor) : null;
1803
2299
  const serverVector = this.store.getVersionVector();
1804
2300
  const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
1805
2301
  this.setSerializerWireFormat(selectedWireFormat);
2302
+ if (!isClientSchemaVersionSupported(msg.schemaVersion, this.supportedSchemaVersions)) {
2303
+ const { min, max } = this.supportedSchemaVersions;
2304
+ const response2 = {
2305
+ type: "handshake-response",
2306
+ messageId: generateUUIDv74(),
2307
+ nodeId: this.store.getNodeId(),
2308
+ versionVector: versionVectorToWire(serverVector),
2309
+ schemaVersion: this.schemaVersion,
2310
+ accepted: false,
2311
+ rejectReason: `${SCHEMA_MISMATCH_PREFIX}: client schema version ${msg.schemaVersion} not in supported range [${min}, ${max}]`,
2312
+ supportedSchemaMin: min,
2313
+ supportedSchemaMax: max
2314
+ };
2315
+ this.sendToClient(response2);
2316
+ this.close("schema version mismatch");
2317
+ return;
2318
+ }
1806
2319
  const response = {
1807
2320
  type: "handshake-response",
1808
2321
  messageId: generateUUIDv74(),
@@ -1810,9 +2323,12 @@ var ClientSession = class {
1810
2323
  versionVector: versionVectorToWire(serverVector),
1811
2324
  schemaVersion: this.schemaVersion,
1812
2325
  accepted: true,
1813
- selectedWireFormat
2326
+ selectedWireFormat,
2327
+ // Confirm the accepted scope so the client knows what data will be synced.
2328
+ // This may differ from what the client requested if auth scopes are narrower.
2329
+ ...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
1814
2330
  };
1815
- this.transport.send(response);
2331
+ this.sendToClient(response);
1816
2332
  this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
1817
2333
  this.state = "syncing";
1818
2334
  const clientVector = wireToVersionVector(msg.versionVector);
@@ -1822,13 +2338,56 @@ var ClientSession = class {
1822
2338
  async handleOperationBatch(msg) {
1823
2339
  const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
1824
2340
  const applied = [];
2341
+ const rejected = [];
1825
2342
  for (const op of operations2) {
1826
- if (!operationMatchesScopes(op, this.authContext?.scopes)) {
2343
+ if (!this.operationVisibleToClient(op)) {
2344
+ rejected.push(op);
1827
2345
  continue;
1828
2346
  }
1829
- const result = await this.store.applyRemoteOperation(op);
1830
- if (result === "applied") {
1831
- applied.push(op);
2347
+ if (!isOperationTimestampValid(op)) {
2348
+ this.sendError(
2349
+ "INVALID_TIMESTAMP",
2350
+ `Operation "${op.id}" timestamp is too far in the future`,
2351
+ false
2352
+ );
2353
+ continue;
2354
+ }
2355
+ if (!this.rateLimiter.allow(1)) {
2356
+ this.sendError(
2357
+ "RATE_LIMIT",
2358
+ `Session exceeded operation rate limit (${String(this.maxOpsPerMinute)} ops/min)`,
2359
+ true
2360
+ );
2361
+ continue;
2362
+ }
2363
+ const sizeCheck = validateOperationSize(op, this.maxOperationBytes);
2364
+ if (!sizeCheck.valid) {
2365
+ this.sendError(
2366
+ "OPERATION_TOO_LARGE",
2367
+ sizeCheck.message ?? `Operation "${op.id}" is too large`,
2368
+ false
2369
+ );
2370
+ continue;
2371
+ }
2372
+ try {
2373
+ const applyResult = await applyServerOperation(this.store, op);
2374
+ if (applyResult.rejection) {
2375
+ this.sendError(applyResult.rejection.code, applyResult.rejection.message, false);
2376
+ continue;
2377
+ }
2378
+ if (applyResult.result === "applied") {
2379
+ applied.push(...applyResult.appliedOperations);
2380
+ }
2381
+ } catch {
2382
+ }
2383
+ }
2384
+ if (rejected.length > 0) {
2385
+ for (const op of rejected) {
2386
+ this.sendError(
2387
+ "SCOPE_VIOLATION",
2388
+ `Operation "${op.id}" in collection "${op.collection}" is outside the client's sync scope`,
2389
+ false
2390
+ );
1832
2391
  }
1833
2392
  }
1834
2393
  if (operations2.length > 0) {
@@ -1845,7 +2404,7 @@ var ClientSession = class {
1845
2404
  acknowledgedMessageId: msg.messageId,
1846
2405
  lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
1847
2406
  };
1848
- this.transport.send(ack);
2407
+ this.sendToClient(ack);
1849
2408
  if (applied.length > 0) {
1850
2409
  this.onRelay?.(this.sessionId, applied);
1851
2410
  }
@@ -1857,7 +2416,7 @@ var ClientSession = class {
1857
2416
  const clientSeq = clientVector.get(nodeId) ?? 0;
1858
2417
  if (serverSeq > clientSeq) {
1859
2418
  const ops = await this.store.getOperationRange(nodeId, clientSeq + 1, serverSeq);
1860
- const visible = ops.filter((op) => operationMatchesScopes(op, this.authContext?.scopes));
2419
+ const visible = ops.filter((op) => this.operationVisibleToClient(op));
1861
2420
  missing.push(...visible);
1862
2421
  }
1863
2422
  }
@@ -1867,25 +2426,42 @@ var ClientSession = class {
1867
2426
  messageId: generateUUIDv74(),
1868
2427
  operations: [],
1869
2428
  isFinal: true,
1870
- batchIndex: 0
2429
+ batchIndex: 0,
2430
+ totalBatches: 1
1871
2431
  };
1872
- this.transport.send(emptyBatch);
2432
+ this.sendToClient(emptyBatch);
1873
2433
  return;
1874
2434
  }
1875
2435
  const sorted = topologicalSort(missing);
1876
- const totalBatches = Math.ceil(sorted.length / this.batchSize);
2436
+ const afterCursor = sliceOperationsAfterCursor(sorted, this.resumeDeltaCursor);
2437
+ const totalBatches = Math.ceil(afterCursor.length / this.batchSize);
2438
+ if (afterCursor.length === 0) {
2439
+ const emptyBatch = {
2440
+ type: "operation-batch",
2441
+ messageId: generateUUIDv74(),
2442
+ operations: [],
2443
+ isFinal: true,
2444
+ batchIndex: this.resumeDeltaCursor?.batchIndex ?? 0,
2445
+ totalBatches: 1
2446
+ };
2447
+ this.sendToClient(emptyBatch);
2448
+ return;
2449
+ }
1877
2450
  for (let i = 0; i < totalBatches; i++) {
1878
2451
  const start = i * this.batchSize;
1879
- const batchOps = sorted.slice(start, start + this.batchSize);
2452
+ const batchOps = afterCursor.slice(start, start + this.batchSize);
1880
2453
  const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
2454
+ const batchCursor = createDeltaCursorFromBatch(batchOps, i);
1881
2455
  const batchMsg = {
1882
2456
  type: "operation-batch",
1883
2457
  messageId: generateUUIDv74(),
1884
2458
  operations: serializedOps,
1885
2459
  isFinal: i === totalBatches - 1,
1886
- batchIndex: i
2460
+ batchIndex: i,
2461
+ totalBatches,
2462
+ ...batchCursor ? { cursor: encodeDeltaCursor(batchCursor) } : {}
1887
2463
  };
1888
- this.transport.send(batchMsg);
2464
+ this.sendToClient(batchMsg);
1889
2465
  this.emitter?.emit({
1890
2466
  type: "sync:sent",
1891
2467
  operations: batchOps,
@@ -1893,6 +2469,18 @@ var ClientSession = class {
1893
2469
  });
1894
2470
  }
1895
2471
  }
2472
+ operationVisibleToClient(op) {
2473
+ if (!operationMatchesScopes(op, this.authContext?.scopes)) {
2474
+ return false;
2475
+ }
2476
+ return operationMatchesQuerySubsets(op, this.syncQuerySubsets);
2477
+ }
2478
+ handleAwarenessUpdate(msg) {
2479
+ this.onAwarenessUpdate?.(this.sessionId, msg);
2480
+ }
2481
+ handleYjsDocUpdate(msg) {
2482
+ this.onYjsDocUpdate?.(this.sessionId, msg);
2483
+ }
1896
2484
  sendError(code, message, retriable) {
1897
2485
  const errorMsg = {
1898
2486
  type: "error",
@@ -1901,7 +2489,7 @@ var ClientSession = class {
1901
2489
  message,
1902
2490
  retriable
1903
2491
  };
1904
- this.transport.send(errorMsg);
2492
+ this.sendToClient(errorMsg);
1905
2493
  }
1906
2494
  setSerializerWireFormat(format) {
1907
2495
  if (typeof this.serializer.setWireFormat === "function") {
@@ -1923,8 +2511,258 @@ function selectWireFormat(supportedWireFormats) {
1923
2511
  }
1924
2512
 
1925
2513
  // src/server/kora-sync-server.ts
1926
- import { SyncError as SyncError4, generateUUIDv7 as generateUUIDv75 } from "@korajs/core";
2514
+ import { SyncError as SyncError4, generateUUIDv7 as generateUUIDv76 } from "@korajs/core";
2515
+ import { SimpleEventEmitter } from "@korajs/core/internal";
1927
2516
  import { JsonMessageSerializer as JsonMessageSerializer2 } from "@korajs/sync";
2517
+
2518
+ // src/awareness/awareness-relay.ts
2519
+ import { generateUUIDv7 as generateUUIDv75 } from "@korajs/core";
2520
+ var AwarenessRelay = class {
2521
+ clients = /* @__PURE__ */ new Map();
2522
+ /**
2523
+ * Register a client for awareness broadcasting.
2524
+ *
2525
+ * @param sessionId - Unique session identifier
2526
+ * @param clientId - Client-assigned awareness ID
2527
+ * @param transport - Transport for sending messages to this client
2528
+ */
2529
+ addClient(sessionId, clientId, transport) {
2530
+ this.clients.set(sessionId, {
2531
+ sessionId,
2532
+ clientId,
2533
+ transport,
2534
+ state: null
2535
+ });
2536
+ const existingStates = {};
2537
+ let hasStates = false;
2538
+ for (const [, client] of this.clients) {
2539
+ if (client.sessionId === sessionId) continue;
2540
+ if (client.state) {
2541
+ existingStates[String(client.clientId)] = client.state;
2542
+ hasStates = true;
2543
+ }
2544
+ }
2545
+ if (hasStates) {
2546
+ const catchUpMsg = {
2547
+ type: "awareness-update",
2548
+ messageId: generateUUIDv75(),
2549
+ clientId: 0,
2550
+ // Server-sourced
2551
+ states: existingStates
2552
+ };
2553
+ transport.send(catchUpMsg);
2554
+ }
2555
+ }
2556
+ /**
2557
+ * Remove a client and broadcast its removal to all remaining clients.
2558
+ *
2559
+ * @param sessionId - Session ID of the disconnecting client
2560
+ */
2561
+ removeClient(sessionId) {
2562
+ const client = this.clients.get(sessionId);
2563
+ if (!client) return;
2564
+ this.clients.delete(sessionId);
2565
+ if (client.state === null) return;
2566
+ const removalStates = {
2567
+ [String(client.clientId)]: null
2568
+ };
2569
+ const msg = {
2570
+ type: "awareness-update",
2571
+ messageId: generateUUIDv75(),
2572
+ clientId: client.clientId,
2573
+ states: removalStates
2574
+ };
2575
+ this.broadcastExcept(sessionId, msg);
2576
+ }
2577
+ /**
2578
+ * Handle an incoming awareness update from a client.
2579
+ * Stores the state and relays to all other connected clients.
2580
+ *
2581
+ * @param sessionId - Session ID of the sending client
2582
+ * @param message - The awareness update message
2583
+ */
2584
+ handleUpdate(sessionId, message) {
2585
+ const sender = this.clients.get(sessionId);
2586
+ if (!sender) return;
2587
+ const senderState = message.states[String(message.clientId)];
2588
+ if (senderState !== void 0) {
2589
+ sender.state = senderState;
2590
+ }
2591
+ this.broadcastExcept(sessionId, message);
2592
+ }
2593
+ /**
2594
+ * Get the number of registered awareness clients.
2595
+ */
2596
+ getClientCount() {
2597
+ return this.clients.size;
2598
+ }
2599
+ /**
2600
+ * Remove all clients and clear all state.
2601
+ */
2602
+ clear() {
2603
+ this.clients.clear();
2604
+ }
2605
+ // --- Private ---
2606
+ broadcastExcept(excludeSessionId, message) {
2607
+ for (const [, client] of this.clients) {
2608
+ if (client.sessionId === excludeSessionId) continue;
2609
+ if (!client.transport.isConnected()) continue;
2610
+ client.transport.send(message);
2611
+ }
2612
+ }
2613
+ };
2614
+
2615
+ // src/diagnostics/server-metrics-collector.ts
2616
+ function estimateByteSize(operations2) {
2617
+ let total = 0;
2618
+ for (const op of operations2) {
2619
+ total += JSON.stringify(op).length;
2620
+ }
2621
+ return total;
2622
+ }
2623
+ var ServerMetricsCollector = class {
2624
+ startedAt = Date.now();
2625
+ peakConnections = 0;
2626
+ connectionsTotal = 0;
2627
+ operationsReceived = 0;
2628
+ operationsSent = 0;
2629
+ bytesReceived = 0;
2630
+ bytesSent = 0;
2631
+ errorCount = 0;
2632
+ schemaVersion = 1;
2633
+ clientMetrics = /* @__PURE__ */ new Map();
2634
+ /** Record a new client connection. */
2635
+ recordConnection(sessionId) {
2636
+ this.connectionsTotal++;
2637
+ this.clientMetrics.set(sessionId, {
2638
+ sessionId,
2639
+ nodeId: null,
2640
+ state: "connected",
2641
+ connectedAt: Date.now(),
2642
+ operationsReceived: 0,
2643
+ operationsSent: 0,
2644
+ authContext: null
2645
+ });
2646
+ this.peakConnections = Math.max(this.peakConnections, this.clientMetrics.size);
2647
+ }
2648
+ /** Record a client disconnection. */
2649
+ recordDisconnection(sessionId) {
2650
+ this.clientMetrics.delete(sessionId);
2651
+ }
2652
+ /** Update the node ID after a handshake completes. */
2653
+ recordHandshake(sessionId, nodeId) {
2654
+ const client = this.clientMetrics.get(sessionId);
2655
+ if (client) {
2656
+ client.nodeId = nodeId;
2657
+ }
2658
+ }
2659
+ /** Update session state. */
2660
+ updateSessionState(sessionId, state) {
2661
+ const client = this.clientMetrics.get(sessionId);
2662
+ if (client) {
2663
+ client.state = state;
2664
+ }
2665
+ }
2666
+ /** Record authentication context for a session. */
2667
+ recordAuth(sessionId, authContext) {
2668
+ const client = this.clientMetrics.get(sessionId);
2669
+ if (client) {
2670
+ client.authContext = authContext;
2671
+ }
2672
+ }
2673
+ /** Record operations received from a client. */
2674
+ recordReceived(sessionId, count3, byteSize) {
2675
+ this.operationsReceived += count3;
2676
+ this.bytesReceived += byteSize;
2677
+ const client = this.clientMetrics.get(sessionId);
2678
+ if (client) {
2679
+ client.operationsReceived += count3;
2680
+ }
2681
+ }
2682
+ /** Record operations sent to a client. */
2683
+ recordSent(sessionId, count3, byteSize) {
2684
+ this.operationsSent += count3;
2685
+ this.bytesSent += byteSize;
2686
+ const client = this.clientMetrics.get(sessionId);
2687
+ if (client) {
2688
+ client.operationsSent += count3;
2689
+ }
2690
+ }
2691
+ /** Record an error. */
2692
+ recordError() {
2693
+ this.errorCount++;
2694
+ }
2695
+ /** Set the schema version. */
2696
+ setSchemaVersion(version) {
2697
+ this.schemaVersion = version;
2698
+ }
2699
+ /** Return a full snapshot of current metrics. */
2700
+ getSnapshot(totalOperations) {
2701
+ return {
2702
+ connectedClients: this.clientMetrics.size,
2703
+ connectedNodeIds: Array.from(this.clientMetrics.values()).filter((c) => c.nodeId !== null).map((c) => c.nodeId),
2704
+ peakConnections: this.peakConnections,
2705
+ connectionsTotal: this.connectionsTotal,
2706
+ operationsReceived: this.operationsReceived,
2707
+ operationsSent: this.operationsSent,
2708
+ bytesReceived: this.bytesReceived,
2709
+ bytesSent: this.bytesSent,
2710
+ clients: Array.from(this.clientMetrics.values()),
2711
+ uptime: Date.now() - this.startedAt,
2712
+ totalOperations,
2713
+ errorCount: this.errorCount,
2714
+ schemaVersion: this.schemaVersion
2715
+ };
2716
+ }
2717
+ /** Reset all metrics. */
2718
+ reset() {
2719
+ this.startedAt = Date.now();
2720
+ this.peakConnections = 0;
2721
+ this.connectionsTotal = 0;
2722
+ this.operationsReceived = 0;
2723
+ this.operationsSent = 0;
2724
+ this.bytesReceived = 0;
2725
+ this.bytesSent = 0;
2726
+ this.errorCount = 0;
2727
+ this.clientMetrics.clear();
2728
+ }
2729
+ };
2730
+
2731
+ // src/richtext/yjs-doc-relay.ts
2732
+ var YjsDocRelay = class {
2733
+ clients = /* @__PURE__ */ new Map();
2734
+ addClient(sessionId, transport) {
2735
+ this.clients.set(sessionId, { sessionId, transport });
2736
+ }
2737
+ removeClient(sessionId) {
2738
+ this.clients.delete(sessionId);
2739
+ }
2740
+ handleUpdate(sourceSessionId, message) {
2741
+ if (!this.clients.has(sourceSessionId)) {
2742
+ return;
2743
+ }
2744
+ this.broadcastExcept(sourceSessionId, message);
2745
+ }
2746
+ getClientCount() {
2747
+ return this.clients.size;
2748
+ }
2749
+ clear() {
2750
+ this.clients.clear();
2751
+ }
2752
+ broadcastExcept(excludeSessionId, message) {
2753
+ for (const [, client] of this.clients) {
2754
+ if (client.sessionId === excludeSessionId) {
2755
+ continue;
2756
+ }
2757
+ if (!client.transport.isConnected()) {
2758
+ continue;
2759
+ }
2760
+ client.transport.send(message);
2761
+ }
2762
+ }
2763
+ };
2764
+
2765
+ // src/server/kora-sync-server.ts
1928
2766
  var DEFAULT_MAX_CONNECTIONS = 0;
1929
2767
  var DEFAULT_BATCH_SIZE2 = 100;
1930
2768
  var DEFAULT_SCHEMA_VERSION2 = 1;
@@ -1938,12 +2776,18 @@ var KoraSyncServer = class {
1938
2776
  maxConnections;
1939
2777
  batchSize;
1940
2778
  schemaVersion;
2779
+ supportedSchemaVersions;
1941
2780
  port;
1942
2781
  host;
1943
2782
  path;
2783
+ logger;
2784
+ metrics;
2785
+ awarenessRelay = new AwarenessRelay();
2786
+ yjsDocRelay = new YjsDocRelay();
1944
2787
  sessions = /* @__PURE__ */ new Map();
1945
2788
  httpClients = /* @__PURE__ */ new Map();
1946
2789
  httpSessionToClient = /* @__PURE__ */ new Map();
2790
+ serverVersion = "0.4.0";
1947
2791
  wsServer = null;
1948
2792
  running = false;
1949
2793
  constructor(config) {
@@ -1954,9 +2798,80 @@ var KoraSyncServer = class {
1954
2798
  this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
1955
2799
  this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
1956
2800
  this.schemaVersion = config.schemaVersion ?? DEFAULT_SCHEMA_VERSION2;
2801
+ this.supportedSchemaVersions = config.supportedSchemaVersions ?? {
2802
+ min: this.schemaVersion,
2803
+ max: this.schemaVersion
2804
+ };
1957
2805
  this.port = config.port;
1958
2806
  this.host = config.host ?? DEFAULT_HOST;
1959
2807
  this.path = config.path ?? DEFAULT_PATH;
2808
+ this.logger = config.logger ?? createDefaultLogger();
2809
+ this.metrics = config.metricsCollector ?? new ServerMetricsCollector();
2810
+ this.metrics.setSchemaVersion(this.schemaVersion);
2811
+ if (!this.emitter) {
2812
+ this.emitter = new SimpleEventEmitter();
2813
+ }
2814
+ }
2815
+ /**
2816
+ * Subscribe to session-level events for metrics collection and logging.
2817
+ * Called when a new session is created.
2818
+ */
2819
+ attachSessionEvents(sessionId, sessionEmitter) {
2820
+ sessionEmitter.on("sync:connected", (event) => {
2821
+ this.metrics.recordHandshake(sessionId, event.nodeId);
2822
+ this.logger.log({
2823
+ timestamp: Date.now(),
2824
+ level: "info",
2825
+ event: "session.handshake",
2826
+ sessionId,
2827
+ nodeId: event.nodeId
2828
+ });
2829
+ });
2830
+ sessionEmitter.on("sync:received", (event) => {
2831
+ const byteSize = estimateByteSize(event.operations);
2832
+ this.metrics.recordReceived(sessionId, event.batchSize, byteSize);
2833
+ this.logger.log({
2834
+ timestamp: Date.now(),
2835
+ level: "info",
2836
+ event: "operations.received",
2837
+ sessionId,
2838
+ count: event.batchSize,
2839
+ bytes: byteSize
2840
+ });
2841
+ });
2842
+ sessionEmitter.on("sync:sent", (event) => {
2843
+ const byteSize = estimateByteSize(event.operations);
2844
+ this.metrics.recordSent(sessionId, event.batchSize, byteSize);
2845
+ this.logger.log({
2846
+ timestamp: Date.now(),
2847
+ level: "info",
2848
+ event: "operations.sent",
2849
+ sessionId,
2850
+ count: event.batchSize,
2851
+ bytes: byteSize
2852
+ });
2853
+ });
2854
+ sessionEmitter.on("sync:disconnected", (event) => {
2855
+ this.logger.log({
2856
+ timestamp: Date.now(),
2857
+ level: "info",
2858
+ event: "session.disconnected",
2859
+ sessionId,
2860
+ details: { reason: event.reason }
2861
+ });
2862
+ });
2863
+ }
2864
+ /**
2865
+ * Get the metrics collector for external access (e.g., HTTP endpoints).
2866
+ */
2867
+ getMetricsCollector() {
2868
+ return this.metrics;
2869
+ }
2870
+ /**
2871
+ * Get the logger for external access (e.g., event streaming).
2872
+ */
2873
+ getLogger() {
2874
+ return this.logger;
1960
2875
  }
1961
2876
  /**
1962
2877
  * Start the WebSocket server in standalone mode.
@@ -1997,11 +2912,25 @@ var KoraSyncServer = class {
1997
2912
  this.handleConnection(transport);
1998
2913
  });
1999
2914
  this.running = true;
2915
+ this.logger.log({
2916
+ timestamp: Date.now(),
2917
+ level: "info",
2918
+ event: "server.started",
2919
+ details: { port: this.port, host: this.host, path: this.path }
2920
+ });
2000
2921
  }
2001
2922
  /**
2002
2923
  * Stop the server. Closes all sessions and the WebSocket server.
2003
2924
  */
2004
2925
  async stop() {
2926
+ this.logger.log({
2927
+ timestamp: Date.now(),
2928
+ level: "info",
2929
+ event: "server.stopping",
2930
+ details: { connectedClients: this.sessions.size }
2931
+ });
2932
+ this.awarenessRelay.clear();
2933
+ this.yjsDocRelay.clear();
2005
2934
  for (const session of this.sessions.values()) {
2006
2935
  session.close("server shutting down");
2007
2936
  }
@@ -2015,6 +2944,11 @@ var KoraSyncServer = class {
2015
2944
  this.wsServer = null;
2016
2945
  }
2017
2946
  this.running = false;
2947
+ this.logger.log({
2948
+ timestamp: Date.now(),
2949
+ level: "info",
2950
+ event: "server.stopped"
2951
+ });
2018
2952
  }
2019
2953
  /**
2020
2954
  * Handle one HTTP sync request for a long-polling client.
@@ -2058,47 +2992,125 @@ var KoraSyncServer = class {
2058
2992
  if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
2059
2993
  transport.send({
2060
2994
  type: "error",
2061
- messageId: generateUUIDv75(),
2995
+ messageId: generateUUIDv76(),
2062
2996
  code: "MAX_CONNECTIONS",
2063
2997
  message: `Server has reached maximum connections (${this.maxConnections})`,
2064
2998
  retriable: true
2065
2999
  });
2066
3000
  transport.close(4029, "max connections reached");
3001
+ this.metrics.recordError();
3002
+ this.logger.log({
3003
+ timestamp: Date.now(),
3004
+ level: "warn",
3005
+ event: "connection.rejected",
3006
+ details: { reason: "max_connections", max: this.maxConnections }
3007
+ });
2067
3008
  throw new SyncError4("Maximum connections reached", {
2068
3009
  current: this.sessions.size,
2069
3010
  max: this.maxConnections
2070
3011
  });
2071
3012
  }
2072
- const sessionId = generateUUIDv75();
3013
+ const sessionId = generateUUIDv76();
3014
+ this.metrics.recordConnection(sessionId);
3015
+ const sessionEmitter = new SimpleEventEmitter();
3016
+ sessionEmitter.on("sync:connected", (event) => {
3017
+ this.metrics.recordHandshake(sessionId, event.nodeId);
3018
+ this.metrics.updateSessionState(sessionId, "authenticated");
3019
+ this.logger.log({
3020
+ timestamp: Date.now(),
3021
+ level: "info",
3022
+ event: "session.handshake",
3023
+ sessionId,
3024
+ nodeId: event.nodeId
3025
+ });
3026
+ });
3027
+ sessionEmitter.on("sync:received", (event) => {
3028
+ const byteSize = estimateOperationByteSize(event.operations);
3029
+ this.metrics.recordReceived(sessionId, event.batchSize, byteSize);
3030
+ this.logger.log({
3031
+ timestamp: Date.now(),
3032
+ level: "info",
3033
+ event: "operations.received",
3034
+ sessionId,
3035
+ count: event.batchSize,
3036
+ bytes: byteSize
3037
+ });
3038
+ });
3039
+ sessionEmitter.on("sync:sent", (event) => {
3040
+ const byteSize = estimateOperationByteSize(event.operations);
3041
+ this.metrics.recordSent(sessionId, event.batchSize, byteSize);
3042
+ this.logger.log({
3043
+ timestamp: Date.now(),
3044
+ level: "info",
3045
+ event: "operations.sent",
3046
+ sessionId,
3047
+ count: event.batchSize,
3048
+ bytes: byteSize
3049
+ });
3050
+ });
3051
+ sessionEmitter.on("sync:disconnected", () => {
3052
+ this.logger.log({
3053
+ timestamp: Date.now(),
3054
+ level: "info",
3055
+ event: "session.disconnected",
3056
+ sessionId
3057
+ });
3058
+ });
2073
3059
  const session = new ClientSession({
2074
3060
  sessionId,
2075
3061
  transport,
2076
3062
  store: this.store,
2077
3063
  auth: this.auth ?? void 0,
2078
3064
  serializer: this.serializer,
2079
- emitter: this.emitter ?? void 0,
3065
+ emitter: sessionEmitter,
2080
3066
  batchSize: this.batchSize,
2081
3067
  schemaVersion: this.schemaVersion,
3068
+ supportedSchemaVersions: this.supportedSchemaVersions,
2082
3069
  onRelay: (sourceSessionId, operations2) => {
2083
3070
  this.handleRelay(sourceSessionId, operations2);
2084
3071
  },
3072
+ onAwarenessUpdate: (sourceSessionId, message) => {
3073
+ this.handleAwarenessRelay(sourceSessionId, message);
3074
+ },
3075
+ onYjsDocUpdate: (sourceSessionId, message) => {
3076
+ this.handleYjsDocRelay(sourceSessionId, message);
3077
+ },
2085
3078
  onClose: (sid) => {
2086
3079
  this.handleSessionClose(sid);
2087
3080
  }
2088
3081
  });
2089
3082
  this.sessions.set(sessionId, session);
3083
+ this.yjsDocRelay.addClient(sessionId, transport);
2090
3084
  session.start();
3085
+ this.logger.log({
3086
+ timestamp: Date.now(),
3087
+ level: "info",
3088
+ event: "session.connected",
3089
+ sessionId,
3090
+ details: { totalSessions: this.sessions.size }
3091
+ });
2091
3092
  return sessionId;
2092
3093
  }
2093
3094
  /**
2094
3095
  * Get the current server status.
2095
3096
  */
2096
3097
  async getStatus() {
3098
+ const totalOps = await this.store.getOperationCount();
3099
+ const snapshot = this.metrics.getSnapshot(totalOps);
2097
3100
  return {
2098
3101
  running: this.running,
2099
- connectedClients: this.sessions.size,
3102
+ connectedClients: snapshot.connectedClients,
2100
3103
  port: this.port ?? null,
2101
- totalOperations: await this.store.getOperationCount()
3104
+ totalOperations: snapshot.totalOperations,
3105
+ uptime: snapshot.uptime,
3106
+ version: this.serverVersion,
3107
+ schemaVersion: this.schemaVersion,
3108
+ connectedNodeIds: snapshot.connectedNodeIds,
3109
+ peakConnections: snapshot.peakConnections,
3110
+ connectionsTotal: snapshot.connectionsTotal,
3111
+ operationsReceived: snapshot.operationsReceived,
3112
+ operationsSent: snapshot.operationsSent,
3113
+ errorCount: snapshot.errorCount
2102
3114
  };
2103
3115
  }
2104
3116
  /**
@@ -2109,12 +3121,31 @@ var KoraSyncServer = class {
2109
3121
  }
2110
3122
  // --- Private ---
2111
3123
  handleRelay(sourceSessionId, operations2) {
3124
+ const targetCount = this.sessions.size - 1;
3125
+ const byteSize = estimateOperationByteSize(operations2);
3126
+ this.metrics.recordSent(
3127
+ sourceSessionId,
3128
+ operations2.length * targetCount,
3129
+ byteSize * targetCount
3130
+ );
3131
+ this.logger.log({
3132
+ timestamp: Date.now(),
3133
+ level: "info",
3134
+ event: "operations.relayed",
3135
+ sessionId: sourceSessionId,
3136
+ count: operations2.length,
3137
+ bytes: byteSize * targetCount,
3138
+ details: { targetSessions: targetCount }
3139
+ });
2112
3140
  for (const [sessionId, session] of this.sessions) {
2113
3141
  if (sessionId === sourceSessionId) continue;
2114
3142
  session.relayOperations(operations2);
2115
3143
  }
2116
3144
  }
2117
3145
  handleSessionClose(sessionId) {
3146
+ this.metrics.recordDisconnection(sessionId);
3147
+ this.awarenessRelay.removeClient(sessionId);
3148
+ this.yjsDocRelay.removeClient(sessionId);
2118
3149
  this.sessions.delete(sessionId);
2119
3150
  const clientId = this.httpSessionToClient.get(sessionId);
2120
3151
  if (clientId) {
@@ -2122,6 +3153,21 @@ var KoraSyncServer = class {
2122
3153
  this.httpClients.delete(clientId);
2123
3154
  }
2124
3155
  }
3156
+ handleAwarenessRelay(sourceSessionId, message) {
3157
+ const session = this.sessions.get(sourceSessionId);
3158
+ if (!session) return;
3159
+ const transport = session.getTransport();
3160
+ if (!this.awarenessRelay.getClientCount() || !transport) {
3161
+ }
3162
+ this.awarenessRelay.addClient(sourceSessionId, message.clientId, transport);
3163
+ this.awarenessRelay.handleUpdate(sourceSessionId, message);
3164
+ }
3165
+ handleYjsDocRelay(sourceSessionId, message) {
3166
+ if (!this.sessions.has(sourceSessionId)) {
3167
+ return;
3168
+ }
3169
+ this.yjsDocRelay.handleUpdate(sourceSessionId, message);
3170
+ }
2125
3171
  getOrCreateHttpClient(clientId) {
2126
3172
  const existing = this.httpClients.get(clientId);
2127
3173
  if (existing) {
@@ -2135,6 +3181,13 @@ var KoraSyncServer = class {
2135
3181
  return client;
2136
3182
  }
2137
3183
  };
3184
+ function estimateOperationByteSize(operations2) {
3185
+ let total = 0;
3186
+ for (const op of operations2) {
3187
+ total += JSON.stringify(op).length;
3188
+ }
3189
+ return total;
3190
+ }
2138
3191
  function normalizeHttpBody(body, contentType) {
2139
3192
  if (body instanceof Uint8Array) {
2140
3193
  return body;
@@ -2176,7 +3229,7 @@ var KoraAuthProvider = class {
2176
3229
  this.resolveScopes = options.resolveScopes;
2177
3230
  }
2178
3231
  async authenticate(token) {
2179
- const payload = this.tokenValidator.validateToken(token);
3232
+ const payload = this.tokenValidator.validateTokenWithRevocation ? await this.tokenValidator.validateTokenWithRevocation(token) : this.tokenValidator.validateToken(token);
2180
3233
  if (payload === null) {
2181
3234
  return null;
2182
3235
  }
@@ -2203,6 +3256,30 @@ var KoraAuthProvider = class {
2203
3256
  }
2204
3257
  };
2205
3258
 
3259
+ // src/auth/mixed-auth-provider.ts
3260
+ var MixedAuthProvider = class {
3261
+ primary;
3262
+ anonymousScopes;
3263
+ anonymousPrefix;
3264
+ anonymousCounter = 0;
3265
+ constructor(options) {
3266
+ this.primary = options.primary;
3267
+ this.anonymousScopes = options.anonymousScopes;
3268
+ this.anonymousPrefix = options.anonymousPrefix ?? "anon";
3269
+ }
3270
+ async authenticate(token) {
3271
+ if (token) {
3272
+ const ctx = await this.primary.authenticate(token);
3273
+ if (ctx) return ctx;
3274
+ }
3275
+ this.anonymousCounter++;
3276
+ return {
3277
+ userId: `${this.anonymousPrefix}-${Date.now()}-${this.anonymousCounter}`,
3278
+ scopes: this.anonymousScopes
3279
+ };
3280
+ }
3281
+ };
3282
+
2206
3283
  // src/server/create-server.ts
2207
3284
  function createKoraServer(config) {
2208
3285
  return new KoraSyncServer(config);
@@ -2229,9 +3306,143 @@ function createProductionServer(config) {
2229
3306
  const syncPath = config.syncPath ?? "/kora-sync";
2230
3307
  const syncServer = new KoraSyncServer({
2231
3308
  store: config.store,
3309
+ enableDashboard: true,
2232
3310
  ...config.syncOptions
2233
3311
  });
2234
3312
  let httpServer = null;
3313
+ function getOperationalToken(kind) {
3314
+ const auth = config.operationalAuth;
3315
+ if (!auth) return void 0;
3316
+ if (kind === "metrics") return auth.metricsToken || auth.adminToken;
3317
+ if (kind === "backup") return auth.backupToken || auth.adminToken;
3318
+ return auth.adminToken;
3319
+ }
3320
+ function extractRequestToken(req) {
3321
+ const authorization = req.headers.authorization;
3322
+ if (authorization?.startsWith("Bearer ")) {
3323
+ return authorization.slice("Bearer ".length).trim();
3324
+ }
3325
+ const headerNames = ["x-kora-admin-token", "x-kora-metrics-token", "x-kora-backup-token"];
3326
+ for (const name of headerNames) {
3327
+ const value = req.headers[name];
3328
+ if (typeof value === "string" && value.length > 0) return value;
3329
+ if (Array.isArray(value) && typeof value[0] === "string" && value[0].length > 0) {
3330
+ return value[0];
3331
+ }
3332
+ }
3333
+ return null;
3334
+ }
3335
+ function isOperationalRequestAllowed(req, kind) {
3336
+ const expected = getOperationalToken(kind);
3337
+ if (!expected) return true;
3338
+ return extractRequestToken(req) === expected;
3339
+ }
3340
+ function rejectUnauthorized(res) {
3341
+ res.writeHead(401, {
3342
+ "Content-Type": "application/json",
3343
+ "WWW-Authenticate": 'Bearer realm="kora"'
3344
+ });
3345
+ res.end(JSON.stringify({ error: "Unauthorized" }));
3346
+ }
3347
+ function formatPrometheusMetrics() {
3348
+ const status = syncServer.getMetricsCollector().getSnapshot(0);
3349
+ const lines = [
3350
+ "# HELP kora_connected_clients Current number of connected clients",
3351
+ "# TYPE kora_connected_clients gauge",
3352
+ `kora_connected_clients ${status.connectedClients}`,
3353
+ "",
3354
+ "# HELP kora_peak_connections Peak number of simultaneous connections since server start",
3355
+ "# TYPE kora_peak_connections gauge",
3356
+ `kora_peak_connections ${status.peakConnections}`,
3357
+ "",
3358
+ "# HELP kora_connections_total Total number of connections handled since server start",
3359
+ "# TYPE kora_connections_total counter",
3360
+ `kora_connections_total ${status.connectionsTotal}`,
3361
+ "",
3362
+ "# HELP kora_operations_received_total Total operations received from clients",
3363
+ "# TYPE kora_operations_received_total counter",
3364
+ `kora_operations_received_total ${status.operationsReceived}`,
3365
+ "",
3366
+ "# HELP kora_operations_sent_total Total operations sent to clients",
3367
+ "# TYPE kora_operations_sent_total counter",
3368
+ `kora_operations_sent_total ${status.operationsSent}`,
3369
+ "",
3370
+ "# HELP kora_bytes_received_total Total bytes received from clients",
3371
+ "# TYPE kora_bytes_received_total counter",
3372
+ `kora_bytes_received_total ${status.bytesReceived}`,
3373
+ "",
3374
+ "# HELP kora_bytes_sent_total Total bytes sent to clients",
3375
+ "# TYPE kora_bytes_sent_total counter",
3376
+ `kora_bytes_sent_total ${status.bytesSent}`,
3377
+ "",
3378
+ "# HELP kora_errors_total Total errors since server start",
3379
+ "# TYPE kora_errors_total counter",
3380
+ `kora_errors_total ${status.errorCount}`,
3381
+ "",
3382
+ "# HELP kora_uptime_seconds Server uptime in seconds",
3383
+ "# TYPE kora_uptime_seconds gauge",
3384
+ `kora_uptime_seconds ${Math.floor(status.uptime / 1e3)}`,
3385
+ "",
3386
+ "# HELP kora_schema_version Schema version the server expects",
3387
+ "# TYPE kora_schema_version gauge",
3388
+ `kora_schema_version ${status.schemaVersion}`,
3389
+ ""
3390
+ ];
3391
+ return lines.join("\n");
3392
+ }
3393
+ function readBodyBuffer(req) {
3394
+ return new Promise((resolve) => {
3395
+ const chunks = [];
3396
+ req.on("data", (chunk) => chunks.push(chunk));
3397
+ req.on("end", () => resolve(Buffer.concat(chunks)));
3398
+ });
3399
+ }
3400
+ async function readJsonBody(req) {
3401
+ const buffer = await readBodyBuffer(req);
3402
+ if (buffer.byteLength === 0) return void 0;
3403
+ try {
3404
+ return JSON.parse(buffer.toString("utf8"));
3405
+ } catch {
3406
+ return void 0;
3407
+ }
3408
+ }
3409
+ function matchesRoutePrefix(pathname, prefix) {
3410
+ const normalizedPrefix = normalizeRoutePath(prefix);
3411
+ return pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`);
3412
+ }
3413
+ function normalizeRoutePath(path) {
3414
+ const prefixed = path.startsWith("/") ? path : `/${path}`;
3415
+ return prefixed.length > 1 ? prefixed.replace(/\/+$/, "") : prefixed;
3416
+ }
3417
+ function getClientIp(req) {
3418
+ const forwarded = req.headers["x-forwarded-for"];
3419
+ if (typeof forwarded === "string" && forwarded.length > 0) {
3420
+ return forwarded.split(",")[0]?.trim();
3421
+ }
3422
+ return req.socket.remoteAddress;
3423
+ }
3424
+ function getQuery(url) {
3425
+ const query = {};
3426
+ for (const [key, value] of url.searchParams) {
3427
+ const existing = query[key];
3428
+ if (existing === void 0) {
3429
+ query[key] = value;
3430
+ } else if (Array.isArray(existing)) {
3431
+ existing.push(value);
3432
+ } else {
3433
+ query[key] = [existing, value];
3434
+ }
3435
+ }
3436
+ return query;
3437
+ }
3438
+ function writeJsonResponse(res, result) {
3439
+ const headers = {
3440
+ "Content-Type": "application/json",
3441
+ ...result.headers ?? {}
3442
+ };
3443
+ res.writeHead(result.status, headers);
3444
+ res.end(JSON.stringify(result.body ?? null));
3445
+ }
2235
3446
  return {
2236
3447
  async start() {
2237
3448
  const { createServer } = await import("http");
@@ -2239,13 +3450,137 @@ function createProductionServer(config) {
2239
3450
  const { extname, join, resolve } = await import("path");
2240
3451
  const { WebSocketServer } = await import("ws");
2241
3452
  const distDir = resolve(staticDir);
2242
- httpServer = createServer((req, res) => {
3453
+ httpServer = createServer(async (req, res) => {
2243
3454
  res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
2244
3455
  res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
2245
3456
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
2246
3457
  if (url.pathname === "/health") {
3458
+ const status = await syncServer.getStatus();
2247
3459
  res.writeHead(200, { "Content-Type": "application/json" });
2248
- res.end(JSON.stringify({ status: "ok", timestamp: Date.now() }));
3460
+ res.end(
3461
+ JSON.stringify({
3462
+ status: "ok",
3463
+ version: status.version,
3464
+ uptime: status.uptime,
3465
+ connectedClients: status.connectedClients,
3466
+ totalOperations: status.totalOperations,
3467
+ timestamp: Date.now()
3468
+ })
3469
+ );
3470
+ return;
3471
+ }
3472
+ if (url.pathname === "/__kora/status") {
3473
+ if (!isOperationalRequestAllowed(req, "admin")) {
3474
+ rejectUnauthorized(res);
3475
+ return;
3476
+ }
3477
+ const status = await syncServer.getStatus();
3478
+ res.writeHead(200, { "Content-Type": "application/json" });
3479
+ res.end(JSON.stringify(status, null, 2));
3480
+ return;
3481
+ }
3482
+ if (url.pathname === "/__kora/metrics") {
3483
+ if (!isOperationalRequestAllowed(req, "metrics")) {
3484
+ rejectUnauthorized(res);
3485
+ return;
3486
+ }
3487
+ res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" });
3488
+ res.end(formatPrometheusMetrics());
3489
+ return;
3490
+ }
3491
+ if (url.pathname === "/__kora/events") {
3492
+ if (!isOperationalRequestAllowed(req, "admin")) {
3493
+ rejectUnauthorized(res);
3494
+ return;
3495
+ }
3496
+ res.writeHead(200, {
3497
+ "Content-Type": "text/event-stream",
3498
+ "Cache-Control": "no-cache",
3499
+ Connection: "keep-alive",
3500
+ "X-Accel-Buffering": "no"
3501
+ });
3502
+ const status = await syncServer.getStatus();
3503
+ res.write(`event: status
3504
+ data: ${JSON.stringify(status)}
3505
+
3506
+ `);
3507
+ const interval = setInterval(async () => {
3508
+ try {
3509
+ const s = await syncServer.getStatus();
3510
+ res.write(`event: status
3511
+ data: ${JSON.stringify(s)}
3512
+
3513
+ `);
3514
+ } catch {
3515
+ }
3516
+ }, 2e3);
3517
+ req.on("close", () => {
3518
+ clearInterval(interval);
3519
+ });
3520
+ return;
3521
+ }
3522
+ if (url.pathname === "/__kora" || url.pathname === "/__kora/") {
3523
+ if (!isOperationalRequestAllowed(req, "admin")) {
3524
+ rejectUnauthorized(res);
3525
+ return;
3526
+ }
3527
+ const status = await syncServer.getStatus();
3528
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3529
+ res.end(renderDashboardHtml(status));
3530
+ return;
3531
+ }
3532
+ if (url.pathname === "/__kora/backup/export" && req.method === "POST") {
3533
+ if (!isOperationalRequestAllowed(req, "backup")) {
3534
+ rejectUnauthorized(res);
3535
+ return;
3536
+ }
3537
+ try {
3538
+ const backup = await config.store.exportBackup();
3539
+ res.writeHead(200, {
3540
+ "Content-Type": "application/octet-stream",
3541
+ "Content-Disposition": `attachment; filename="kora-backup-${Date.now()}.kora"`,
3542
+ "Content-Length": String(backup.byteLength)
3543
+ });
3544
+ res.end(Buffer.from(backup));
3545
+ } catch (error) {
3546
+ res.writeHead(500, { "Content-Type": "application/json" });
3547
+ res.end(JSON.stringify({ error: "Backup failed", message: error.message }));
3548
+ }
3549
+ return;
3550
+ }
3551
+ if (url.pathname === "/__kora/backup/import" && req.method === "POST") {
3552
+ if (!isOperationalRequestAllowed(req, "backup")) {
3553
+ rejectUnauthorized(res);
3554
+ return;
3555
+ }
3556
+ try {
3557
+ const body = await readBodyBuffer(req);
3558
+ const merge = url.searchParams.get("merge") === "true";
3559
+ const result = await config.store.importBackup(
3560
+ new Uint8Array(body.buffer, body.byteOffset, body.byteLength),
3561
+ merge
3562
+ );
3563
+ res.writeHead(result.success ? 200 : 400, { "Content-Type": "application/json" });
3564
+ res.end(JSON.stringify(result));
3565
+ } catch (error) {
3566
+ res.writeHead(500, { "Content-Type": "application/json" });
3567
+ res.end(JSON.stringify({ error: "Restore failed", message: error.message }));
3568
+ }
3569
+ return;
3570
+ }
3571
+ const customRoute = config.httpRoutes?.find(
3572
+ (route) => matchesRoutePrefix(url.pathname, route.path)
3573
+ );
3574
+ if (customRoute) {
3575
+ const result = await customRoute.handle({
3576
+ method: req.method ?? "GET",
3577
+ path: url.pathname,
3578
+ body: await readJsonBody(req),
3579
+ headers: req.headers,
3580
+ query: getQuery(url),
3581
+ ip: getClientIp(req)
3582
+ });
3583
+ writeJsonResponse(res, result);
2249
3584
  return;
2250
3585
  }
2251
3586
  let filePath = join(distDir, url.pathname);
@@ -2291,7 +3626,7 @@ function createProductionServer(config) {
2291
3626
  }
2292
3627
  });
2293
3628
  return new Promise((resolve2) => {
2294
- httpServer.listen(port, "0.0.0.0", () => {
3629
+ httpServer?.listen(port, "0.0.0.0", () => {
2295
3630
  resolve2(`http://localhost:${port}`);
2296
3631
  });
2297
3632
  });
@@ -2300,27 +3635,109 @@ function createProductionServer(config) {
2300
3635
  await syncServer.stop();
2301
3636
  if (httpServer) {
2302
3637
  await new Promise((resolve) => {
2303
- httpServer.close(() => resolve());
3638
+ httpServer?.close(() => resolve());
2304
3639
  });
2305
3640
  httpServer = null;
2306
3641
  }
2307
3642
  }
2308
3643
  };
2309
3644
  }
3645
+ function renderDashboardHtml(status) {
3646
+ const version = status.version;
3647
+ const uptime = formatUptime(status.uptime);
3648
+ return `<!DOCTYPE html>
3649
+ <html lang="en">
3650
+ <head>
3651
+ <meta charset="UTF-8">
3652
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3653
+ <title>Kora Dashboard</title>
3654
+ <style>
3655
+ * { box-sizing: border-box; margin: 0; padding: 0 }
3656
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem }
3657
+ h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.25rem }
3658
+ .subtitle { color: #64748b; margin-bottom: 2rem; font-size: 0.875rem }
3659
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem }
3660
+ .card { background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; padding: 1.25rem }
3661
+ .card .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; margin-bottom: 0.5rem }
3662
+ .card .value { font-size: 1.75rem; font-weight: 700; color: #38bdf8 }
3663
+ .card .value.green { color: #4ade80 }
3664
+ .card .value.red { color: #f87171 }
3665
+ .card .value.yellow { color: #fbbf24 }
3666
+ .section-title { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; margin-top: 1.5rem }
3667
+ table { width: 100%; border-collapse: collapse; font-size: 0.875rem }
3668
+ th { text-align: left; padding: 0.5rem 0.75rem; color: #64748b; font-weight: 500; border-bottom: 1px solid #334155 }
3669
+ td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #1e293b }
3670
+ .status-dot { display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; margin-right: 0.375rem }
3671
+ .status-dot.running { background: #4ade80 }
3672
+ .status-dot.stopped { background: #f87171 }
3673
+ </style>
3674
+ </head>
3675
+ <body>
3676
+ <h1>Kora Sync Server</h1>
3677
+ <p class="subtitle">v${version} &middot; <span class="status-dot running"></span>Running</p>
3678
+ <div class="grid">
3679
+ <div class="card"><div class="label">Uptime</div><div class="value">${uptime}</div></div>
3680
+ <div class="card"><div class="label">Connected Clients</div><div class="value" id="connectedClients">${status.connectedClients}</div></div>
3681
+ <div class="card"><div class="label">Total Operations</div><div class="value" id="totalOperations">${status.totalOperations}</div></div>
3682
+ <div class="card"><div class="label">Peak Connections</div><div class="value green" id="peakConnections">${status.peakConnections}</div></div>
3683
+ <div class="card"><div class="label">Ops Received</div><div class="value" id="opsReceived">${status.operationsReceived}</div></div>
3684
+ <div class="card"><div class="label">Ops Sent</div><div class="value" id="opsSent">${status.operationsSent}</div></div>
3685
+ <div class="card"><div class="label">Errors</div><div class="value ${status.errorCount > 0 ? "red" : "green"}" id="errors">${status.errorCount}</div></div>
3686
+ <div class="card"><div class="label">Schema Version</div><div class="value">${status.schemaVersion}</div></div>
3687
+ </div>
3688
+ <script>
3689
+ (function() {
3690
+ const es = new EventSource('/__kora/events');
3691
+ es.addEventListener('status', (e) => {
3692
+ const s = JSON.parse(e.data);
3693
+ for (const [id, val] of Object.entries({
3694
+ connectedClients: s.connectedClients,
3695
+ totalOperations: s.totalOperations,
3696
+ peakConnections: s.peakConnections,
3697
+ opsReceived: s.operationsReceived,
3698
+ opsSent: s.operationsSent,
3699
+ errors: s.errorCount,
3700
+ })) {
3701
+ const el = document.getElementById(id);
3702
+ if (el) { el.textContent = String(val); el.className = 'value' + (id === 'errors' && val > 0 ? ' red' : id === 'errors' ? ' green' : ''); }
3703
+ }
3704
+ });
3705
+ es.onerror = () => { setTimeout(() => document.location.reload(), 5000); };
3706
+ })();
3707
+ </script>
3708
+ </body>
3709
+ </html>`;
3710
+ }
3711
+ function formatUptime(ms) {
3712
+ const seconds = Math.floor(ms / 1e3);
3713
+ const minutes = Math.floor(seconds / 60);
3714
+ const hours = Math.floor(minutes / 60);
3715
+ const parts = [];
3716
+ if (hours > 0) parts.push(`${hours}h`);
3717
+ if (minutes % 60 > 0) parts.push(`${minutes % 60}m`);
3718
+ parts.push(`${seconds % 60}s`);
3719
+ return parts.join(" ");
3720
+ }
2310
3721
  export {
3722
+ AwarenessRelay,
2311
3723
  ClientSession,
2312
3724
  HttpServerTransport,
2313
3725
  KoraAuthProvider,
2314
3726
  KoraSyncServer,
2315
3727
  MemoryServerStore,
3728
+ MixedAuthProvider,
2316
3729
  NoAuthProvider,
2317
3730
  PostgresServerStore,
2318
3731
  SqliteServerStore,
2319
3732
  TokenAuthProvider,
2320
3733
  WsServerTransport,
3734
+ createDefaultLogger,
3735
+ createJsonLogger,
2321
3736
  createKoraServer,
2322
3737
  createPostgresServerStore,
3738
+ createPrettyLogger,
2323
3739
  createProductionServer,
3740
+ createSilentLogger,
2324
3741
  createSqliteServerStore
2325
3742
  };
2326
3743
  //# sourceMappingURL=index.js.map