@korajs/server 0.3.3 → 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/README.md +33 -0
- package/dist/index.cjs +250 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +142 -2
- package/dist/index.d.ts +142 -2
- package/dist/index.js +246 -76
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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
|
|
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,
|
|
210
|
+
validateFieldName(collection, key, schema);
|
|
212
211
|
}
|
|
213
212
|
}
|
|
214
213
|
if (options?.orderBy) {
|
|
215
|
-
validateFieldName(collection, options.orderBy,
|
|
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,
|
|
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
|
|
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)
|
|
329
|
-
|
|
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)
|
|
382
|
-
|
|
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
|
-
|
|
432
|
+
const schema = this.schema;
|
|
433
|
+
if (!schema.collections[collection]) {
|
|
429
434
|
throw new Error(
|
|
430
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
435
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
431
436
|
);
|
|
432
437
|
}
|
|
433
438
|
}
|
|
@@ -537,7 +542,7 @@ var PostgresServerStore = class {
|
|
|
537
542
|
lastSeenAt: sql`${now}`
|
|
538
543
|
}
|
|
539
544
|
});
|
|
540
|
-
if (this.schema
|
|
545
|
+
if (this.schema?.collections[op.collection]) {
|
|
541
546
|
await this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
|
|
542
547
|
}
|
|
543
548
|
});
|
|
@@ -551,10 +556,7 @@ var PostgresServerStore = class {
|
|
|
551
556
|
this.assertOpen();
|
|
552
557
|
await this.ready;
|
|
553
558
|
const rows = await this.db.select().from(pgOperations).where(
|
|
554
|
-
and(
|
|
555
|
-
eq(pgOperations.nodeId, nodeId),
|
|
556
|
-
between(pgOperations.sequenceNumber, fromSeq, toSeq)
|
|
557
|
-
)
|
|
559
|
+
and(eq(pgOperations.nodeId, nodeId), between(pgOperations.sequenceNumber, fromSeq, toSeq))
|
|
558
560
|
).orderBy(asc(pgOperations.sequenceNumber));
|
|
559
561
|
return rows.map((row) => this.deserializeOperation(row));
|
|
560
562
|
}
|
|
@@ -567,7 +569,7 @@ var PostgresServerStore = class {
|
|
|
567
569
|
async materializeCollection(collection) {
|
|
568
570
|
this.assertOpen();
|
|
569
571
|
await this.ready;
|
|
570
|
-
if (this.schema
|
|
572
|
+
if (this.schema?.collections[collection]) {
|
|
571
573
|
return this.queryCollection(collection);
|
|
572
574
|
}
|
|
573
575
|
return this.materializeFromOpsLog(collection);
|
|
@@ -577,14 +579,15 @@ var PostgresServerStore = class {
|
|
|
577
579
|
await this.ready;
|
|
578
580
|
this.assertSchema();
|
|
579
581
|
this.assertCollection(collection);
|
|
580
|
-
const
|
|
582
|
+
const schema = this.schema;
|
|
583
|
+
const collectionDef = schema.collections[collection];
|
|
581
584
|
if (options?.where) {
|
|
582
585
|
for (const key of Object.keys(options.where)) {
|
|
583
|
-
validateFieldName(collection, key,
|
|
586
|
+
validateFieldName(collection, key, schema);
|
|
584
587
|
}
|
|
585
588
|
}
|
|
586
589
|
if (options?.orderBy) {
|
|
587
|
-
validateFieldName(collection, options.orderBy,
|
|
590
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
588
591
|
}
|
|
589
592
|
const query = this.buildSelectQuery(collection, options);
|
|
590
593
|
const rows = await this.db.execute(query);
|
|
@@ -595,7 +598,8 @@ var PostgresServerStore = class {
|
|
|
595
598
|
await this.ready;
|
|
596
599
|
this.assertSchema();
|
|
597
600
|
this.assertCollection(collection);
|
|
598
|
-
const
|
|
601
|
+
const schema = this.schema;
|
|
602
|
+
const collectionDef = schema.collections[collection];
|
|
599
603
|
const query = sql`SELECT * FROM ${sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
600
604
|
const rows = await this.db.execute(query);
|
|
601
605
|
if (rows.length === 0) return null;
|
|
@@ -606,9 +610,10 @@ var PostgresServerStore = class {
|
|
|
606
610
|
await this.ready;
|
|
607
611
|
this.assertSchema();
|
|
608
612
|
this.assertCollection(collection);
|
|
613
|
+
const schema = this.schema;
|
|
609
614
|
if (where) {
|
|
610
615
|
for (const key of Object.keys(where)) {
|
|
611
|
-
validateFieldName(collection, key,
|
|
616
|
+
validateFieldName(collection, key, schema);
|
|
612
617
|
}
|
|
613
618
|
}
|
|
614
619
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
@@ -628,18 +633,13 @@ var PostgresServerStore = class {
|
|
|
628
633
|
* all operations for that record.
|
|
629
634
|
*/
|
|
630
635
|
async rebuildMaterializedRecord(txOrDb, collection, recordId) {
|
|
631
|
-
const collectionDef = this.schema
|
|
636
|
+
const collectionDef = this.schema?.collections[collection];
|
|
632
637
|
if (!collectionDef) return;
|
|
633
638
|
const ops = await txOrDb.select({
|
|
634
639
|
type: pgOperations.type,
|
|
635
640
|
data: pgOperations.data,
|
|
636
641
|
wallTime: pgOperations.wallTime
|
|
637
|
-
}).from(pgOperations).where(
|
|
638
|
-
and(
|
|
639
|
-
eq(pgOperations.collection, collection),
|
|
640
|
-
eq(pgOperations.recordId, recordId)
|
|
641
|
-
)
|
|
642
|
-
).orderBy(
|
|
642
|
+
}).from(pgOperations).where(and(eq(pgOperations.collection, collection), eq(pgOperations.recordId, recordId))).orderBy(
|
|
643
643
|
asc(pgOperations.wallTime),
|
|
644
644
|
asc(pgOperations.logical),
|
|
645
645
|
asc(pgOperations.sequenceNumber)
|
|
@@ -709,7 +709,7 @@ var PostgresServerStore = class {
|
|
|
709
709
|
* Backfill a single collection's materialized table from operations.
|
|
710
710
|
*/
|
|
711
711
|
async backfillCollection(collectionName) {
|
|
712
|
-
const collectionDef = this.schema
|
|
712
|
+
const collectionDef = this.schema?.collections[collectionName];
|
|
713
713
|
if (!collectionDef) return;
|
|
714
714
|
const allOps = await this.db.select({
|
|
715
715
|
recordId: pgOperations.recordId,
|
|
@@ -766,9 +766,7 @@ var PostgresServerStore = class {
|
|
|
766
766
|
options?.where ?? {},
|
|
767
767
|
options?.includeDeleted ?? false
|
|
768
768
|
);
|
|
769
|
-
const parts = [
|
|
770
|
-
sql`SELECT * FROM ${sql.raw(collection)} WHERE ${whereClause}`
|
|
771
|
-
];
|
|
769
|
+
const parts = [sql`SELECT * FROM ${sql.raw(collection)} WHERE ${whereClause}`];
|
|
772
770
|
if (options?.orderBy) {
|
|
773
771
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
774
772
|
parts.push(sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
@@ -881,12 +879,8 @@ var PostgresServerStore = class {
|
|
|
881
879
|
await this.db.execute(
|
|
882
880
|
sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
|
|
883
881
|
);
|
|
884
|
-
await this.db.execute(
|
|
885
|
-
|
|
886
|
-
);
|
|
887
|
-
await this.db.execute(
|
|
888
|
-
sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`
|
|
889
|
-
);
|
|
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)`);
|
|
890
884
|
await this.db.execute(
|
|
891
885
|
sql`CREATE INDEX IF NOT EXISTS idx_collection_record ON operations (collection, record_id)`
|
|
892
886
|
);
|
|
@@ -954,9 +948,10 @@ var PostgresServerStore = class {
|
|
|
954
948
|
}
|
|
955
949
|
}
|
|
956
950
|
assertCollection(collection) {
|
|
957
|
-
|
|
951
|
+
const schema = this.schema;
|
|
952
|
+
if (!schema.collections[collection]) {
|
|
958
953
|
throw new Error(
|
|
959
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
954
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
960
955
|
);
|
|
961
956
|
}
|
|
962
957
|
}
|
|
@@ -1089,7 +1084,7 @@ var SqliteServerStore = class {
|
|
|
1089
1084
|
lastSeenAt: sql2`${now}`
|
|
1090
1085
|
}
|
|
1091
1086
|
}).run();
|
|
1092
|
-
if (this.schema
|
|
1087
|
+
if (this.schema?.collections[op.collection]) {
|
|
1093
1088
|
this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
|
|
1094
1089
|
}
|
|
1095
1090
|
return "applied";
|
|
@@ -1108,7 +1103,7 @@ var SqliteServerStore = class {
|
|
|
1108
1103
|
}
|
|
1109
1104
|
async materializeCollection(collection) {
|
|
1110
1105
|
this.assertOpen();
|
|
1111
|
-
if (this.schema
|
|
1106
|
+
if (this.schema?.collections[collection]) {
|
|
1112
1107
|
return this.queryCollection(collection);
|
|
1113
1108
|
}
|
|
1114
1109
|
return this.materializeFromOpsLog(collection);
|
|
@@ -1117,14 +1112,15 @@ var SqliteServerStore = class {
|
|
|
1117
1112
|
this.assertOpen();
|
|
1118
1113
|
this.assertSchema();
|
|
1119
1114
|
this.assertCollection(collection);
|
|
1120
|
-
const
|
|
1115
|
+
const schema = this.schema;
|
|
1116
|
+
const collectionDef = schema.collections[collection];
|
|
1121
1117
|
if (options?.where) {
|
|
1122
1118
|
for (const key of Object.keys(options.where)) {
|
|
1123
|
-
validateFieldName(collection, key,
|
|
1119
|
+
validateFieldName(collection, key, schema);
|
|
1124
1120
|
}
|
|
1125
1121
|
}
|
|
1126
1122
|
if (options?.orderBy) {
|
|
1127
|
-
validateFieldName(collection, options.orderBy,
|
|
1123
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
1128
1124
|
}
|
|
1129
1125
|
const query = this.buildSelectQuery(collection, options);
|
|
1130
1126
|
const rows = this.db.all(query);
|
|
@@ -1134,7 +1130,8 @@ var SqliteServerStore = class {
|
|
|
1134
1130
|
this.assertOpen();
|
|
1135
1131
|
this.assertSchema();
|
|
1136
1132
|
this.assertCollection(collection);
|
|
1137
|
-
const
|
|
1133
|
+
const schema = this.schema;
|
|
1134
|
+
const collectionDef = schema.collections[collection];
|
|
1138
1135
|
const query = sql2`SELECT * FROM ${sql2.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
1139
1136
|
const rows = this.db.all(query);
|
|
1140
1137
|
if (rows.length === 0) return null;
|
|
@@ -1144,9 +1141,10 @@ var SqliteServerStore = class {
|
|
|
1144
1141
|
this.assertOpen();
|
|
1145
1142
|
this.assertSchema();
|
|
1146
1143
|
this.assertCollection(collection);
|
|
1144
|
+
const schema = this.schema;
|
|
1147
1145
|
if (where) {
|
|
1148
1146
|
for (const key of Object.keys(where)) {
|
|
1149
|
-
validateFieldName(collection, key,
|
|
1147
|
+
validateFieldName(collection, key, schema);
|
|
1150
1148
|
}
|
|
1151
1149
|
}
|
|
1152
1150
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
@@ -1166,18 +1164,13 @@ var SqliteServerStore = class {
|
|
|
1166
1164
|
* transaction for atomic dual-write.
|
|
1167
1165
|
*/
|
|
1168
1166
|
rebuildMaterializedRecord(txOrDb, collection, recordId) {
|
|
1169
|
-
const collectionDef = this.schema
|
|
1167
|
+
const collectionDef = this.schema?.collections[collection];
|
|
1170
1168
|
if (!collectionDef) return;
|
|
1171
1169
|
const ops = txOrDb.select({
|
|
1172
1170
|
type: operations.type,
|
|
1173
1171
|
data: operations.data,
|
|
1174
1172
|
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();
|
|
1173
|
+
}).from(operations).where(and2(eq2(operations.collection, collection), eq2(operations.recordId, recordId))).orderBy(asc2(operations.wallTime), asc2(operations.logical), asc2(operations.sequenceNumber)).all();
|
|
1181
1174
|
const parsedOps = ops.map((op) => ({
|
|
1182
1175
|
type: op.type,
|
|
1183
1176
|
data: op.data !== null ? JSON.parse(op.data) : null
|
|
@@ -1246,7 +1239,7 @@ var SqliteServerStore = class {
|
|
|
1246
1239
|
* Backfill a single collection's materialized table from operations.
|
|
1247
1240
|
*/
|
|
1248
1241
|
backfillCollection(collectionName) {
|
|
1249
|
-
const collectionDef = this.schema
|
|
1242
|
+
const collectionDef = this.schema?.collections[collectionName];
|
|
1250
1243
|
if (!collectionDef) return;
|
|
1251
1244
|
const allOps = this.db.select({
|
|
1252
1245
|
recordId: operations.recordId,
|
|
@@ -1301,9 +1294,7 @@ var SqliteServerStore = class {
|
|
|
1301
1294
|
options?.where ?? {},
|
|
1302
1295
|
options?.includeDeleted ?? false
|
|
1303
1296
|
);
|
|
1304
|
-
const parts = [
|
|
1305
|
-
sql2`SELECT * FROM ${sql2.raw(collection)} WHERE ${whereClause}`
|
|
1306
|
-
];
|
|
1297
|
+
const parts = [sql2`SELECT * FROM ${sql2.raw(collection)} WHERE ${whereClause}`];
|
|
1307
1298
|
if (options?.orderBy) {
|
|
1308
1299
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
1309
1300
|
parts.push(sql2.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
@@ -1478,9 +1469,10 @@ var SqliteServerStore = class {
|
|
|
1478
1469
|
}
|
|
1479
1470
|
}
|
|
1480
1471
|
assertCollection(collection) {
|
|
1481
|
-
|
|
1472
|
+
const schema = this.schema;
|
|
1473
|
+
if (!schema.collections[collection]) {
|
|
1482
1474
|
throw new Error(
|
|
1483
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
1475
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
1484
1476
|
);
|
|
1485
1477
|
}
|
|
1486
1478
|
}
|
|
@@ -1642,11 +1634,7 @@ var WsServerTransport = class {
|
|
|
1642
1634
|
// src/session/client-session.ts
|
|
1643
1635
|
import { generateUUIDv7 as generateUUIDv74 } from "@korajs/core";
|
|
1644
1636
|
import { topologicalSort } from "@korajs/core/internal";
|
|
1645
|
-
import {
|
|
1646
|
-
NegotiatedMessageSerializer,
|
|
1647
|
-
versionVectorToWire,
|
|
1648
|
-
wireToVersionVector
|
|
1649
|
-
} from "@korajs/sync";
|
|
1637
|
+
import { NegotiatedMessageSerializer, versionVectorToWire, wireToVersionVector } from "@korajs/sync";
|
|
1650
1638
|
|
|
1651
1639
|
// src/scopes/server-scope-filter.ts
|
|
1652
1640
|
function operationMatchesScopes(op, scopes) {
|
|
@@ -1694,6 +1682,7 @@ var ClientSession = class {
|
|
|
1694
1682
|
batchSize;
|
|
1695
1683
|
schemaVersion;
|
|
1696
1684
|
onRelay;
|
|
1685
|
+
onAwarenessUpdate;
|
|
1697
1686
|
onClose;
|
|
1698
1687
|
constructor(options) {
|
|
1699
1688
|
this.sessionId = options.sessionId;
|
|
@@ -1705,6 +1694,7 @@ var ClientSession = class {
|
|
|
1705
1694
|
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
1706
1695
|
this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
1707
1696
|
this.onRelay = options.onRelay ?? null;
|
|
1697
|
+
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
1708
1698
|
this.onClose = options.onClose ?? null;
|
|
1709
1699
|
}
|
|
1710
1700
|
/**
|
|
@@ -1767,6 +1757,13 @@ var ClientSession = class {
|
|
|
1767
1757
|
isStreaming() {
|
|
1768
1758
|
return this.state === "streaming";
|
|
1769
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
|
+
}
|
|
1770
1767
|
// --- Private protocol handlers ---
|
|
1771
1768
|
handleMessage(message) {
|
|
1772
1769
|
switch (message.type) {
|
|
@@ -1781,6 +1778,9 @@ var ClientSession = class {
|
|
|
1781
1778
|
break;
|
|
1782
1779
|
case "error":
|
|
1783
1780
|
break;
|
|
1781
|
+
case "awareness-update":
|
|
1782
|
+
this.handleAwarenessUpdate(message);
|
|
1783
|
+
break;
|
|
1784
1784
|
}
|
|
1785
1785
|
}
|
|
1786
1786
|
async handleHandshake(msg) {
|
|
@@ -1800,6 +1800,19 @@ var ClientSession = class {
|
|
|
1800
1800
|
this.authContext = context;
|
|
1801
1801
|
this.state = "authenticated";
|
|
1802
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
|
+
}
|
|
1803
1816
|
const serverVector = this.store.getVersionVector();
|
|
1804
1817
|
const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
|
|
1805
1818
|
this.setSerializerWireFormat(selectedWireFormat);
|
|
@@ -1810,7 +1823,10 @@ var ClientSession = class {
|
|
|
1810
1823
|
versionVector: versionVectorToWire(serverVector),
|
|
1811
1824
|
schemaVersion: this.schemaVersion,
|
|
1812
1825
|
accepted: true,
|
|
1813
|
-
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 } : {}
|
|
1814
1830
|
};
|
|
1815
1831
|
this.transport.send(response);
|
|
1816
1832
|
this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
|
|
@@ -1822,8 +1838,10 @@ var ClientSession = class {
|
|
|
1822
1838
|
async handleOperationBatch(msg) {
|
|
1823
1839
|
const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
1824
1840
|
const applied = [];
|
|
1841
|
+
const rejected = [];
|
|
1825
1842
|
for (const op of operations2) {
|
|
1826
1843
|
if (!operationMatchesScopes(op, this.authContext?.scopes)) {
|
|
1844
|
+
rejected.push(op);
|
|
1827
1845
|
continue;
|
|
1828
1846
|
}
|
|
1829
1847
|
const result = await this.store.applyRemoteOperation(op);
|
|
@@ -1831,6 +1849,15 @@ var ClientSession = class {
|
|
|
1831
1849
|
applied.push(op);
|
|
1832
1850
|
}
|
|
1833
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
|
+
}
|
|
1834
1861
|
if (operations2.length > 0) {
|
|
1835
1862
|
this.emitter?.emit({
|
|
1836
1863
|
type: "sync:received",
|
|
@@ -1893,6 +1920,9 @@ var ClientSession = class {
|
|
|
1893
1920
|
});
|
|
1894
1921
|
}
|
|
1895
1922
|
}
|
|
1923
|
+
handleAwarenessUpdate(msg) {
|
|
1924
|
+
this.onAwarenessUpdate?.(this.sessionId, msg);
|
|
1925
|
+
}
|
|
1896
1926
|
sendError(code, message, retriable) {
|
|
1897
1927
|
const errorMsg = {
|
|
1898
1928
|
type: "error",
|
|
@@ -1923,8 +1953,107 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
1923
1953
|
}
|
|
1924
1954
|
|
|
1925
1955
|
// src/server/kora-sync-server.ts
|
|
1926
|
-
import { SyncError as SyncError4, generateUUIDv7 as
|
|
1956
|
+
import { SyncError as SyncError4, generateUUIDv7 as generateUUIDv76 } from "@korajs/core";
|
|
1927
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
|
|
1928
2057
|
var DEFAULT_MAX_CONNECTIONS = 0;
|
|
1929
2058
|
var DEFAULT_BATCH_SIZE2 = 100;
|
|
1930
2059
|
var DEFAULT_SCHEMA_VERSION2 = 1;
|
|
@@ -1941,6 +2070,7 @@ var KoraSyncServer = class {
|
|
|
1941
2070
|
port;
|
|
1942
2071
|
host;
|
|
1943
2072
|
path;
|
|
2073
|
+
awarenessRelay = new AwarenessRelay();
|
|
1944
2074
|
sessions = /* @__PURE__ */ new Map();
|
|
1945
2075
|
httpClients = /* @__PURE__ */ new Map();
|
|
1946
2076
|
httpSessionToClient = /* @__PURE__ */ new Map();
|
|
@@ -2002,6 +2132,7 @@ var KoraSyncServer = class {
|
|
|
2002
2132
|
* Stop the server. Closes all sessions and the WebSocket server.
|
|
2003
2133
|
*/
|
|
2004
2134
|
async stop() {
|
|
2135
|
+
this.awarenessRelay.clear();
|
|
2005
2136
|
for (const session of this.sessions.values()) {
|
|
2006
2137
|
session.close("server shutting down");
|
|
2007
2138
|
}
|
|
@@ -2058,7 +2189,7 @@ var KoraSyncServer = class {
|
|
|
2058
2189
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
2059
2190
|
transport.send({
|
|
2060
2191
|
type: "error",
|
|
2061
|
-
messageId:
|
|
2192
|
+
messageId: generateUUIDv76(),
|
|
2062
2193
|
code: "MAX_CONNECTIONS",
|
|
2063
2194
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
2064
2195
|
retriable: true
|
|
@@ -2069,7 +2200,7 @@ var KoraSyncServer = class {
|
|
|
2069
2200
|
max: this.maxConnections
|
|
2070
2201
|
});
|
|
2071
2202
|
}
|
|
2072
|
-
const sessionId =
|
|
2203
|
+
const sessionId = generateUUIDv76();
|
|
2073
2204
|
const session = new ClientSession({
|
|
2074
2205
|
sessionId,
|
|
2075
2206
|
transport,
|
|
@@ -2082,6 +2213,9 @@ var KoraSyncServer = class {
|
|
|
2082
2213
|
onRelay: (sourceSessionId, operations2) => {
|
|
2083
2214
|
this.handleRelay(sourceSessionId, operations2);
|
|
2084
2215
|
},
|
|
2216
|
+
onAwarenessUpdate: (sourceSessionId, message) => {
|
|
2217
|
+
this.handleAwarenessRelay(sourceSessionId, message);
|
|
2218
|
+
},
|
|
2085
2219
|
onClose: (sid) => {
|
|
2086
2220
|
this.handleSessionClose(sid);
|
|
2087
2221
|
}
|
|
@@ -2115,6 +2249,7 @@ var KoraSyncServer = class {
|
|
|
2115
2249
|
}
|
|
2116
2250
|
}
|
|
2117
2251
|
handleSessionClose(sessionId) {
|
|
2252
|
+
this.awarenessRelay.removeClient(sessionId);
|
|
2118
2253
|
this.sessions.delete(sessionId);
|
|
2119
2254
|
const clientId = this.httpSessionToClient.get(sessionId);
|
|
2120
2255
|
if (clientId) {
|
|
@@ -2122,6 +2257,15 @@ var KoraSyncServer = class {
|
|
|
2122
2257
|
this.httpClients.delete(clientId);
|
|
2123
2258
|
}
|
|
2124
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
|
+
}
|
|
2125
2269
|
getOrCreateHttpClient(clientId) {
|
|
2126
2270
|
const existing = this.httpClients.get(clientId);
|
|
2127
2271
|
if (existing) {
|
|
@@ -2203,6 +2347,30 @@ var KoraAuthProvider = class {
|
|
|
2203
2347
|
}
|
|
2204
2348
|
};
|
|
2205
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
|
+
|
|
2206
2374
|
// src/server/create-server.ts
|
|
2207
2375
|
function createKoraServer(config) {
|
|
2208
2376
|
return new KoraSyncServer(config);
|
|
@@ -2291,7 +2459,7 @@ function createProductionServer(config) {
|
|
|
2291
2459
|
}
|
|
2292
2460
|
});
|
|
2293
2461
|
return new Promise((resolve2) => {
|
|
2294
|
-
httpServer
|
|
2462
|
+
httpServer?.listen(port, "0.0.0.0", () => {
|
|
2295
2463
|
resolve2(`http://localhost:${port}`);
|
|
2296
2464
|
});
|
|
2297
2465
|
});
|
|
@@ -2300,7 +2468,7 @@ function createProductionServer(config) {
|
|
|
2300
2468
|
await syncServer.stop();
|
|
2301
2469
|
if (httpServer) {
|
|
2302
2470
|
await new Promise((resolve) => {
|
|
2303
|
-
httpServer
|
|
2471
|
+
httpServer?.close(() => resolve());
|
|
2304
2472
|
});
|
|
2305
2473
|
httpServer = null;
|
|
2306
2474
|
}
|
|
@@ -2308,11 +2476,13 @@ function createProductionServer(config) {
|
|
|
2308
2476
|
};
|
|
2309
2477
|
}
|
|
2310
2478
|
export {
|
|
2479
|
+
AwarenessRelay,
|
|
2311
2480
|
ClientSession,
|
|
2312
2481
|
HttpServerTransport,
|
|
2313
2482
|
KoraAuthProvider,
|
|
2314
2483
|
KoraSyncServer,
|
|
2315
2484
|
MemoryServerStore,
|
|
2485
|
+
MixedAuthProvider,
|
|
2316
2486
|
NoAuthProvider,
|
|
2317
2487
|
PostgresServerStore,
|
|
2318
2488
|
SqliteServerStore,
|