@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.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
|
}
|
|
@@ -556,7 +563,9 @@ var PostgresServerStore = class {
|
|
|
556
563
|
try {
|
|
557
564
|
await this.db.execute(import_drizzle_orm.sql.raw(alterSql));
|
|
558
565
|
} catch (e) {
|
|
559
|
-
|
|
566
|
+
const msg = e instanceof Error ? e.message : "";
|
|
567
|
+
const causeMsg = e instanceof Error && e.cause instanceof Error ? e.cause.message : "";
|
|
568
|
+
if (!msg.includes("already exists") && !msg.includes("duplicate column") && !causeMsg.includes("already exists") && !causeMsg.includes("duplicate column")) {
|
|
560
569
|
throw e;
|
|
561
570
|
}
|
|
562
571
|
}
|
|
@@ -588,7 +597,7 @@ var PostgresServerStore = class {
|
|
|
588
597
|
lastSeenAt: import_drizzle_orm.sql`${now}`
|
|
589
598
|
}
|
|
590
599
|
});
|
|
591
|
-
if (this.schema
|
|
600
|
+
if (this.schema?.collections[op.collection]) {
|
|
592
601
|
await this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
|
|
593
602
|
}
|
|
594
603
|
});
|
|
@@ -602,10 +611,7 @@ var PostgresServerStore = class {
|
|
|
602
611
|
this.assertOpen();
|
|
603
612
|
await this.ready;
|
|
604
613
|
const rows = await this.db.select().from(pgOperations).where(
|
|
605
|
-
(0, import_drizzle_orm.and)(
|
|
606
|
-
(0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId),
|
|
607
|
-
(0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq)
|
|
608
|
-
)
|
|
614
|
+
(0, import_drizzle_orm.and)((0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId), (0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq))
|
|
609
615
|
).orderBy((0, import_drizzle_orm.asc)(pgOperations.sequenceNumber));
|
|
610
616
|
return rows.map((row) => this.deserializeOperation(row));
|
|
611
617
|
}
|
|
@@ -618,7 +624,7 @@ var PostgresServerStore = class {
|
|
|
618
624
|
async materializeCollection(collection) {
|
|
619
625
|
this.assertOpen();
|
|
620
626
|
await this.ready;
|
|
621
|
-
if (this.schema
|
|
627
|
+
if (this.schema?.collections[collection]) {
|
|
622
628
|
return this.queryCollection(collection);
|
|
623
629
|
}
|
|
624
630
|
return this.materializeFromOpsLog(collection);
|
|
@@ -628,14 +634,15 @@ var PostgresServerStore = class {
|
|
|
628
634
|
await this.ready;
|
|
629
635
|
this.assertSchema();
|
|
630
636
|
this.assertCollection(collection);
|
|
631
|
-
const
|
|
637
|
+
const schema = this.schema;
|
|
638
|
+
const collectionDef = schema.collections[collection];
|
|
632
639
|
if (options?.where) {
|
|
633
640
|
for (const key of Object.keys(options.where)) {
|
|
634
|
-
validateFieldName(collection, key,
|
|
641
|
+
validateFieldName(collection, key, schema);
|
|
635
642
|
}
|
|
636
643
|
}
|
|
637
644
|
if (options?.orderBy) {
|
|
638
|
-
validateFieldName(collection, options.orderBy,
|
|
645
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
639
646
|
}
|
|
640
647
|
const query = this.buildSelectQuery(collection, options);
|
|
641
648
|
const rows = await this.db.execute(query);
|
|
@@ -646,7 +653,8 @@ var PostgresServerStore = class {
|
|
|
646
653
|
await this.ready;
|
|
647
654
|
this.assertSchema();
|
|
648
655
|
this.assertCollection(collection);
|
|
649
|
-
const
|
|
656
|
+
const schema = this.schema;
|
|
657
|
+
const collectionDef = schema.collections[collection];
|
|
650
658
|
const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
651
659
|
const rows = await this.db.execute(query);
|
|
652
660
|
if (rows.length === 0) return null;
|
|
@@ -657,9 +665,10 @@ var PostgresServerStore = class {
|
|
|
657
665
|
await this.ready;
|
|
658
666
|
this.assertSchema();
|
|
659
667
|
this.assertCollection(collection);
|
|
668
|
+
const schema = this.schema;
|
|
660
669
|
if (where) {
|
|
661
670
|
for (const key of Object.keys(where)) {
|
|
662
|
-
validateFieldName(collection, key,
|
|
671
|
+
validateFieldName(collection, key, schema);
|
|
663
672
|
}
|
|
664
673
|
}
|
|
665
674
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
@@ -679,18 +688,13 @@ var PostgresServerStore = class {
|
|
|
679
688
|
* all operations for that record.
|
|
680
689
|
*/
|
|
681
690
|
async rebuildMaterializedRecord(txOrDb, collection, recordId) {
|
|
682
|
-
const collectionDef = this.schema
|
|
691
|
+
const collectionDef = this.schema?.collections[collection];
|
|
683
692
|
if (!collectionDef) return;
|
|
684
693
|
const ops = await txOrDb.select({
|
|
685
694
|
type: pgOperations.type,
|
|
686
695
|
data: pgOperations.data,
|
|
687
696
|
wallTime: pgOperations.wallTime
|
|
688
|
-
}).from(pgOperations).where(
|
|
689
|
-
(0, import_drizzle_orm.and)(
|
|
690
|
-
(0, import_drizzle_orm.eq)(pgOperations.collection, collection),
|
|
691
|
-
(0, import_drizzle_orm.eq)(pgOperations.recordId, recordId)
|
|
692
|
-
)
|
|
693
|
-
).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(
|
|
694
698
|
(0, import_drizzle_orm.asc)(pgOperations.wallTime),
|
|
695
699
|
(0, import_drizzle_orm.asc)(pgOperations.logical),
|
|
696
700
|
(0, import_drizzle_orm.asc)(pgOperations.sequenceNumber)
|
|
@@ -760,7 +764,7 @@ var PostgresServerStore = class {
|
|
|
760
764
|
* Backfill a single collection's materialized table from operations.
|
|
761
765
|
*/
|
|
762
766
|
async backfillCollection(collectionName) {
|
|
763
|
-
const collectionDef = this.schema
|
|
767
|
+
const collectionDef = this.schema?.collections[collectionName];
|
|
764
768
|
if (!collectionDef) return;
|
|
765
769
|
const allOps = await this.db.select({
|
|
766
770
|
recordId: pgOperations.recordId,
|
|
@@ -817,9 +821,7 @@ var PostgresServerStore = class {
|
|
|
817
821
|
options?.where ?? {},
|
|
818
822
|
options?.includeDeleted ?? false
|
|
819
823
|
);
|
|
820
|
-
const parts = [
|
|
821
|
-
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`
|
|
822
|
-
];
|
|
824
|
+
const parts = [import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`];
|
|
823
825
|
if (options?.orderBy) {
|
|
824
826
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
825
827
|
parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
@@ -932,12 +934,8 @@ var PostgresServerStore = class {
|
|
|
932
934
|
await this.db.execute(
|
|
933
935
|
import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
|
|
934
936
|
);
|
|
935
|
-
await this.db.execute(
|
|
936
|
-
|
|
937
|
-
);
|
|
938
|
-
await this.db.execute(
|
|
939
|
-
import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`
|
|
940
|
-
);
|
|
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)`);
|
|
941
939
|
await this.db.execute(
|
|
942
940
|
import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection_record ON operations (collection, record_id)`
|
|
943
941
|
);
|
|
@@ -1005,9 +1003,10 @@ var PostgresServerStore = class {
|
|
|
1005
1003
|
}
|
|
1006
1004
|
}
|
|
1007
1005
|
assertCollection(collection) {
|
|
1008
|
-
|
|
1006
|
+
const schema = this.schema;
|
|
1007
|
+
if (!schema.collections[collection]) {
|
|
1009
1008
|
throw new Error(
|
|
1010
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
1009
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
1011
1010
|
);
|
|
1012
1011
|
}
|
|
1013
1012
|
}
|
|
@@ -1140,7 +1139,7 @@ var SqliteServerStore = class {
|
|
|
1140
1139
|
lastSeenAt: import_drizzle_orm2.sql`${now}`
|
|
1141
1140
|
}
|
|
1142
1141
|
}).run();
|
|
1143
|
-
if (this.schema
|
|
1142
|
+
if (this.schema?.collections[op.collection]) {
|
|
1144
1143
|
this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
|
|
1145
1144
|
}
|
|
1146
1145
|
return "applied";
|
|
@@ -1159,7 +1158,7 @@ var SqliteServerStore = class {
|
|
|
1159
1158
|
}
|
|
1160
1159
|
async materializeCollection(collection) {
|
|
1161
1160
|
this.assertOpen();
|
|
1162
|
-
if (this.schema
|
|
1161
|
+
if (this.schema?.collections[collection]) {
|
|
1163
1162
|
return this.queryCollection(collection);
|
|
1164
1163
|
}
|
|
1165
1164
|
return this.materializeFromOpsLog(collection);
|
|
@@ -1168,14 +1167,15 @@ var SqliteServerStore = class {
|
|
|
1168
1167
|
this.assertOpen();
|
|
1169
1168
|
this.assertSchema();
|
|
1170
1169
|
this.assertCollection(collection);
|
|
1171
|
-
const
|
|
1170
|
+
const schema = this.schema;
|
|
1171
|
+
const collectionDef = schema.collections[collection];
|
|
1172
1172
|
if (options?.where) {
|
|
1173
1173
|
for (const key of Object.keys(options.where)) {
|
|
1174
|
-
validateFieldName(collection, key,
|
|
1174
|
+
validateFieldName(collection, key, schema);
|
|
1175
1175
|
}
|
|
1176
1176
|
}
|
|
1177
1177
|
if (options?.orderBy) {
|
|
1178
|
-
validateFieldName(collection, options.orderBy,
|
|
1178
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
1179
1179
|
}
|
|
1180
1180
|
const query = this.buildSelectQuery(collection, options);
|
|
1181
1181
|
const rows = this.db.all(query);
|
|
@@ -1185,7 +1185,8 @@ var SqliteServerStore = class {
|
|
|
1185
1185
|
this.assertOpen();
|
|
1186
1186
|
this.assertSchema();
|
|
1187
1187
|
this.assertCollection(collection);
|
|
1188
|
-
const
|
|
1188
|
+
const schema = this.schema;
|
|
1189
|
+
const collectionDef = schema.collections[collection];
|
|
1189
1190
|
const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
1190
1191
|
const rows = this.db.all(query);
|
|
1191
1192
|
if (rows.length === 0) return null;
|
|
@@ -1195,9 +1196,10 @@ var SqliteServerStore = class {
|
|
|
1195
1196
|
this.assertOpen();
|
|
1196
1197
|
this.assertSchema();
|
|
1197
1198
|
this.assertCollection(collection);
|
|
1199
|
+
const schema = this.schema;
|
|
1198
1200
|
if (where) {
|
|
1199
1201
|
for (const key of Object.keys(where)) {
|
|
1200
|
-
validateFieldName(collection, key,
|
|
1202
|
+
validateFieldName(collection, key, schema);
|
|
1201
1203
|
}
|
|
1202
1204
|
}
|
|
1203
1205
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
@@ -1217,18 +1219,13 @@ var SqliteServerStore = class {
|
|
|
1217
1219
|
* transaction for atomic dual-write.
|
|
1218
1220
|
*/
|
|
1219
1221
|
rebuildMaterializedRecord(txOrDb, collection, recordId) {
|
|
1220
|
-
const collectionDef = this.schema
|
|
1222
|
+
const collectionDef = this.schema?.collections[collection];
|
|
1221
1223
|
if (!collectionDef) return;
|
|
1222
1224
|
const ops = txOrDb.select({
|
|
1223
1225
|
type: operations.type,
|
|
1224
1226
|
data: operations.data,
|
|
1225
1227
|
wallTime: operations.wallTime
|
|
1226
|
-
}).from(operations).where(
|
|
1227
|
-
(0, import_drizzle_orm2.and)(
|
|
1228
|
-
(0, import_drizzle_orm2.eq)(operations.collection, collection),
|
|
1229
|
-
(0, import_drizzle_orm2.eq)(operations.recordId, recordId)
|
|
1230
|
-
)
|
|
1231
|
-
).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();
|
|
1232
1229
|
const parsedOps = ops.map((op) => ({
|
|
1233
1230
|
type: op.type,
|
|
1234
1231
|
data: op.data !== null ? JSON.parse(op.data) : null
|
|
@@ -1297,7 +1294,7 @@ var SqliteServerStore = class {
|
|
|
1297
1294
|
* Backfill a single collection's materialized table from operations.
|
|
1298
1295
|
*/
|
|
1299
1296
|
backfillCollection(collectionName) {
|
|
1300
|
-
const collectionDef = this.schema
|
|
1297
|
+
const collectionDef = this.schema?.collections[collectionName];
|
|
1301
1298
|
if (!collectionDef) return;
|
|
1302
1299
|
const allOps = this.db.select({
|
|
1303
1300
|
recordId: operations.recordId,
|
|
@@ -1352,9 +1349,7 @@ var SqliteServerStore = class {
|
|
|
1352
1349
|
options?.where ?? {},
|
|
1353
1350
|
options?.includeDeleted ?? false
|
|
1354
1351
|
);
|
|
1355
|
-
const parts = [
|
|
1356
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`
|
|
1357
|
-
];
|
|
1352
|
+
const parts = [import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`];
|
|
1358
1353
|
if (options?.orderBy) {
|
|
1359
1354
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
1360
1355
|
parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
@@ -1529,9 +1524,10 @@ var SqliteServerStore = class {
|
|
|
1529
1524
|
}
|
|
1530
1525
|
}
|
|
1531
1526
|
assertCollection(collection) {
|
|
1532
|
-
|
|
1527
|
+
const schema = this.schema;
|
|
1528
|
+
if (!schema.collections[collection]) {
|
|
1533
1529
|
throw new Error(
|
|
1534
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
1530
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
1535
1531
|
);
|
|
1536
1532
|
}
|
|
1537
1533
|
}
|
|
@@ -1741,6 +1737,7 @@ var ClientSession = class {
|
|
|
1741
1737
|
batchSize;
|
|
1742
1738
|
schemaVersion;
|
|
1743
1739
|
onRelay;
|
|
1740
|
+
onAwarenessUpdate;
|
|
1744
1741
|
onClose;
|
|
1745
1742
|
constructor(options) {
|
|
1746
1743
|
this.sessionId = options.sessionId;
|
|
@@ -1752,6 +1749,7 @@ var ClientSession = class {
|
|
|
1752
1749
|
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
1753
1750
|
this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
1754
1751
|
this.onRelay = options.onRelay ?? null;
|
|
1752
|
+
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
1755
1753
|
this.onClose = options.onClose ?? null;
|
|
1756
1754
|
}
|
|
1757
1755
|
/**
|
|
@@ -1814,6 +1812,13 @@ var ClientSession = class {
|
|
|
1814
1812
|
isStreaming() {
|
|
1815
1813
|
return this.state === "streaming";
|
|
1816
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
|
+
}
|
|
1817
1822
|
// --- Private protocol handlers ---
|
|
1818
1823
|
handleMessage(message) {
|
|
1819
1824
|
switch (message.type) {
|
|
@@ -1828,6 +1833,9 @@ var ClientSession = class {
|
|
|
1828
1833
|
break;
|
|
1829
1834
|
case "error":
|
|
1830
1835
|
break;
|
|
1836
|
+
case "awareness-update":
|
|
1837
|
+
this.handleAwarenessUpdate(message);
|
|
1838
|
+
break;
|
|
1831
1839
|
}
|
|
1832
1840
|
}
|
|
1833
1841
|
async handleHandshake(msg) {
|
|
@@ -1847,6 +1855,19 @@ var ClientSession = class {
|
|
|
1847
1855
|
this.authContext = context;
|
|
1848
1856
|
this.state = "authenticated";
|
|
1849
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
|
+
}
|
|
1850
1871
|
const serverVector = this.store.getVersionVector();
|
|
1851
1872
|
const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
|
|
1852
1873
|
this.setSerializerWireFormat(selectedWireFormat);
|
|
@@ -1857,7 +1878,10 @@ var ClientSession = class {
|
|
|
1857
1878
|
versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
|
|
1858
1879
|
schemaVersion: this.schemaVersion,
|
|
1859
1880
|
accepted: true,
|
|
1860
|
-
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 } : {}
|
|
1861
1885
|
};
|
|
1862
1886
|
this.transport.send(response);
|
|
1863
1887
|
this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
|
|
@@ -1869,8 +1893,10 @@ var ClientSession = class {
|
|
|
1869
1893
|
async handleOperationBatch(msg) {
|
|
1870
1894
|
const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
1871
1895
|
const applied = [];
|
|
1896
|
+
const rejected = [];
|
|
1872
1897
|
for (const op of operations2) {
|
|
1873
1898
|
if (!operationMatchesScopes(op, this.authContext?.scopes)) {
|
|
1899
|
+
rejected.push(op);
|
|
1874
1900
|
continue;
|
|
1875
1901
|
}
|
|
1876
1902
|
const result = await this.store.applyRemoteOperation(op);
|
|
@@ -1878,6 +1904,15 @@ var ClientSession = class {
|
|
|
1878
1904
|
applied.push(op);
|
|
1879
1905
|
}
|
|
1880
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
|
+
}
|
|
1881
1916
|
if (operations2.length > 0) {
|
|
1882
1917
|
this.emitter?.emit({
|
|
1883
1918
|
type: "sync:received",
|
|
@@ -1940,6 +1975,9 @@ var ClientSession = class {
|
|
|
1940
1975
|
});
|
|
1941
1976
|
}
|
|
1942
1977
|
}
|
|
1978
|
+
handleAwarenessUpdate(msg) {
|
|
1979
|
+
this.onAwarenessUpdate?.(this.sessionId, msg);
|
|
1980
|
+
}
|
|
1943
1981
|
sendError(code, message, retriable) {
|
|
1944
1982
|
const errorMsg = {
|
|
1945
1983
|
type: "error",
|
|
@@ -1970,8 +2008,107 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
1970
2008
|
}
|
|
1971
2009
|
|
|
1972
2010
|
// src/server/kora-sync-server.ts
|
|
1973
|
-
var
|
|
2011
|
+
var import_core8 = require("@korajs/core");
|
|
1974
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
|
|
1975
2112
|
var DEFAULT_MAX_CONNECTIONS = 0;
|
|
1976
2113
|
var DEFAULT_BATCH_SIZE2 = 100;
|
|
1977
2114
|
var DEFAULT_SCHEMA_VERSION2 = 1;
|
|
@@ -1988,6 +2125,7 @@ var KoraSyncServer = class {
|
|
|
1988
2125
|
port;
|
|
1989
2126
|
host;
|
|
1990
2127
|
path;
|
|
2128
|
+
awarenessRelay = new AwarenessRelay();
|
|
1991
2129
|
sessions = /* @__PURE__ */ new Map();
|
|
1992
2130
|
httpClients = /* @__PURE__ */ new Map();
|
|
1993
2131
|
httpSessionToClient = /* @__PURE__ */ new Map();
|
|
@@ -2012,10 +2150,10 @@ var KoraSyncServer = class {
|
|
|
2012
2150
|
*/
|
|
2013
2151
|
async start(wsServerImpl) {
|
|
2014
2152
|
if (this.running) {
|
|
2015
|
-
throw new
|
|
2153
|
+
throw new import_core8.SyncError("Server is already running", { port: this.port });
|
|
2016
2154
|
}
|
|
2017
2155
|
if (!wsServerImpl && this.port === void 0) {
|
|
2018
|
-
throw new
|
|
2156
|
+
throw new import_core8.SyncError(
|
|
2019
2157
|
"Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
|
|
2020
2158
|
{}
|
|
2021
2159
|
);
|
|
@@ -2049,6 +2187,7 @@ var KoraSyncServer = class {
|
|
|
2049
2187
|
* Stop the server. Closes all sessions and the WebSocket server.
|
|
2050
2188
|
*/
|
|
2051
2189
|
async stop() {
|
|
2190
|
+
this.awarenessRelay.clear();
|
|
2052
2191
|
for (const session of this.sessions.values()) {
|
|
2053
2192
|
session.close("server shutting down");
|
|
2054
2193
|
}
|
|
@@ -2105,18 +2244,18 @@ var KoraSyncServer = class {
|
|
|
2105
2244
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
2106
2245
|
transport.send({
|
|
2107
2246
|
type: "error",
|
|
2108
|
-
messageId: (0,
|
|
2247
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
2109
2248
|
code: "MAX_CONNECTIONS",
|
|
2110
2249
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
2111
2250
|
retriable: true
|
|
2112
2251
|
});
|
|
2113
2252
|
transport.close(4029, "max connections reached");
|
|
2114
|
-
throw new
|
|
2253
|
+
throw new import_core8.SyncError("Maximum connections reached", {
|
|
2115
2254
|
current: this.sessions.size,
|
|
2116
2255
|
max: this.maxConnections
|
|
2117
2256
|
});
|
|
2118
2257
|
}
|
|
2119
|
-
const sessionId = (0,
|
|
2258
|
+
const sessionId = (0, import_core8.generateUUIDv7)();
|
|
2120
2259
|
const session = new ClientSession({
|
|
2121
2260
|
sessionId,
|
|
2122
2261
|
transport,
|
|
@@ -2129,6 +2268,9 @@ var KoraSyncServer = class {
|
|
|
2129
2268
|
onRelay: (sourceSessionId, operations2) => {
|
|
2130
2269
|
this.handleRelay(sourceSessionId, operations2);
|
|
2131
2270
|
},
|
|
2271
|
+
onAwarenessUpdate: (sourceSessionId, message) => {
|
|
2272
|
+
this.handleAwarenessRelay(sourceSessionId, message);
|
|
2273
|
+
},
|
|
2132
2274
|
onClose: (sid) => {
|
|
2133
2275
|
this.handleSessionClose(sid);
|
|
2134
2276
|
}
|
|
@@ -2162,6 +2304,7 @@ var KoraSyncServer = class {
|
|
|
2162
2304
|
}
|
|
2163
2305
|
}
|
|
2164
2306
|
handleSessionClose(sessionId) {
|
|
2307
|
+
this.awarenessRelay.removeClient(sessionId);
|
|
2165
2308
|
this.sessions.delete(sessionId);
|
|
2166
2309
|
const clientId = this.httpSessionToClient.get(sessionId);
|
|
2167
2310
|
if (clientId) {
|
|
@@ -2169,6 +2312,15 @@ var KoraSyncServer = class {
|
|
|
2169
2312
|
this.httpClients.delete(clientId);
|
|
2170
2313
|
}
|
|
2171
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
|
+
}
|
|
2172
2324
|
getOrCreateHttpClient(clientId) {
|
|
2173
2325
|
const existing = this.httpClients.get(clientId);
|
|
2174
2326
|
if (existing) {
|
|
@@ -2250,6 +2402,30 @@ var KoraAuthProvider = class {
|
|
|
2250
2402
|
}
|
|
2251
2403
|
};
|
|
2252
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
|
+
|
|
2253
2429
|
// src/server/create-server.ts
|
|
2254
2430
|
function createKoraServer(config) {
|
|
2255
2431
|
return new KoraSyncServer(config);
|
|
@@ -2338,7 +2514,7 @@ function createProductionServer(config) {
|
|
|
2338
2514
|
}
|
|
2339
2515
|
});
|
|
2340
2516
|
return new Promise((resolve2) => {
|
|
2341
|
-
httpServer
|
|
2517
|
+
httpServer?.listen(port, "0.0.0.0", () => {
|
|
2342
2518
|
resolve2(`http://localhost:${port}`);
|
|
2343
2519
|
});
|
|
2344
2520
|
});
|
|
@@ -2347,7 +2523,7 @@ function createProductionServer(config) {
|
|
|
2347
2523
|
await syncServer.stop();
|
|
2348
2524
|
if (httpServer) {
|
|
2349
2525
|
await new Promise((resolve) => {
|
|
2350
|
-
httpServer
|
|
2526
|
+
httpServer?.close(() => resolve());
|
|
2351
2527
|
});
|
|
2352
2528
|
httpServer = null;
|
|
2353
2529
|
}
|
|
@@ -2356,11 +2532,13 @@ function createProductionServer(config) {
|
|
|
2356
2532
|
}
|
|
2357
2533
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2358
2534
|
0 && (module.exports = {
|
|
2535
|
+
AwarenessRelay,
|
|
2359
2536
|
ClientSession,
|
|
2360
2537
|
HttpServerTransport,
|
|
2361
2538
|
KoraAuthProvider,
|
|
2362
2539
|
KoraSyncServer,
|
|
2363
2540
|
MemoryServerStore,
|
|
2541
|
+
MixedAuthProvider,
|
|
2364
2542
|
NoAuthProvider,
|
|
2365
2543
|
PostgresServerStore,
|
|
2366
2544
|
SqliteServerStore,
|