@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.cjs
CHANGED
|
@@ -30,11 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AwarenessRelay: () => AwarenessRelay,
|
|
33
34
|
ClientSession: () => ClientSession,
|
|
34
35
|
HttpServerTransport: () => HttpServerTransport,
|
|
35
36
|
KoraAuthProvider: () => KoraAuthProvider,
|
|
36
37
|
KoraSyncServer: () => KoraSyncServer,
|
|
37
38
|
MemoryServerStore: () => MemoryServerStore,
|
|
39
|
+
MixedAuthProvider: () => MixedAuthProvider,
|
|
38
40
|
NoAuthProvider: () => NoAuthProvider,
|
|
39
41
|
PostgresServerStore: () => PostgresServerStore,
|
|
40
42
|
SqliteServerStore: () => SqliteServerStore,
|
|
@@ -116,9 +118,7 @@ ALTER TABLE ${name} ADD COLUMN ${colDef}`);
|
|
|
116
118
|
`CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${name} (${indexField})`
|
|
117
119
|
);
|
|
118
120
|
}
|
|
119
|
-
statements.push(
|
|
120
|
-
`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`
|
|
121
|
-
);
|
|
121
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`);
|
|
122
122
|
return statements;
|
|
123
123
|
}
|
|
124
124
|
function generateAllCollectionDDL(schema, dialect) {
|
|
@@ -233,7 +233,7 @@ var MemoryServerStore = class {
|
|
|
233
233
|
if (op.sequenceNumber > currentSeq) {
|
|
234
234
|
this.versionVector.set(op.nodeId, op.sequenceNumber);
|
|
235
235
|
}
|
|
236
|
-
if (this.schema
|
|
236
|
+
if (this.schema?.collections[op.collection]) {
|
|
237
237
|
this.rebuildMaterializedRecord(op.collection, op.recordId);
|
|
238
238
|
}
|
|
239
239
|
return "applied";
|
|
@@ -250,7 +250,7 @@ var MemoryServerStore = class {
|
|
|
250
250
|
}
|
|
251
251
|
async materializeCollection(collection) {
|
|
252
252
|
this.assertOpen();
|
|
253
|
-
if (this.schema
|
|
253
|
+
if (this.schema?.collections[collection]) {
|
|
254
254
|
return this.queryCollection(collection);
|
|
255
255
|
}
|
|
256
256
|
return this.materializeFromOps(collection);
|
|
@@ -259,13 +259,14 @@ var MemoryServerStore = class {
|
|
|
259
259
|
this.assertOpen();
|
|
260
260
|
this.assertSchema();
|
|
261
261
|
this.assertCollection(collection);
|
|
262
|
+
const schema = this.schema;
|
|
262
263
|
if (options?.where) {
|
|
263
264
|
for (const key of Object.keys(options.where)) {
|
|
264
|
-
validateFieldName(collection, key,
|
|
265
|
+
validateFieldName(collection, key, schema);
|
|
265
266
|
}
|
|
266
267
|
}
|
|
267
268
|
if (options?.orderBy) {
|
|
268
|
-
validateFieldName(collection, options.orderBy,
|
|
269
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
269
270
|
}
|
|
270
271
|
const collectionMap = this.materializedRecords.get(collection);
|
|
271
272
|
if (!collectionMap) return [];
|
|
@@ -332,9 +333,10 @@ var MemoryServerStore = class {
|
|
|
332
333
|
this.assertOpen();
|
|
333
334
|
this.assertSchema();
|
|
334
335
|
this.assertCollection(collection);
|
|
336
|
+
const schema = this.schema;
|
|
335
337
|
if (where) {
|
|
336
338
|
for (const key of Object.keys(where)) {
|
|
337
|
-
validateFieldName(collection, key,
|
|
339
|
+
validateFieldName(collection, key, schema);
|
|
338
340
|
}
|
|
339
341
|
}
|
|
340
342
|
const collectionMap = this.materializedRecords.get(collection);
|
|
@@ -370,7 +372,7 @@ var MemoryServerStore = class {
|
|
|
370
372
|
// Materialization internals
|
|
371
373
|
// ---------------------------------------------------------------------------
|
|
372
374
|
rebuildMaterializedRecord(collection, recordId) {
|
|
373
|
-
const collectionDef = this.schema
|
|
375
|
+
const collectionDef = this.schema?.collections[collection];
|
|
374
376
|
if (!collectionDef) return;
|
|
375
377
|
let collectionMap = this.materializedRecords.get(collection);
|
|
376
378
|
if (!collectionMap) {
|
|
@@ -378,8 +380,10 @@ var MemoryServerStore = class {
|
|
|
378
380
|
this.materializedRecords.set(collection, collectionMap);
|
|
379
381
|
}
|
|
380
382
|
const recordOps = this.operations.filter((op) => op.collection === collection && op.recordId === recordId).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 parsedOps = recordOps.map((op) => ({
|
|
@@ -431,8 +435,10 @@ var MemoryServerStore = class {
|
|
|
431
435
|
// ---------------------------------------------------------------------------
|
|
432
436
|
materializeFromOps(collection) {
|
|
433
437
|
const collectionOps = this.operations.filter((op) => op.collection === collection).sort((a, b) => {
|
|
434
|
-
if (a.timestamp.wallTime !== b.timestamp.wallTime)
|
|
435
|
-
|
|
438
|
+
if (a.timestamp.wallTime !== b.timestamp.wallTime)
|
|
439
|
+
return a.timestamp.wallTime - b.timestamp.wallTime;
|
|
440
|
+
if (a.timestamp.logical !== b.timestamp.logical)
|
|
441
|
+
return a.timestamp.logical - b.timestamp.logical;
|
|
436
442
|
return a.sequenceNumber - b.sequenceNumber;
|
|
437
443
|
});
|
|
438
444
|
const records = /* @__PURE__ */ new Map();
|
|
@@ -478,9 +484,10 @@ var MemoryServerStore = class {
|
|
|
478
484
|
}
|
|
479
485
|
}
|
|
480
486
|
assertCollection(collection) {
|
|
481
|
-
|
|
487
|
+
const schema = this.schema;
|
|
488
|
+
if (!schema.collections[collection]) {
|
|
482
489
|
throw new Error(
|
|
483
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
490
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
484
491
|
);
|
|
485
492
|
}
|
|
486
493
|
}
|
|
@@ -590,7 +597,7 @@ var PostgresServerStore = class {
|
|
|
590
597
|
lastSeenAt: import_drizzle_orm.sql`${now}`
|
|
591
598
|
}
|
|
592
599
|
});
|
|
593
|
-
if (this.schema
|
|
600
|
+
if (this.schema?.collections[op.collection]) {
|
|
594
601
|
await this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
|
|
595
602
|
}
|
|
596
603
|
});
|
|
@@ -604,10 +611,7 @@ var PostgresServerStore = class {
|
|
|
604
611
|
this.assertOpen();
|
|
605
612
|
await this.ready;
|
|
606
613
|
const rows = await this.db.select().from(pgOperations).where(
|
|
607
|
-
(0, import_drizzle_orm.and)(
|
|
608
|
-
(0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId),
|
|
609
|
-
(0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq)
|
|
610
|
-
)
|
|
614
|
+
(0, import_drizzle_orm.and)((0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId), (0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq))
|
|
611
615
|
).orderBy((0, import_drizzle_orm.asc)(pgOperations.sequenceNumber));
|
|
612
616
|
return rows.map((row) => this.deserializeOperation(row));
|
|
613
617
|
}
|
|
@@ -620,7 +624,7 @@ var PostgresServerStore = class {
|
|
|
620
624
|
async materializeCollection(collection) {
|
|
621
625
|
this.assertOpen();
|
|
622
626
|
await this.ready;
|
|
623
|
-
if (this.schema
|
|
627
|
+
if (this.schema?.collections[collection]) {
|
|
624
628
|
return this.queryCollection(collection);
|
|
625
629
|
}
|
|
626
630
|
return this.materializeFromOpsLog(collection);
|
|
@@ -630,14 +634,15 @@ var PostgresServerStore = class {
|
|
|
630
634
|
await this.ready;
|
|
631
635
|
this.assertSchema();
|
|
632
636
|
this.assertCollection(collection);
|
|
633
|
-
const
|
|
637
|
+
const schema = this.schema;
|
|
638
|
+
const collectionDef = schema.collections[collection];
|
|
634
639
|
if (options?.where) {
|
|
635
640
|
for (const key of Object.keys(options.where)) {
|
|
636
|
-
validateFieldName(collection, key,
|
|
641
|
+
validateFieldName(collection, key, schema);
|
|
637
642
|
}
|
|
638
643
|
}
|
|
639
644
|
if (options?.orderBy) {
|
|
640
|
-
validateFieldName(collection, options.orderBy,
|
|
645
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
641
646
|
}
|
|
642
647
|
const query = this.buildSelectQuery(collection, options);
|
|
643
648
|
const rows = await this.db.execute(query);
|
|
@@ -648,7 +653,8 @@ var PostgresServerStore = class {
|
|
|
648
653
|
await this.ready;
|
|
649
654
|
this.assertSchema();
|
|
650
655
|
this.assertCollection(collection);
|
|
651
|
-
const
|
|
656
|
+
const schema = this.schema;
|
|
657
|
+
const collectionDef = schema.collections[collection];
|
|
652
658
|
const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
653
659
|
const rows = await this.db.execute(query);
|
|
654
660
|
if (rows.length === 0) return null;
|
|
@@ -659,9 +665,10 @@ var PostgresServerStore = class {
|
|
|
659
665
|
await this.ready;
|
|
660
666
|
this.assertSchema();
|
|
661
667
|
this.assertCollection(collection);
|
|
668
|
+
const schema = this.schema;
|
|
662
669
|
if (where) {
|
|
663
670
|
for (const key of Object.keys(where)) {
|
|
664
|
-
validateFieldName(collection, key,
|
|
671
|
+
validateFieldName(collection, key, schema);
|
|
665
672
|
}
|
|
666
673
|
}
|
|
667
674
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
@@ -681,18 +688,13 @@ var PostgresServerStore = class {
|
|
|
681
688
|
* all operations for that record.
|
|
682
689
|
*/
|
|
683
690
|
async rebuildMaterializedRecord(txOrDb, collection, recordId) {
|
|
684
|
-
const collectionDef = this.schema
|
|
691
|
+
const collectionDef = this.schema?.collections[collection];
|
|
685
692
|
if (!collectionDef) return;
|
|
686
693
|
const ops = await txOrDb.select({
|
|
687
694
|
type: pgOperations.type,
|
|
688
695
|
data: pgOperations.data,
|
|
689
696
|
wallTime: pgOperations.wallTime
|
|
690
|
-
}).from(pgOperations).where(
|
|
691
|
-
(0, import_drizzle_orm.and)(
|
|
692
|
-
(0, import_drizzle_orm.eq)(pgOperations.collection, collection),
|
|
693
|
-
(0, import_drizzle_orm.eq)(pgOperations.recordId, recordId)
|
|
694
|
-
)
|
|
695
|
-
).orderBy(
|
|
697
|
+
}).from(pgOperations).where((0, import_drizzle_orm.and)((0, import_drizzle_orm.eq)(pgOperations.collection, collection), (0, import_drizzle_orm.eq)(pgOperations.recordId, recordId))).orderBy(
|
|
696
698
|
(0, import_drizzle_orm.asc)(pgOperations.wallTime),
|
|
697
699
|
(0, import_drizzle_orm.asc)(pgOperations.logical),
|
|
698
700
|
(0, import_drizzle_orm.asc)(pgOperations.sequenceNumber)
|
|
@@ -762,7 +764,7 @@ var PostgresServerStore = class {
|
|
|
762
764
|
* Backfill a single collection's materialized table from operations.
|
|
763
765
|
*/
|
|
764
766
|
async backfillCollection(collectionName) {
|
|
765
|
-
const collectionDef = this.schema
|
|
767
|
+
const collectionDef = this.schema?.collections[collectionName];
|
|
766
768
|
if (!collectionDef) return;
|
|
767
769
|
const allOps = await this.db.select({
|
|
768
770
|
recordId: pgOperations.recordId,
|
|
@@ -819,9 +821,7 @@ var PostgresServerStore = class {
|
|
|
819
821
|
options?.where ?? {},
|
|
820
822
|
options?.includeDeleted ?? false
|
|
821
823
|
);
|
|
822
|
-
const parts = [
|
|
823
|
-
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`
|
|
824
|
-
];
|
|
824
|
+
const parts = [import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`];
|
|
825
825
|
if (options?.orderBy) {
|
|
826
826
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
827
827
|
parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
@@ -934,12 +934,8 @@ var PostgresServerStore = class {
|
|
|
934
934
|
await this.db.execute(
|
|
935
935
|
import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
|
|
936
936
|
);
|
|
937
|
-
await this.db.execute(
|
|
938
|
-
|
|
939
|
-
);
|
|
940
|
-
await this.db.execute(
|
|
941
|
-
import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`
|
|
942
|
-
);
|
|
937
|
+
await this.db.execute(import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`);
|
|
938
|
+
await this.db.execute(import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`);
|
|
943
939
|
await this.db.execute(
|
|
944
940
|
import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection_record ON operations (collection, record_id)`
|
|
945
941
|
);
|
|
@@ -1007,9 +1003,10 @@ var PostgresServerStore = class {
|
|
|
1007
1003
|
}
|
|
1008
1004
|
}
|
|
1009
1005
|
assertCollection(collection) {
|
|
1010
|
-
|
|
1006
|
+
const schema = this.schema;
|
|
1007
|
+
if (!schema.collections[collection]) {
|
|
1011
1008
|
throw new Error(
|
|
1012
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
1009
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
1013
1010
|
);
|
|
1014
1011
|
}
|
|
1015
1012
|
}
|
|
@@ -1142,7 +1139,7 @@ var SqliteServerStore = class {
|
|
|
1142
1139
|
lastSeenAt: import_drizzle_orm2.sql`${now}`
|
|
1143
1140
|
}
|
|
1144
1141
|
}).run();
|
|
1145
|
-
if (this.schema
|
|
1142
|
+
if (this.schema?.collections[op.collection]) {
|
|
1146
1143
|
this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
|
|
1147
1144
|
}
|
|
1148
1145
|
return "applied";
|
|
@@ -1161,7 +1158,7 @@ var SqliteServerStore = class {
|
|
|
1161
1158
|
}
|
|
1162
1159
|
async materializeCollection(collection) {
|
|
1163
1160
|
this.assertOpen();
|
|
1164
|
-
if (this.schema
|
|
1161
|
+
if (this.schema?.collections[collection]) {
|
|
1165
1162
|
return this.queryCollection(collection);
|
|
1166
1163
|
}
|
|
1167
1164
|
return this.materializeFromOpsLog(collection);
|
|
@@ -1170,14 +1167,15 @@ var SqliteServerStore = class {
|
|
|
1170
1167
|
this.assertOpen();
|
|
1171
1168
|
this.assertSchema();
|
|
1172
1169
|
this.assertCollection(collection);
|
|
1173
|
-
const
|
|
1170
|
+
const schema = this.schema;
|
|
1171
|
+
const collectionDef = schema.collections[collection];
|
|
1174
1172
|
if (options?.where) {
|
|
1175
1173
|
for (const key of Object.keys(options.where)) {
|
|
1176
|
-
validateFieldName(collection, key,
|
|
1174
|
+
validateFieldName(collection, key, schema);
|
|
1177
1175
|
}
|
|
1178
1176
|
}
|
|
1179
1177
|
if (options?.orderBy) {
|
|
1180
|
-
validateFieldName(collection, options.orderBy,
|
|
1178
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
1181
1179
|
}
|
|
1182
1180
|
const query = this.buildSelectQuery(collection, options);
|
|
1183
1181
|
const rows = this.db.all(query);
|
|
@@ -1187,7 +1185,8 @@ var SqliteServerStore = class {
|
|
|
1187
1185
|
this.assertOpen();
|
|
1188
1186
|
this.assertSchema();
|
|
1189
1187
|
this.assertCollection(collection);
|
|
1190
|
-
const
|
|
1188
|
+
const schema = this.schema;
|
|
1189
|
+
const collectionDef = schema.collections[collection];
|
|
1191
1190
|
const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
1192
1191
|
const rows = this.db.all(query);
|
|
1193
1192
|
if (rows.length === 0) return null;
|
|
@@ -1197,9 +1196,10 @@ var SqliteServerStore = class {
|
|
|
1197
1196
|
this.assertOpen();
|
|
1198
1197
|
this.assertSchema();
|
|
1199
1198
|
this.assertCollection(collection);
|
|
1199
|
+
const schema = this.schema;
|
|
1200
1200
|
if (where) {
|
|
1201
1201
|
for (const key of Object.keys(where)) {
|
|
1202
|
-
validateFieldName(collection, key,
|
|
1202
|
+
validateFieldName(collection, key, schema);
|
|
1203
1203
|
}
|
|
1204
1204
|
}
|
|
1205
1205
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
@@ -1219,18 +1219,13 @@ var SqliteServerStore = class {
|
|
|
1219
1219
|
* transaction for atomic dual-write.
|
|
1220
1220
|
*/
|
|
1221
1221
|
rebuildMaterializedRecord(txOrDb, collection, recordId) {
|
|
1222
|
-
const collectionDef = this.schema
|
|
1222
|
+
const collectionDef = this.schema?.collections[collection];
|
|
1223
1223
|
if (!collectionDef) return;
|
|
1224
1224
|
const ops = txOrDb.select({
|
|
1225
1225
|
type: operations.type,
|
|
1226
1226
|
data: operations.data,
|
|
1227
1227
|
wallTime: operations.wallTime
|
|
1228
|
-
}).from(operations).where(
|
|
1229
|
-
(0, import_drizzle_orm2.and)(
|
|
1230
|
-
(0, import_drizzle_orm2.eq)(operations.collection, collection),
|
|
1231
|
-
(0, import_drizzle_orm2.eq)(operations.recordId, recordId)
|
|
1232
|
-
)
|
|
1233
|
-
).orderBy((0, import_drizzle_orm2.asc)(operations.wallTime), (0, import_drizzle_orm2.asc)(operations.logical), (0, import_drizzle_orm2.asc)(operations.sequenceNumber)).all();
|
|
1228
|
+
}).from(operations).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(operations.collection, collection), (0, import_drizzle_orm2.eq)(operations.recordId, recordId))).orderBy((0, import_drizzle_orm2.asc)(operations.wallTime), (0, import_drizzle_orm2.asc)(operations.logical), (0, import_drizzle_orm2.asc)(operations.sequenceNumber)).all();
|
|
1234
1229
|
const parsedOps = ops.map((op) => ({
|
|
1235
1230
|
type: op.type,
|
|
1236
1231
|
data: op.data !== null ? JSON.parse(op.data) : null
|
|
@@ -1299,7 +1294,7 @@ var SqliteServerStore = class {
|
|
|
1299
1294
|
* Backfill a single collection's materialized table from operations.
|
|
1300
1295
|
*/
|
|
1301
1296
|
backfillCollection(collectionName) {
|
|
1302
|
-
const collectionDef = this.schema
|
|
1297
|
+
const collectionDef = this.schema?.collections[collectionName];
|
|
1303
1298
|
if (!collectionDef) return;
|
|
1304
1299
|
const allOps = this.db.select({
|
|
1305
1300
|
recordId: operations.recordId,
|
|
@@ -1354,9 +1349,7 @@ var SqliteServerStore = class {
|
|
|
1354
1349
|
options?.where ?? {},
|
|
1355
1350
|
options?.includeDeleted ?? false
|
|
1356
1351
|
);
|
|
1357
|
-
const parts = [
|
|
1358
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`
|
|
1359
|
-
];
|
|
1352
|
+
const parts = [import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`];
|
|
1360
1353
|
if (options?.orderBy) {
|
|
1361
1354
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
1362
1355
|
parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
@@ -1531,9 +1524,10 @@ var SqliteServerStore = class {
|
|
|
1531
1524
|
}
|
|
1532
1525
|
}
|
|
1533
1526
|
assertCollection(collection) {
|
|
1534
|
-
|
|
1527
|
+
const schema = this.schema;
|
|
1528
|
+
if (!schema.collections[collection]) {
|
|
1535
1529
|
throw new Error(
|
|
1536
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
1530
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
1537
1531
|
);
|
|
1538
1532
|
}
|
|
1539
1533
|
}
|
|
@@ -1743,6 +1737,7 @@ var ClientSession = class {
|
|
|
1743
1737
|
batchSize;
|
|
1744
1738
|
schemaVersion;
|
|
1745
1739
|
onRelay;
|
|
1740
|
+
onAwarenessUpdate;
|
|
1746
1741
|
onClose;
|
|
1747
1742
|
constructor(options) {
|
|
1748
1743
|
this.sessionId = options.sessionId;
|
|
@@ -1754,6 +1749,7 @@ var ClientSession = class {
|
|
|
1754
1749
|
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
1755
1750
|
this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
1756
1751
|
this.onRelay = options.onRelay ?? null;
|
|
1752
|
+
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
1757
1753
|
this.onClose = options.onClose ?? null;
|
|
1758
1754
|
}
|
|
1759
1755
|
/**
|
|
@@ -1816,6 +1812,13 @@ var ClientSession = class {
|
|
|
1816
1812
|
isStreaming() {
|
|
1817
1813
|
return this.state === "streaming";
|
|
1818
1814
|
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Get the transport for this session.
|
|
1817
|
+
* Used by the awareness relay to send messages to this client.
|
|
1818
|
+
*/
|
|
1819
|
+
getTransport() {
|
|
1820
|
+
return this.transport;
|
|
1821
|
+
}
|
|
1819
1822
|
// --- Private protocol handlers ---
|
|
1820
1823
|
handleMessage(message) {
|
|
1821
1824
|
switch (message.type) {
|
|
@@ -1830,6 +1833,9 @@ var ClientSession = class {
|
|
|
1830
1833
|
break;
|
|
1831
1834
|
case "error":
|
|
1832
1835
|
break;
|
|
1836
|
+
case "awareness-update":
|
|
1837
|
+
this.handleAwarenessUpdate(message);
|
|
1838
|
+
break;
|
|
1833
1839
|
}
|
|
1834
1840
|
}
|
|
1835
1841
|
async handleHandshake(msg) {
|
|
@@ -1849,6 +1855,19 @@ var ClientSession = class {
|
|
|
1849
1855
|
this.authContext = context;
|
|
1850
1856
|
this.state = "authenticated";
|
|
1851
1857
|
}
|
|
1858
|
+
if (msg.syncScope) {
|
|
1859
|
+
const mergedScopes = { ...msg.syncScope };
|
|
1860
|
+
if (this.authContext?.scopes) {
|
|
1861
|
+
for (const [collection, authScope] of Object.entries(this.authContext.scopes)) {
|
|
1862
|
+
mergedScopes[collection] = { ...mergedScopes[collection] ?? {}, ...authScope };
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (this.authContext) {
|
|
1866
|
+
this.authContext = { ...this.authContext, scopes: mergedScopes };
|
|
1867
|
+
} else {
|
|
1868
|
+
this.authContext = { userId: msg.nodeId, scopes: mergedScopes };
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1852
1871
|
const serverVector = this.store.getVersionVector();
|
|
1853
1872
|
const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
|
|
1854
1873
|
this.setSerializerWireFormat(selectedWireFormat);
|
|
@@ -1859,7 +1878,10 @@ var ClientSession = class {
|
|
|
1859
1878
|
versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
|
|
1860
1879
|
schemaVersion: this.schemaVersion,
|
|
1861
1880
|
accepted: true,
|
|
1862
|
-
selectedWireFormat
|
|
1881
|
+
selectedWireFormat,
|
|
1882
|
+
// Confirm the accepted scope so the client knows what data will be synced.
|
|
1883
|
+
// This may differ from what the client requested if auth scopes are narrower.
|
|
1884
|
+
...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
|
|
1863
1885
|
};
|
|
1864
1886
|
this.transport.send(response);
|
|
1865
1887
|
this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
|
|
@@ -1871,8 +1893,10 @@ var ClientSession = class {
|
|
|
1871
1893
|
async handleOperationBatch(msg) {
|
|
1872
1894
|
const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
1873
1895
|
const applied = [];
|
|
1896
|
+
const rejected = [];
|
|
1874
1897
|
for (const op of operations2) {
|
|
1875
1898
|
if (!operationMatchesScopes(op, this.authContext?.scopes)) {
|
|
1899
|
+
rejected.push(op);
|
|
1876
1900
|
continue;
|
|
1877
1901
|
}
|
|
1878
1902
|
const result = await this.store.applyRemoteOperation(op);
|
|
@@ -1880,6 +1904,15 @@ var ClientSession = class {
|
|
|
1880
1904
|
applied.push(op);
|
|
1881
1905
|
}
|
|
1882
1906
|
}
|
|
1907
|
+
if (rejected.length > 0) {
|
|
1908
|
+
for (const op of rejected) {
|
|
1909
|
+
this.sendError(
|
|
1910
|
+
"SCOPE_VIOLATION",
|
|
1911
|
+
`Operation "${op.id}" in collection "${op.collection}" is outside the client's sync scope`,
|
|
1912
|
+
false
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1883
1916
|
if (operations2.length > 0) {
|
|
1884
1917
|
this.emitter?.emit({
|
|
1885
1918
|
type: "sync:received",
|
|
@@ -1942,6 +1975,9 @@ var ClientSession = class {
|
|
|
1942
1975
|
});
|
|
1943
1976
|
}
|
|
1944
1977
|
}
|
|
1978
|
+
handleAwarenessUpdate(msg) {
|
|
1979
|
+
this.onAwarenessUpdate?.(this.sessionId, msg);
|
|
1980
|
+
}
|
|
1945
1981
|
sendError(code, message, retriable) {
|
|
1946
1982
|
const errorMsg = {
|
|
1947
1983
|
type: "error",
|
|
@@ -1972,8 +2008,107 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
1972
2008
|
}
|
|
1973
2009
|
|
|
1974
2010
|
// src/server/kora-sync-server.ts
|
|
1975
|
-
var
|
|
2011
|
+
var import_core8 = require("@korajs/core");
|
|
1976
2012
|
var import_sync3 = require("@korajs/sync");
|
|
2013
|
+
|
|
2014
|
+
// src/awareness/awareness-relay.ts
|
|
2015
|
+
var import_core7 = require("@korajs/core");
|
|
2016
|
+
var AwarenessRelay = class {
|
|
2017
|
+
clients = /* @__PURE__ */ new Map();
|
|
2018
|
+
/**
|
|
2019
|
+
* Register a client for awareness broadcasting.
|
|
2020
|
+
*
|
|
2021
|
+
* @param sessionId - Unique session identifier
|
|
2022
|
+
* @param clientId - Client-assigned awareness ID
|
|
2023
|
+
* @param transport - Transport for sending messages to this client
|
|
2024
|
+
*/
|
|
2025
|
+
addClient(sessionId, clientId, transport) {
|
|
2026
|
+
this.clients.set(sessionId, {
|
|
2027
|
+
sessionId,
|
|
2028
|
+
clientId,
|
|
2029
|
+
transport,
|
|
2030
|
+
state: null
|
|
2031
|
+
});
|
|
2032
|
+
const existingStates = {};
|
|
2033
|
+
let hasStates = false;
|
|
2034
|
+
for (const [, client] of this.clients) {
|
|
2035
|
+
if (client.sessionId === sessionId) continue;
|
|
2036
|
+
if (client.state) {
|
|
2037
|
+
existingStates[String(client.clientId)] = client.state;
|
|
2038
|
+
hasStates = true;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (hasStates) {
|
|
2042
|
+
const catchUpMsg = {
|
|
2043
|
+
type: "awareness-update",
|
|
2044
|
+
messageId: (0, import_core7.generateUUIDv7)(),
|
|
2045
|
+
clientId: 0,
|
|
2046
|
+
// Server-sourced
|
|
2047
|
+
states: existingStates
|
|
2048
|
+
};
|
|
2049
|
+
transport.send(catchUpMsg);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Remove a client and broadcast its removal to all remaining clients.
|
|
2054
|
+
*
|
|
2055
|
+
* @param sessionId - Session ID of the disconnecting client
|
|
2056
|
+
*/
|
|
2057
|
+
removeClient(sessionId) {
|
|
2058
|
+
const client = this.clients.get(sessionId);
|
|
2059
|
+
if (!client) return;
|
|
2060
|
+
this.clients.delete(sessionId);
|
|
2061
|
+
if (client.state === null) return;
|
|
2062
|
+
const removalStates = {
|
|
2063
|
+
[String(client.clientId)]: null
|
|
2064
|
+
};
|
|
2065
|
+
const msg = {
|
|
2066
|
+
type: "awareness-update",
|
|
2067
|
+
messageId: (0, import_core7.generateUUIDv7)(),
|
|
2068
|
+
clientId: client.clientId,
|
|
2069
|
+
states: removalStates
|
|
2070
|
+
};
|
|
2071
|
+
this.broadcastExcept(sessionId, msg);
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Handle an incoming awareness update from a client.
|
|
2075
|
+
* Stores the state and relays to all other connected clients.
|
|
2076
|
+
*
|
|
2077
|
+
* @param sessionId - Session ID of the sending client
|
|
2078
|
+
* @param message - The awareness update message
|
|
2079
|
+
*/
|
|
2080
|
+
handleUpdate(sessionId, message) {
|
|
2081
|
+
const sender = this.clients.get(sessionId);
|
|
2082
|
+
if (!sender) return;
|
|
2083
|
+
const senderState = message.states[String(message.clientId)];
|
|
2084
|
+
if (senderState !== void 0) {
|
|
2085
|
+
sender.state = senderState;
|
|
2086
|
+
}
|
|
2087
|
+
this.broadcastExcept(sessionId, message);
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Get the number of registered awareness clients.
|
|
2091
|
+
*/
|
|
2092
|
+
getClientCount() {
|
|
2093
|
+
return this.clients.size;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Remove all clients and clear all state.
|
|
2097
|
+
*/
|
|
2098
|
+
clear() {
|
|
2099
|
+
this.clients.clear();
|
|
2100
|
+
}
|
|
2101
|
+
// --- Private ---
|
|
2102
|
+
broadcastExcept(excludeSessionId, message) {
|
|
2103
|
+
for (const [, client] of this.clients) {
|
|
2104
|
+
if (client.sessionId === excludeSessionId) continue;
|
|
2105
|
+
if (!client.transport.isConnected()) continue;
|
|
2106
|
+
client.transport.send(message);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
|
|
2111
|
+
// src/server/kora-sync-server.ts
|
|
1977
2112
|
var DEFAULT_MAX_CONNECTIONS = 0;
|
|
1978
2113
|
var DEFAULT_BATCH_SIZE2 = 100;
|
|
1979
2114
|
var DEFAULT_SCHEMA_VERSION2 = 1;
|
|
@@ -1990,6 +2125,7 @@ var KoraSyncServer = class {
|
|
|
1990
2125
|
port;
|
|
1991
2126
|
host;
|
|
1992
2127
|
path;
|
|
2128
|
+
awarenessRelay = new AwarenessRelay();
|
|
1993
2129
|
sessions = /* @__PURE__ */ new Map();
|
|
1994
2130
|
httpClients = /* @__PURE__ */ new Map();
|
|
1995
2131
|
httpSessionToClient = /* @__PURE__ */ new Map();
|
|
@@ -2014,10 +2150,10 @@ var KoraSyncServer = class {
|
|
|
2014
2150
|
*/
|
|
2015
2151
|
async start(wsServerImpl) {
|
|
2016
2152
|
if (this.running) {
|
|
2017
|
-
throw new
|
|
2153
|
+
throw new import_core8.SyncError("Server is already running", { port: this.port });
|
|
2018
2154
|
}
|
|
2019
2155
|
if (!wsServerImpl && this.port === void 0) {
|
|
2020
|
-
throw new
|
|
2156
|
+
throw new import_core8.SyncError(
|
|
2021
2157
|
"Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
|
|
2022
2158
|
{}
|
|
2023
2159
|
);
|
|
@@ -2051,6 +2187,7 @@ var KoraSyncServer = class {
|
|
|
2051
2187
|
* Stop the server. Closes all sessions and the WebSocket server.
|
|
2052
2188
|
*/
|
|
2053
2189
|
async stop() {
|
|
2190
|
+
this.awarenessRelay.clear();
|
|
2054
2191
|
for (const session of this.sessions.values()) {
|
|
2055
2192
|
session.close("server shutting down");
|
|
2056
2193
|
}
|
|
@@ -2107,18 +2244,18 @@ var KoraSyncServer = class {
|
|
|
2107
2244
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
2108
2245
|
transport.send({
|
|
2109
2246
|
type: "error",
|
|
2110
|
-
messageId: (0,
|
|
2247
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
2111
2248
|
code: "MAX_CONNECTIONS",
|
|
2112
2249
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
2113
2250
|
retriable: true
|
|
2114
2251
|
});
|
|
2115
2252
|
transport.close(4029, "max connections reached");
|
|
2116
|
-
throw new
|
|
2253
|
+
throw new import_core8.SyncError("Maximum connections reached", {
|
|
2117
2254
|
current: this.sessions.size,
|
|
2118
2255
|
max: this.maxConnections
|
|
2119
2256
|
});
|
|
2120
2257
|
}
|
|
2121
|
-
const sessionId = (0,
|
|
2258
|
+
const sessionId = (0, import_core8.generateUUIDv7)();
|
|
2122
2259
|
const session = new ClientSession({
|
|
2123
2260
|
sessionId,
|
|
2124
2261
|
transport,
|
|
@@ -2131,6 +2268,9 @@ var KoraSyncServer = class {
|
|
|
2131
2268
|
onRelay: (sourceSessionId, operations2) => {
|
|
2132
2269
|
this.handleRelay(sourceSessionId, operations2);
|
|
2133
2270
|
},
|
|
2271
|
+
onAwarenessUpdate: (sourceSessionId, message) => {
|
|
2272
|
+
this.handleAwarenessRelay(sourceSessionId, message);
|
|
2273
|
+
},
|
|
2134
2274
|
onClose: (sid) => {
|
|
2135
2275
|
this.handleSessionClose(sid);
|
|
2136
2276
|
}
|
|
@@ -2164,6 +2304,7 @@ var KoraSyncServer = class {
|
|
|
2164
2304
|
}
|
|
2165
2305
|
}
|
|
2166
2306
|
handleSessionClose(sessionId) {
|
|
2307
|
+
this.awarenessRelay.removeClient(sessionId);
|
|
2167
2308
|
this.sessions.delete(sessionId);
|
|
2168
2309
|
const clientId = this.httpSessionToClient.get(sessionId);
|
|
2169
2310
|
if (clientId) {
|
|
@@ -2171,6 +2312,15 @@ var KoraSyncServer = class {
|
|
|
2171
2312
|
this.httpClients.delete(clientId);
|
|
2172
2313
|
}
|
|
2173
2314
|
}
|
|
2315
|
+
handleAwarenessRelay(sourceSessionId, message) {
|
|
2316
|
+
const session = this.sessions.get(sourceSessionId);
|
|
2317
|
+
if (!session) return;
|
|
2318
|
+
const transport = session.getTransport();
|
|
2319
|
+
if (!this.awarenessRelay.getClientCount() || !transport) {
|
|
2320
|
+
}
|
|
2321
|
+
this.awarenessRelay.addClient(sourceSessionId, message.clientId, transport);
|
|
2322
|
+
this.awarenessRelay.handleUpdate(sourceSessionId, message);
|
|
2323
|
+
}
|
|
2174
2324
|
getOrCreateHttpClient(clientId) {
|
|
2175
2325
|
const existing = this.httpClients.get(clientId);
|
|
2176
2326
|
if (existing) {
|
|
@@ -2252,6 +2402,30 @@ var KoraAuthProvider = class {
|
|
|
2252
2402
|
}
|
|
2253
2403
|
};
|
|
2254
2404
|
|
|
2405
|
+
// src/auth/mixed-auth-provider.ts
|
|
2406
|
+
var MixedAuthProvider = class {
|
|
2407
|
+
primary;
|
|
2408
|
+
anonymousScopes;
|
|
2409
|
+
anonymousPrefix;
|
|
2410
|
+
anonymousCounter = 0;
|
|
2411
|
+
constructor(options) {
|
|
2412
|
+
this.primary = options.primary;
|
|
2413
|
+
this.anonymousScopes = options.anonymousScopes;
|
|
2414
|
+
this.anonymousPrefix = options.anonymousPrefix ?? "anon";
|
|
2415
|
+
}
|
|
2416
|
+
async authenticate(token) {
|
|
2417
|
+
if (token) {
|
|
2418
|
+
const ctx = await this.primary.authenticate(token);
|
|
2419
|
+
if (ctx) return ctx;
|
|
2420
|
+
}
|
|
2421
|
+
this.anonymousCounter++;
|
|
2422
|
+
return {
|
|
2423
|
+
userId: `${this.anonymousPrefix}-${Date.now()}-${this.anonymousCounter}`,
|
|
2424
|
+
scopes: this.anonymousScopes
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
|
|
2255
2429
|
// src/server/create-server.ts
|
|
2256
2430
|
function createKoraServer(config) {
|
|
2257
2431
|
return new KoraSyncServer(config);
|
|
@@ -2340,7 +2514,7 @@ function createProductionServer(config) {
|
|
|
2340
2514
|
}
|
|
2341
2515
|
});
|
|
2342
2516
|
return new Promise((resolve2) => {
|
|
2343
|
-
httpServer
|
|
2517
|
+
httpServer?.listen(port, "0.0.0.0", () => {
|
|
2344
2518
|
resolve2(`http://localhost:${port}`);
|
|
2345
2519
|
});
|
|
2346
2520
|
});
|
|
@@ -2349,7 +2523,7 @@ function createProductionServer(config) {
|
|
|
2349
2523
|
await syncServer.stop();
|
|
2350
2524
|
if (httpServer) {
|
|
2351
2525
|
await new Promise((resolve) => {
|
|
2352
|
-
httpServer
|
|
2526
|
+
httpServer?.close(() => resolve());
|
|
2353
2527
|
});
|
|
2354
2528
|
httpServer = null;
|
|
2355
2529
|
}
|
|
@@ -2358,11 +2532,13 @@ function createProductionServer(config) {
|
|
|
2358
2532
|
}
|
|
2359
2533
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2360
2534
|
0 && (module.exports = {
|
|
2535
|
+
AwarenessRelay,
|
|
2361
2536
|
ClientSession,
|
|
2362
2537
|
HttpServerTransport,
|
|
2363
2538
|
KoraAuthProvider,
|
|
2364
2539
|
KoraSyncServer,
|
|
2365
2540
|
MemoryServerStore,
|
|
2541
|
+
MixedAuthProvider,
|
|
2366
2542
|
NoAuthProvider,
|
|
2367
2543
|
PostgresServerStore,
|
|
2368
2544
|
SqliteServerStore,
|