@korajs/server 0.3.2 → 0.4.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
@@ -63,9 +63,7 @@ ALTER TABLE ${name} ADD COLUMN ${colDef}`);
63
63
  `CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${name} (${indexField})`
64
64
  );
65
65
  }
66
- statements.push(
67
- `CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`
68
- );
66
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`);
69
67
  return statements;
70
68
  }
71
69
  function generateAllCollectionDDL(schema, dialect) {
@@ -180,7 +178,7 @@ var MemoryServerStore = class {
180
178
  if (op.sequenceNumber > currentSeq) {
181
179
  this.versionVector.set(op.nodeId, op.sequenceNumber);
182
180
  }
183
- if (this.schema && this.schema.collections[op.collection]) {
181
+ if (this.schema?.collections[op.collection]) {
184
182
  this.rebuildMaterializedRecord(op.collection, op.recordId);
185
183
  }
186
184
  return "applied";
@@ -197,7 +195,7 @@ var MemoryServerStore = class {
197
195
  }
198
196
  async materializeCollection(collection) {
199
197
  this.assertOpen();
200
- if (this.schema && this.schema.collections[collection]) {
198
+ if (this.schema?.collections[collection]) {
201
199
  return this.queryCollection(collection);
202
200
  }
203
201
  return this.materializeFromOps(collection);
@@ -206,13 +204,14 @@ var MemoryServerStore = class {
206
204
  this.assertOpen();
207
205
  this.assertSchema();
208
206
  this.assertCollection(collection);
207
+ const schema = this.schema;
209
208
  if (options?.where) {
210
209
  for (const key of Object.keys(options.where)) {
211
- validateFieldName(collection, key, this.schema);
210
+ validateFieldName(collection, key, schema);
212
211
  }
213
212
  }
214
213
  if (options?.orderBy) {
215
- validateFieldName(collection, options.orderBy, this.schema);
214
+ validateFieldName(collection, options.orderBy, schema);
216
215
  }
217
216
  const collectionMap = this.materializedRecords.get(collection);
218
217
  if (!collectionMap) return [];
@@ -279,9 +278,10 @@ var MemoryServerStore = class {
279
278
  this.assertOpen();
280
279
  this.assertSchema();
281
280
  this.assertCollection(collection);
281
+ const schema = this.schema;
282
282
  if (where) {
283
283
  for (const key of Object.keys(where)) {
284
- validateFieldName(collection, key, this.schema);
284
+ validateFieldName(collection, key, schema);
285
285
  }
286
286
  }
287
287
  const collectionMap = this.materializedRecords.get(collection);
@@ -317,7 +317,7 @@ var MemoryServerStore = class {
317
317
  // Materialization internals
318
318
  // ---------------------------------------------------------------------------
319
319
  rebuildMaterializedRecord(collection, recordId) {
320
- const collectionDef = this.schema.collections[collection];
320
+ const collectionDef = this.schema?.collections[collection];
321
321
  if (!collectionDef) return;
322
322
  let collectionMap = this.materializedRecords.get(collection);
323
323
  if (!collectionMap) {
@@ -325,8 +325,10 @@ var MemoryServerStore = class {
325
325
  this.materializedRecords.set(collection, collectionMap);
326
326
  }
327
327
  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;
328
+ if (a.timestamp.wallTime !== b.timestamp.wallTime)
329
+ return a.timestamp.wallTime - b.timestamp.wallTime;
330
+ if (a.timestamp.logical !== b.timestamp.logical)
331
+ return a.timestamp.logical - b.timestamp.logical;
330
332
  return a.sequenceNumber - b.sequenceNumber;
331
333
  });
332
334
  const parsedOps = recordOps.map((op) => ({
@@ -378,8 +380,10 @@ var MemoryServerStore = class {
378
380
  // ---------------------------------------------------------------------------
379
381
  materializeFromOps(collection) {
380
382
  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;
383
+ if (a.timestamp.wallTime !== b.timestamp.wallTime)
384
+ return a.timestamp.wallTime - b.timestamp.wallTime;
385
+ if (a.timestamp.logical !== b.timestamp.logical)
386
+ return a.timestamp.logical - b.timestamp.logical;
383
387
  return a.sequenceNumber - b.sequenceNumber;
384
388
  });
385
389
  const records = /* @__PURE__ */ new Map();
@@ -425,9 +429,10 @@ var MemoryServerStore = class {
425
429
  }
426
430
  }
427
431
  assertCollection(collection) {
428
- if (!this.schema.collections[collection]) {
432
+ const schema = this.schema;
433
+ if (!schema.collections[collection]) {
429
434
  throw new Error(
430
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
435
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
431
436
  );
432
437
  }
433
438
  }
@@ -503,7 +508,9 @@ var PostgresServerStore = class {
503
508
  try {
504
509
  await this.db.execute(sql.raw(alterSql));
505
510
  } catch (e) {
506
- if (!(e instanceof Error && (e.message.includes("already exists") || e.message.includes("duplicate column")))) {
511
+ const msg = e instanceof Error ? e.message : "";
512
+ const causeMsg = e instanceof Error && e.cause instanceof Error ? e.cause.message : "";
513
+ if (!msg.includes("already exists") && !msg.includes("duplicate column") && !causeMsg.includes("already exists") && !causeMsg.includes("duplicate column")) {
507
514
  throw e;
508
515
  }
509
516
  }
@@ -535,7 +542,7 @@ var PostgresServerStore = class {
535
542
  lastSeenAt: sql`${now}`
536
543
  }
537
544
  });
538
- if (this.schema && this.schema.collections[op.collection]) {
545
+ if (this.schema?.collections[op.collection]) {
539
546
  await this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
540
547
  }
541
548
  });
@@ -549,10 +556,7 @@ var PostgresServerStore = class {
549
556
  this.assertOpen();
550
557
  await this.ready;
551
558
  const rows = await this.db.select().from(pgOperations).where(
552
- and(
553
- eq(pgOperations.nodeId, nodeId),
554
- between(pgOperations.sequenceNumber, fromSeq, toSeq)
555
- )
559
+ and(eq(pgOperations.nodeId, nodeId), between(pgOperations.sequenceNumber, fromSeq, toSeq))
556
560
  ).orderBy(asc(pgOperations.sequenceNumber));
557
561
  return rows.map((row) => this.deserializeOperation(row));
558
562
  }
@@ -565,7 +569,7 @@ var PostgresServerStore = class {
565
569
  async materializeCollection(collection) {
566
570
  this.assertOpen();
567
571
  await this.ready;
568
- if (this.schema && this.schema.collections[collection]) {
572
+ if (this.schema?.collections[collection]) {
569
573
  return this.queryCollection(collection);
570
574
  }
571
575
  return this.materializeFromOpsLog(collection);
@@ -575,14 +579,15 @@ var PostgresServerStore = class {
575
579
  await this.ready;
576
580
  this.assertSchema();
577
581
  this.assertCollection(collection);
578
- const collectionDef = this.schema.collections[collection];
582
+ const schema = this.schema;
583
+ const collectionDef = schema.collections[collection];
579
584
  if (options?.where) {
580
585
  for (const key of Object.keys(options.where)) {
581
- validateFieldName(collection, key, this.schema);
586
+ validateFieldName(collection, key, schema);
582
587
  }
583
588
  }
584
589
  if (options?.orderBy) {
585
- validateFieldName(collection, options.orderBy, this.schema);
590
+ validateFieldName(collection, options.orderBy, schema);
586
591
  }
587
592
  const query = this.buildSelectQuery(collection, options);
588
593
  const rows = await this.db.execute(query);
@@ -593,7 +598,8 @@ var PostgresServerStore = class {
593
598
  await this.ready;
594
599
  this.assertSchema();
595
600
  this.assertCollection(collection);
596
- const collectionDef = this.schema.collections[collection];
601
+ const schema = this.schema;
602
+ const collectionDef = schema.collections[collection];
597
603
  const query = sql`SELECT * FROM ${sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
598
604
  const rows = await this.db.execute(query);
599
605
  if (rows.length === 0) return null;
@@ -604,9 +610,10 @@ var PostgresServerStore = class {
604
610
  await this.ready;
605
611
  this.assertSchema();
606
612
  this.assertCollection(collection);
613
+ const schema = this.schema;
607
614
  if (where) {
608
615
  for (const key of Object.keys(where)) {
609
- validateFieldName(collection, key, this.schema);
616
+ validateFieldName(collection, key, schema);
610
617
  }
611
618
  }
612
619
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -626,18 +633,13 @@ var PostgresServerStore = class {
626
633
  * all operations for that record.
627
634
  */
628
635
  async rebuildMaterializedRecord(txOrDb, collection, recordId) {
629
- const collectionDef = this.schema.collections[collection];
636
+ const collectionDef = this.schema?.collections[collection];
630
637
  if (!collectionDef) return;
631
638
  const ops = await txOrDb.select({
632
639
  type: pgOperations.type,
633
640
  data: pgOperations.data,
634
641
  wallTime: pgOperations.wallTime
635
- }).from(pgOperations).where(
636
- and(
637
- eq(pgOperations.collection, collection),
638
- eq(pgOperations.recordId, recordId)
639
- )
640
- ).orderBy(
642
+ }).from(pgOperations).where(and(eq(pgOperations.collection, collection), eq(pgOperations.recordId, recordId))).orderBy(
641
643
  asc(pgOperations.wallTime),
642
644
  asc(pgOperations.logical),
643
645
  asc(pgOperations.sequenceNumber)
@@ -707,7 +709,7 @@ var PostgresServerStore = class {
707
709
  * Backfill a single collection's materialized table from operations.
708
710
  */
709
711
  async backfillCollection(collectionName) {
710
- const collectionDef = this.schema.collections[collectionName];
712
+ const collectionDef = this.schema?.collections[collectionName];
711
713
  if (!collectionDef) return;
712
714
  const allOps = await this.db.select({
713
715
  recordId: pgOperations.recordId,
@@ -764,9 +766,7 @@ var PostgresServerStore = class {
764
766
  options?.where ?? {},
765
767
  options?.includeDeleted ?? false
766
768
  );
767
- const parts = [
768
- sql`SELECT * FROM ${sql.raw(collection)} WHERE ${whereClause}`
769
- ];
769
+ const parts = [sql`SELECT * FROM ${sql.raw(collection)} WHERE ${whereClause}`];
770
770
  if (options?.orderBy) {
771
771
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
772
772
  parts.push(sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -879,12 +879,8 @@ var PostgresServerStore = class {
879
879
  await this.db.execute(
880
880
  sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
881
881
  );
882
- await this.db.execute(
883
- sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`
884
- );
885
- await this.db.execute(
886
- sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`
887
- );
882
+ await this.db.execute(sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`);
883
+ await this.db.execute(sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`);
888
884
  await this.db.execute(
889
885
  sql`CREATE INDEX IF NOT EXISTS idx_collection_record ON operations (collection, record_id)`
890
886
  );
@@ -952,9 +948,10 @@ var PostgresServerStore = class {
952
948
  }
953
949
  }
954
950
  assertCollection(collection) {
955
- if (!this.schema.collections[collection]) {
951
+ const schema = this.schema;
952
+ if (!schema.collections[collection]) {
956
953
  throw new Error(
957
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
954
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
958
955
  );
959
956
  }
960
957
  }
@@ -1087,7 +1084,7 @@ var SqliteServerStore = class {
1087
1084
  lastSeenAt: sql2`${now}`
1088
1085
  }
1089
1086
  }).run();
1090
- if (this.schema && this.schema.collections[op.collection]) {
1087
+ if (this.schema?.collections[op.collection]) {
1091
1088
  this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
1092
1089
  }
1093
1090
  return "applied";
@@ -1106,7 +1103,7 @@ var SqliteServerStore = class {
1106
1103
  }
1107
1104
  async materializeCollection(collection) {
1108
1105
  this.assertOpen();
1109
- if (this.schema && this.schema.collections[collection]) {
1106
+ if (this.schema?.collections[collection]) {
1110
1107
  return this.queryCollection(collection);
1111
1108
  }
1112
1109
  return this.materializeFromOpsLog(collection);
@@ -1115,14 +1112,15 @@ var SqliteServerStore = class {
1115
1112
  this.assertOpen();
1116
1113
  this.assertSchema();
1117
1114
  this.assertCollection(collection);
1118
- const collectionDef = this.schema.collections[collection];
1115
+ const schema = this.schema;
1116
+ const collectionDef = schema.collections[collection];
1119
1117
  if (options?.where) {
1120
1118
  for (const key of Object.keys(options.where)) {
1121
- validateFieldName(collection, key, this.schema);
1119
+ validateFieldName(collection, key, schema);
1122
1120
  }
1123
1121
  }
1124
1122
  if (options?.orderBy) {
1125
- validateFieldName(collection, options.orderBy, this.schema);
1123
+ validateFieldName(collection, options.orderBy, schema);
1126
1124
  }
1127
1125
  const query = this.buildSelectQuery(collection, options);
1128
1126
  const rows = this.db.all(query);
@@ -1132,7 +1130,8 @@ var SqliteServerStore = class {
1132
1130
  this.assertOpen();
1133
1131
  this.assertSchema();
1134
1132
  this.assertCollection(collection);
1135
- const collectionDef = this.schema.collections[collection];
1133
+ const schema = this.schema;
1134
+ const collectionDef = schema.collections[collection];
1136
1135
  const query = sql2`SELECT * FROM ${sql2.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
1137
1136
  const rows = this.db.all(query);
1138
1137
  if (rows.length === 0) return null;
@@ -1142,9 +1141,10 @@ var SqliteServerStore = class {
1142
1141
  this.assertOpen();
1143
1142
  this.assertSchema();
1144
1143
  this.assertCollection(collection);
1144
+ const schema = this.schema;
1145
1145
  if (where) {
1146
1146
  for (const key of Object.keys(where)) {
1147
- validateFieldName(collection, key, this.schema);
1147
+ validateFieldName(collection, key, schema);
1148
1148
  }
1149
1149
  }
1150
1150
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -1164,18 +1164,13 @@ var SqliteServerStore = class {
1164
1164
  * transaction for atomic dual-write.
1165
1165
  */
1166
1166
  rebuildMaterializedRecord(txOrDb, collection, recordId) {
1167
- const collectionDef = this.schema.collections[collection];
1167
+ const collectionDef = this.schema?.collections[collection];
1168
1168
  if (!collectionDef) return;
1169
1169
  const ops = txOrDb.select({
1170
1170
  type: operations.type,
1171
1171
  data: operations.data,
1172
1172
  wallTime: operations.wallTime
1173
- }).from(operations).where(
1174
- and2(
1175
- eq2(operations.collection, collection),
1176
- eq2(operations.recordId, recordId)
1177
- )
1178
- ).orderBy(asc2(operations.wallTime), asc2(operations.logical), asc2(operations.sequenceNumber)).all();
1173
+ }).from(operations).where(and2(eq2(operations.collection, collection), eq2(operations.recordId, recordId))).orderBy(asc2(operations.wallTime), asc2(operations.logical), asc2(operations.sequenceNumber)).all();
1179
1174
  const parsedOps = ops.map((op) => ({
1180
1175
  type: op.type,
1181
1176
  data: op.data !== null ? JSON.parse(op.data) : null
@@ -1244,7 +1239,7 @@ var SqliteServerStore = class {
1244
1239
  * Backfill a single collection's materialized table from operations.
1245
1240
  */
1246
1241
  backfillCollection(collectionName) {
1247
- const collectionDef = this.schema.collections[collectionName];
1242
+ const collectionDef = this.schema?.collections[collectionName];
1248
1243
  if (!collectionDef) return;
1249
1244
  const allOps = this.db.select({
1250
1245
  recordId: operations.recordId,
@@ -1299,9 +1294,7 @@ var SqliteServerStore = class {
1299
1294
  options?.where ?? {},
1300
1295
  options?.includeDeleted ?? false
1301
1296
  );
1302
- const parts = [
1303
- sql2`SELECT * FROM ${sql2.raw(collection)} WHERE ${whereClause}`
1304
- ];
1297
+ const parts = [sql2`SELECT * FROM ${sql2.raw(collection)} WHERE ${whereClause}`];
1305
1298
  if (options?.orderBy) {
1306
1299
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
1307
1300
  parts.push(sql2.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -1476,9 +1469,10 @@ var SqliteServerStore = class {
1476
1469
  }
1477
1470
  }
1478
1471
  assertCollection(collection) {
1479
- if (!this.schema.collections[collection]) {
1472
+ const schema = this.schema;
1473
+ if (!schema.collections[collection]) {
1480
1474
  throw new Error(
1481
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
1475
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
1482
1476
  );
1483
1477
  }
1484
1478
  }
@@ -1640,11 +1634,7 @@ var WsServerTransport = class {
1640
1634
  // src/session/client-session.ts
1641
1635
  import { generateUUIDv7 as generateUUIDv74 } from "@korajs/core";
1642
1636
  import { topologicalSort } from "@korajs/core/internal";
1643
- import {
1644
- NegotiatedMessageSerializer,
1645
- versionVectorToWire,
1646
- wireToVersionVector
1647
- } from "@korajs/sync";
1637
+ import { NegotiatedMessageSerializer, versionVectorToWire, wireToVersionVector } from "@korajs/sync";
1648
1638
 
1649
1639
  // src/scopes/server-scope-filter.ts
1650
1640
  function operationMatchesScopes(op, scopes) {
@@ -1692,6 +1682,7 @@ var ClientSession = class {
1692
1682
  batchSize;
1693
1683
  schemaVersion;
1694
1684
  onRelay;
1685
+ onAwarenessUpdate;
1695
1686
  onClose;
1696
1687
  constructor(options) {
1697
1688
  this.sessionId = options.sessionId;
@@ -1703,6 +1694,7 @@ var ClientSession = class {
1703
1694
  this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
1704
1695
  this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
1705
1696
  this.onRelay = options.onRelay ?? null;
1697
+ this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
1706
1698
  this.onClose = options.onClose ?? null;
1707
1699
  }
1708
1700
  /**
@@ -1765,6 +1757,13 @@ var ClientSession = class {
1765
1757
  isStreaming() {
1766
1758
  return this.state === "streaming";
1767
1759
  }
1760
+ /**
1761
+ * Get the transport for this session.
1762
+ * Used by the awareness relay to send messages to this client.
1763
+ */
1764
+ getTransport() {
1765
+ return this.transport;
1766
+ }
1768
1767
  // --- Private protocol handlers ---
1769
1768
  handleMessage(message) {
1770
1769
  switch (message.type) {
@@ -1779,6 +1778,9 @@ var ClientSession = class {
1779
1778
  break;
1780
1779
  case "error":
1781
1780
  break;
1781
+ case "awareness-update":
1782
+ this.handleAwarenessUpdate(message);
1783
+ break;
1782
1784
  }
1783
1785
  }
1784
1786
  async handleHandshake(msg) {
@@ -1798,6 +1800,19 @@ var ClientSession = class {
1798
1800
  this.authContext = context;
1799
1801
  this.state = "authenticated";
1800
1802
  }
1803
+ if (msg.syncScope) {
1804
+ const mergedScopes = { ...msg.syncScope };
1805
+ if (this.authContext?.scopes) {
1806
+ for (const [collection, authScope] of Object.entries(this.authContext.scopes)) {
1807
+ mergedScopes[collection] = { ...mergedScopes[collection] ?? {}, ...authScope };
1808
+ }
1809
+ }
1810
+ if (this.authContext) {
1811
+ this.authContext = { ...this.authContext, scopes: mergedScopes };
1812
+ } else {
1813
+ this.authContext = { userId: msg.nodeId, scopes: mergedScopes };
1814
+ }
1815
+ }
1801
1816
  const serverVector = this.store.getVersionVector();
1802
1817
  const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
1803
1818
  this.setSerializerWireFormat(selectedWireFormat);
@@ -1808,7 +1823,10 @@ var ClientSession = class {
1808
1823
  versionVector: versionVectorToWire(serverVector),
1809
1824
  schemaVersion: this.schemaVersion,
1810
1825
  accepted: true,
1811
- selectedWireFormat
1826
+ selectedWireFormat,
1827
+ // Confirm the accepted scope so the client knows what data will be synced.
1828
+ // This may differ from what the client requested if auth scopes are narrower.
1829
+ ...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
1812
1830
  };
1813
1831
  this.transport.send(response);
1814
1832
  this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
@@ -1820,8 +1838,10 @@ var ClientSession = class {
1820
1838
  async handleOperationBatch(msg) {
1821
1839
  const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
1822
1840
  const applied = [];
1841
+ const rejected = [];
1823
1842
  for (const op of operations2) {
1824
1843
  if (!operationMatchesScopes(op, this.authContext?.scopes)) {
1844
+ rejected.push(op);
1825
1845
  continue;
1826
1846
  }
1827
1847
  const result = await this.store.applyRemoteOperation(op);
@@ -1829,6 +1849,15 @@ var ClientSession = class {
1829
1849
  applied.push(op);
1830
1850
  }
1831
1851
  }
1852
+ if (rejected.length > 0) {
1853
+ for (const op of rejected) {
1854
+ this.sendError(
1855
+ "SCOPE_VIOLATION",
1856
+ `Operation "${op.id}" in collection "${op.collection}" is outside the client's sync scope`,
1857
+ false
1858
+ );
1859
+ }
1860
+ }
1832
1861
  if (operations2.length > 0) {
1833
1862
  this.emitter?.emit({
1834
1863
  type: "sync:received",
@@ -1891,6 +1920,9 @@ var ClientSession = class {
1891
1920
  });
1892
1921
  }
1893
1922
  }
1923
+ handleAwarenessUpdate(msg) {
1924
+ this.onAwarenessUpdate?.(this.sessionId, msg);
1925
+ }
1894
1926
  sendError(code, message, retriable) {
1895
1927
  const errorMsg = {
1896
1928
  type: "error",
@@ -1921,8 +1953,107 @@ function selectWireFormat(supportedWireFormats) {
1921
1953
  }
1922
1954
 
1923
1955
  // src/server/kora-sync-server.ts
1924
- import { SyncError as SyncError4, generateUUIDv7 as generateUUIDv75 } from "@korajs/core";
1956
+ import { SyncError as SyncError4, generateUUIDv7 as generateUUIDv76 } from "@korajs/core";
1925
1957
  import { JsonMessageSerializer as JsonMessageSerializer2 } from "@korajs/sync";
1958
+
1959
+ // src/awareness/awareness-relay.ts
1960
+ import { generateUUIDv7 as generateUUIDv75 } from "@korajs/core";
1961
+ var AwarenessRelay = class {
1962
+ clients = /* @__PURE__ */ new Map();
1963
+ /**
1964
+ * Register a client for awareness broadcasting.
1965
+ *
1966
+ * @param sessionId - Unique session identifier
1967
+ * @param clientId - Client-assigned awareness ID
1968
+ * @param transport - Transport for sending messages to this client
1969
+ */
1970
+ addClient(sessionId, clientId, transport) {
1971
+ this.clients.set(sessionId, {
1972
+ sessionId,
1973
+ clientId,
1974
+ transport,
1975
+ state: null
1976
+ });
1977
+ const existingStates = {};
1978
+ let hasStates = false;
1979
+ for (const [, client] of this.clients) {
1980
+ if (client.sessionId === sessionId) continue;
1981
+ if (client.state) {
1982
+ existingStates[String(client.clientId)] = client.state;
1983
+ hasStates = true;
1984
+ }
1985
+ }
1986
+ if (hasStates) {
1987
+ const catchUpMsg = {
1988
+ type: "awareness-update",
1989
+ messageId: generateUUIDv75(),
1990
+ clientId: 0,
1991
+ // Server-sourced
1992
+ states: existingStates
1993
+ };
1994
+ transport.send(catchUpMsg);
1995
+ }
1996
+ }
1997
+ /**
1998
+ * Remove a client and broadcast its removal to all remaining clients.
1999
+ *
2000
+ * @param sessionId - Session ID of the disconnecting client
2001
+ */
2002
+ removeClient(sessionId) {
2003
+ const client = this.clients.get(sessionId);
2004
+ if (!client) return;
2005
+ this.clients.delete(sessionId);
2006
+ if (client.state === null) return;
2007
+ const removalStates = {
2008
+ [String(client.clientId)]: null
2009
+ };
2010
+ const msg = {
2011
+ type: "awareness-update",
2012
+ messageId: generateUUIDv75(),
2013
+ clientId: client.clientId,
2014
+ states: removalStates
2015
+ };
2016
+ this.broadcastExcept(sessionId, msg);
2017
+ }
2018
+ /**
2019
+ * Handle an incoming awareness update from a client.
2020
+ * Stores the state and relays to all other connected clients.
2021
+ *
2022
+ * @param sessionId - Session ID of the sending client
2023
+ * @param message - The awareness update message
2024
+ */
2025
+ handleUpdate(sessionId, message) {
2026
+ const sender = this.clients.get(sessionId);
2027
+ if (!sender) return;
2028
+ const senderState = message.states[String(message.clientId)];
2029
+ if (senderState !== void 0) {
2030
+ sender.state = senderState;
2031
+ }
2032
+ this.broadcastExcept(sessionId, message);
2033
+ }
2034
+ /**
2035
+ * Get the number of registered awareness clients.
2036
+ */
2037
+ getClientCount() {
2038
+ return this.clients.size;
2039
+ }
2040
+ /**
2041
+ * Remove all clients and clear all state.
2042
+ */
2043
+ clear() {
2044
+ this.clients.clear();
2045
+ }
2046
+ // --- Private ---
2047
+ broadcastExcept(excludeSessionId, message) {
2048
+ for (const [, client] of this.clients) {
2049
+ if (client.sessionId === excludeSessionId) continue;
2050
+ if (!client.transport.isConnected()) continue;
2051
+ client.transport.send(message);
2052
+ }
2053
+ }
2054
+ };
2055
+
2056
+ // src/server/kora-sync-server.ts
1926
2057
  var DEFAULT_MAX_CONNECTIONS = 0;
1927
2058
  var DEFAULT_BATCH_SIZE2 = 100;
1928
2059
  var DEFAULT_SCHEMA_VERSION2 = 1;
@@ -1939,6 +2070,7 @@ var KoraSyncServer = class {
1939
2070
  port;
1940
2071
  host;
1941
2072
  path;
2073
+ awarenessRelay = new AwarenessRelay();
1942
2074
  sessions = /* @__PURE__ */ new Map();
1943
2075
  httpClients = /* @__PURE__ */ new Map();
1944
2076
  httpSessionToClient = /* @__PURE__ */ new Map();
@@ -2000,6 +2132,7 @@ var KoraSyncServer = class {
2000
2132
  * Stop the server. Closes all sessions and the WebSocket server.
2001
2133
  */
2002
2134
  async stop() {
2135
+ this.awarenessRelay.clear();
2003
2136
  for (const session of this.sessions.values()) {
2004
2137
  session.close("server shutting down");
2005
2138
  }
@@ -2056,7 +2189,7 @@ var KoraSyncServer = class {
2056
2189
  if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
2057
2190
  transport.send({
2058
2191
  type: "error",
2059
- messageId: generateUUIDv75(),
2192
+ messageId: generateUUIDv76(),
2060
2193
  code: "MAX_CONNECTIONS",
2061
2194
  message: `Server has reached maximum connections (${this.maxConnections})`,
2062
2195
  retriable: true
@@ -2067,7 +2200,7 @@ var KoraSyncServer = class {
2067
2200
  max: this.maxConnections
2068
2201
  });
2069
2202
  }
2070
- const sessionId = generateUUIDv75();
2203
+ const sessionId = generateUUIDv76();
2071
2204
  const session = new ClientSession({
2072
2205
  sessionId,
2073
2206
  transport,
@@ -2080,6 +2213,9 @@ var KoraSyncServer = class {
2080
2213
  onRelay: (sourceSessionId, operations2) => {
2081
2214
  this.handleRelay(sourceSessionId, operations2);
2082
2215
  },
2216
+ onAwarenessUpdate: (sourceSessionId, message) => {
2217
+ this.handleAwarenessRelay(sourceSessionId, message);
2218
+ },
2083
2219
  onClose: (sid) => {
2084
2220
  this.handleSessionClose(sid);
2085
2221
  }
@@ -2113,6 +2249,7 @@ var KoraSyncServer = class {
2113
2249
  }
2114
2250
  }
2115
2251
  handleSessionClose(sessionId) {
2252
+ this.awarenessRelay.removeClient(sessionId);
2116
2253
  this.sessions.delete(sessionId);
2117
2254
  const clientId = this.httpSessionToClient.get(sessionId);
2118
2255
  if (clientId) {
@@ -2120,6 +2257,15 @@ var KoraSyncServer = class {
2120
2257
  this.httpClients.delete(clientId);
2121
2258
  }
2122
2259
  }
2260
+ handleAwarenessRelay(sourceSessionId, message) {
2261
+ const session = this.sessions.get(sourceSessionId);
2262
+ if (!session) return;
2263
+ const transport = session.getTransport();
2264
+ if (!this.awarenessRelay.getClientCount() || !transport) {
2265
+ }
2266
+ this.awarenessRelay.addClient(sourceSessionId, message.clientId, transport);
2267
+ this.awarenessRelay.handleUpdate(sourceSessionId, message);
2268
+ }
2123
2269
  getOrCreateHttpClient(clientId) {
2124
2270
  const existing = this.httpClients.get(clientId);
2125
2271
  if (existing) {
@@ -2201,6 +2347,30 @@ var KoraAuthProvider = class {
2201
2347
  }
2202
2348
  };
2203
2349
 
2350
+ // src/auth/mixed-auth-provider.ts
2351
+ var MixedAuthProvider = class {
2352
+ primary;
2353
+ anonymousScopes;
2354
+ anonymousPrefix;
2355
+ anonymousCounter = 0;
2356
+ constructor(options) {
2357
+ this.primary = options.primary;
2358
+ this.anonymousScopes = options.anonymousScopes;
2359
+ this.anonymousPrefix = options.anonymousPrefix ?? "anon";
2360
+ }
2361
+ async authenticate(token) {
2362
+ if (token) {
2363
+ const ctx = await this.primary.authenticate(token);
2364
+ if (ctx) return ctx;
2365
+ }
2366
+ this.anonymousCounter++;
2367
+ return {
2368
+ userId: `${this.anonymousPrefix}-${Date.now()}-${this.anonymousCounter}`,
2369
+ scopes: this.anonymousScopes
2370
+ };
2371
+ }
2372
+ };
2373
+
2204
2374
  // src/server/create-server.ts
2205
2375
  function createKoraServer(config) {
2206
2376
  return new KoraSyncServer(config);
@@ -2289,7 +2459,7 @@ function createProductionServer(config) {
2289
2459
  }
2290
2460
  });
2291
2461
  return new Promise((resolve2) => {
2292
- httpServer.listen(port, "0.0.0.0", () => {
2462
+ httpServer?.listen(port, "0.0.0.0", () => {
2293
2463
  resolve2(`http://localhost:${port}`);
2294
2464
  });
2295
2465
  });
@@ -2298,7 +2468,7 @@ function createProductionServer(config) {
2298
2468
  await syncServer.stop();
2299
2469
  if (httpServer) {
2300
2470
  await new Promise((resolve) => {
2301
- httpServer.close(() => resolve());
2471
+ httpServer?.close(() => resolve());
2302
2472
  });
2303
2473
  httpServer = null;
2304
2474
  }
@@ -2306,11 +2476,13 @@ function createProductionServer(config) {
2306
2476
  };
2307
2477
  }
2308
2478
  export {
2479
+ AwarenessRelay,
2309
2480
  ClientSession,
2310
2481
  HttpServerTransport,
2311
2482
  KoraAuthProvider,
2312
2483
  KoraSyncServer,
2313
2484
  MemoryServerStore,
2485
+ MixedAuthProvider,
2314
2486
  NoAuthProvider,
2315
2487
  PostgresServerStore,
2316
2488
  SqliteServerStore,