@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/README.md +33 -0
- package/dist/index.cjs +253 -75
- 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 +249 -77
- 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
|
}
|
|
@@ -503,7 +508,9 @@ var PostgresServerStore = class {
|
|
|
503
508
|
try {
|
|
504
509
|
await this.db.execute(sql.raw(alterSql));
|
|
505
510
|
} catch (e) {
|
|
506
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
586
|
+
validateFieldName(collection, key, schema);
|
|
582
587
|
}
|
|
583
588
|
}
|
|
584
589
|
if (options?.orderBy) {
|
|
585
|
-
validateFieldName(collection, options.orderBy,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
951
|
+
const schema = this.schema;
|
|
952
|
+
if (!schema.collections[collection]) {
|
|
956
953
|
throw new Error(
|
|
957
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
1119
|
+
validateFieldName(collection, key, schema);
|
|
1122
1120
|
}
|
|
1123
1121
|
}
|
|
1124
1122
|
if (options?.orderBy) {
|
|
1125
|
-
validateFieldName(collection, options.orderBy,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
1472
|
+
const schema = this.schema;
|
|
1473
|
+
if (!schema.collections[collection]) {
|
|
1480
1474
|
throw new Error(
|
|
1481
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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,
|