@korajs/server 0.4.0 → 0.5.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 +10 -0
- package/dist/index.cjs +1459 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +302 -9
- package/dist/index.d.ts +302 -9
- package/dist/index.js +1286 -39
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +1 -1
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/server-backup-MOICK6MI.js +116 -0
- package/dist/server-backup-MOICK6MI.js.map +1 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
// src/logging/structured-logger.ts
|
|
2
|
+
function defaultJsonSerializer(entry) {
|
|
3
|
+
return JSON.stringify(entry);
|
|
4
|
+
}
|
|
5
|
+
function defaultPrettySerializer(entry) {
|
|
6
|
+
const time = new Date(entry.timestamp).toISOString().slice(11, 23);
|
|
7
|
+
const levelTag = { info: " INFO", warn: " WARN", error: "ERROR" }[entry.level];
|
|
8
|
+
const node = entry.nodeId ? ` [${entry.nodeId.slice(0, 8)}]` : "";
|
|
9
|
+
const session = entry.sessionId ? ` [session:${entry.sessionId.slice(0, 8)}]` : "";
|
|
10
|
+
const dur = entry.duration !== void 0 ? ` +${entry.duration}ms` : "";
|
|
11
|
+
const count3 = entry.count !== void 0 ? ` (${entry.count})` : "";
|
|
12
|
+
const err = entry.error ? ` \u2014 ${entry.error}` : "";
|
|
13
|
+
return `${time} ${levelTag}${node}${session} ${entry.event}${count3}${dur}${err}`;
|
|
14
|
+
}
|
|
15
|
+
function createJsonLogger(writer) {
|
|
16
|
+
const out = writer ?? {
|
|
17
|
+
info: (s) => process.stdout.write(`${s}
|
|
18
|
+
`),
|
|
19
|
+
error: (s) => process.stderr.write(`${s}
|
|
20
|
+
`)
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
log(entry) {
|
|
24
|
+
const line = defaultJsonSerializer(entry);
|
|
25
|
+
if (entry.level === "error") {
|
|
26
|
+
out.error(line);
|
|
27
|
+
} else {
|
|
28
|
+
out.info(line);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createPrettyLogger(writer) {
|
|
34
|
+
const out = writer ?? { write: (s) => process.stdout.write(`${s}
|
|
35
|
+
`) };
|
|
36
|
+
return {
|
|
37
|
+
log(entry) {
|
|
38
|
+
out.write(defaultPrettySerializer(entry));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function createSilentLogger() {
|
|
43
|
+
return { log() {
|
|
44
|
+
} };
|
|
45
|
+
}
|
|
46
|
+
function createDefaultLogger() {
|
|
47
|
+
switch (process.env.NODE_ENV) {
|
|
48
|
+
case "production":
|
|
49
|
+
return createJsonLogger();
|
|
50
|
+
case "test":
|
|
51
|
+
return createSilentLogger();
|
|
52
|
+
default:
|
|
53
|
+
return createPrettyLogger();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
1
57
|
// src/store/memory-server-store.ts
|
|
2
58
|
import { generateUUIDv7 } from "@korajs/core";
|
|
3
59
|
|
|
@@ -157,6 +213,9 @@ var MemoryServerStore = class {
|
|
|
157
213
|
getNodeId() {
|
|
158
214
|
return this.nodeId;
|
|
159
215
|
}
|
|
216
|
+
getSchema() {
|
|
217
|
+
return this.schema;
|
|
218
|
+
}
|
|
160
219
|
async setSchema(schema) {
|
|
161
220
|
this.assertOpen();
|
|
162
221
|
this.schema = schema;
|
|
@@ -306,6 +365,49 @@ var MemoryServerStore = class {
|
|
|
306
365
|
async close() {
|
|
307
366
|
this.closed = true;
|
|
308
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Wipes all in-memory state. For tests and E2E isolation only.
|
|
370
|
+
*/
|
|
371
|
+
resetForTests() {
|
|
372
|
+
this.assertOpen();
|
|
373
|
+
this.operations.length = 0;
|
|
374
|
+
this.operationIndex.clear();
|
|
375
|
+
this.versionVector.clear();
|
|
376
|
+
this.materializedRecords.clear();
|
|
377
|
+
this.schema = null;
|
|
378
|
+
}
|
|
379
|
+
async exportBackup() {
|
|
380
|
+
this.assertOpen();
|
|
381
|
+
const { buildServerBackup } = await import("./server-backup-MOICK6MI.js");
|
|
382
|
+
return buildServerBackup(this.nodeId, this.operations, this.versionVector);
|
|
383
|
+
}
|
|
384
|
+
async importBackup(data, merge) {
|
|
385
|
+
this.assertOpen();
|
|
386
|
+
const { parseServerBackup } = await import("./server-backup-MOICK6MI.js");
|
|
387
|
+
const { operations: operations2, versionVector } = parseServerBackup(data);
|
|
388
|
+
if (merge) {
|
|
389
|
+
let restored = 0;
|
|
390
|
+
for (const op of operations2) {
|
|
391
|
+
const result = await this.applyRemoteOperation(op);
|
|
392
|
+
if (result === "applied") restored++;
|
|
393
|
+
}
|
|
394
|
+
return { operationsRestored: restored, success: true };
|
|
395
|
+
}
|
|
396
|
+
this.operations.length = 0;
|
|
397
|
+
this.operationIndex.clear();
|
|
398
|
+
this.versionVector.clear();
|
|
399
|
+
for (const [nid, seq] of versionVector) {
|
|
400
|
+
this.versionVector.set(nid, seq);
|
|
401
|
+
}
|
|
402
|
+
for (const op of operations2) {
|
|
403
|
+
this.operations.push(op);
|
|
404
|
+
this.operationIndex.set(op.id, op);
|
|
405
|
+
if (this.schema?.collections[op.collection]) {
|
|
406
|
+
this.rebuildMaterializedRecord(op.collection, op.recordId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return { operationsRestored: operations2.length, success: true };
|
|
410
|
+
}
|
|
309
411
|
// --- Testing helpers (not on interface) ---
|
|
310
412
|
/**
|
|
311
413
|
* Get all stored operations (for test assertions).
|
|
@@ -497,6 +599,9 @@ var PostgresServerStore = class {
|
|
|
497
599
|
getNodeId() {
|
|
498
600
|
return this.nodeId;
|
|
499
601
|
}
|
|
602
|
+
getSchema() {
|
|
603
|
+
return this.schema;
|
|
604
|
+
}
|
|
500
605
|
async setSchema(schema) {
|
|
501
606
|
this.assertOpen();
|
|
502
607
|
await this.ready;
|
|
@@ -625,6 +730,45 @@ var PostgresServerStore = class {
|
|
|
625
730
|
async close() {
|
|
626
731
|
this.closed = true;
|
|
627
732
|
}
|
|
733
|
+
async exportBackup() {
|
|
734
|
+
this.assertOpen();
|
|
735
|
+
await this.ready;
|
|
736
|
+
const { buildServerBackup } = await import("./server-backup-MOICK6MI.js");
|
|
737
|
+
const rows = await this.db.select().from(pgOperations).orderBy(asc(pgOperations.sequenceNumber));
|
|
738
|
+
const operations2 = rows.map((row) => this.deserializeOperation(row));
|
|
739
|
+
return buildServerBackup(this.nodeId, operations2, this.versionVector);
|
|
740
|
+
}
|
|
741
|
+
async importBackup(data, merge) {
|
|
742
|
+
this.assertOpen();
|
|
743
|
+
await this.ready;
|
|
744
|
+
const { parseServerBackup } = await import("./server-backup-MOICK6MI.js");
|
|
745
|
+
const { operations: operations2, versionVector } = parseServerBackup(data);
|
|
746
|
+
if (merge) {
|
|
747
|
+
let restored = 0;
|
|
748
|
+
for (const op of operations2) {
|
|
749
|
+
const result = await this.applyRemoteOperation(op);
|
|
750
|
+
if (result === "applied") restored++;
|
|
751
|
+
}
|
|
752
|
+
return { operationsRestored: restored, success: true };
|
|
753
|
+
}
|
|
754
|
+
const now = Date.now();
|
|
755
|
+
await this.db.transaction(async (tx) => {
|
|
756
|
+
await tx.delete(pgOperations);
|
|
757
|
+
await tx.delete(pgSyncState);
|
|
758
|
+
for (const [nid, seq] of versionVector) {
|
|
759
|
+
await tx.insert(pgSyncState).values({ nodeId: nid, maxSequenceNumber: seq, lastSeenAt: now }).onConflictDoNothing({ target: pgSyncState.nodeId });
|
|
760
|
+
}
|
|
761
|
+
for (const op of operations2) {
|
|
762
|
+
const row = this.serializeOperation(op, now);
|
|
763
|
+
await tx.insert(pgOperations).values(row).onConflictDoNothing({ target: pgOperations.id });
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
this.versionVector.clear();
|
|
767
|
+
for (const [nid, seq] of versionVector) {
|
|
768
|
+
this.versionVector.set(nid, seq);
|
|
769
|
+
}
|
|
770
|
+
return { operationsRestored: operations2.length, success: true };
|
|
771
|
+
}
|
|
628
772
|
// ---------------------------------------------------------------------------
|
|
629
773
|
// Materialization internals
|
|
630
774
|
// ---------------------------------------------------------------------------
|
|
@@ -1042,6 +1186,9 @@ var SqliteServerStore = class {
|
|
|
1042
1186
|
getNodeId() {
|
|
1043
1187
|
return this.nodeId;
|
|
1044
1188
|
}
|
|
1189
|
+
getSchema() {
|
|
1190
|
+
return this.schema;
|
|
1191
|
+
}
|
|
1045
1192
|
async setSchema(schema) {
|
|
1046
1193
|
this.assertOpen();
|
|
1047
1194
|
this.schema = schema;
|
|
@@ -1155,6 +1302,39 @@ var SqliteServerStore = class {
|
|
|
1155
1302
|
async close() {
|
|
1156
1303
|
this.closed = true;
|
|
1157
1304
|
}
|
|
1305
|
+
async exportBackup() {
|
|
1306
|
+
this.assertOpen();
|
|
1307
|
+
const { buildServerBackup } = await import("./server-backup-MOICK6MI.js");
|
|
1308
|
+
const rows = this.db.select().from(operations).all();
|
|
1309
|
+
const deserialized = rows.map((row) => this.deserializeOperation(row));
|
|
1310
|
+
const vv = this.getVersionVector();
|
|
1311
|
+
return buildServerBackup(this.nodeId, deserialized, vv);
|
|
1312
|
+
}
|
|
1313
|
+
async importBackup(data, merge) {
|
|
1314
|
+
this.assertOpen();
|
|
1315
|
+
const { parseServerBackup } = await import("./server-backup-MOICK6MI.js");
|
|
1316
|
+
const { operations: ops, versionVector } = parseServerBackup(data);
|
|
1317
|
+
if (merge) {
|
|
1318
|
+
let restored = 0;
|
|
1319
|
+
for (const op of ops) {
|
|
1320
|
+
const result = await this.applyRemoteOperation(op);
|
|
1321
|
+
if (result === "applied") restored++;
|
|
1322
|
+
}
|
|
1323
|
+
return { operationsRestored: restored, success: true };
|
|
1324
|
+
}
|
|
1325
|
+
this.db.transaction((tx) => {
|
|
1326
|
+
tx.run(sql2.raw("DELETE FROM operations"));
|
|
1327
|
+
tx.run(sql2.raw("DELETE FROM sync_state"));
|
|
1328
|
+
for (const [nid, seq] of versionVector) {
|
|
1329
|
+
tx.insert(syncState).values({ nodeId: nid, maxSequenceNumber: seq, lastSeenAt: Date.now() }).run();
|
|
1330
|
+
}
|
|
1331
|
+
for (const op of ops) {
|
|
1332
|
+
const row = this.serializeOperation(op, Date.now());
|
|
1333
|
+
tx.insert(operations).values(row).run();
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
return { operationsRestored: ops.length, success: true };
|
|
1337
|
+
}
|
|
1158
1338
|
// ---------------------------------------------------------------------------
|
|
1159
1339
|
// Materialization internals
|
|
1160
1340
|
// ---------------------------------------------------------------------------
|
|
@@ -1634,7 +1814,220 @@ var WsServerTransport = class {
|
|
|
1634
1814
|
// src/session/client-session.ts
|
|
1635
1815
|
import { generateUUIDv7 as generateUUIDv74 } from "@korajs/core";
|
|
1636
1816
|
import { topologicalSort } from "@korajs/core/internal";
|
|
1637
|
-
import {
|
|
1817
|
+
import {
|
|
1818
|
+
NegotiatedMessageSerializer,
|
|
1819
|
+
SCHEMA_MISMATCH_PREFIX,
|
|
1820
|
+
createDeltaCursorFromBatch,
|
|
1821
|
+
decodeDeltaCursor,
|
|
1822
|
+
dedupeQuerySubsets,
|
|
1823
|
+
encodeDeltaCursor,
|
|
1824
|
+
isClientSchemaVersionSupported,
|
|
1825
|
+
operationMatchesQuerySubsets,
|
|
1826
|
+
sliceOperationsAfterCursor,
|
|
1827
|
+
versionVectorToWire,
|
|
1828
|
+
wireToVersionVector
|
|
1829
|
+
} from "@korajs/sync";
|
|
1830
|
+
|
|
1831
|
+
// src/apply/apply-server-operation.ts
|
|
1832
|
+
import { buildMergeRelationLookup, checkReferentialIntegrityOnDelete } from "@korajs/merge";
|
|
1833
|
+
|
|
1834
|
+
// src/constraints/operation-constraint-validator.ts
|
|
1835
|
+
import { checkConstraints } from "@korajs/merge";
|
|
1836
|
+
|
|
1837
|
+
// src/constraints/server-constraint-context.ts
|
|
1838
|
+
function createServerConstraintContext(store) {
|
|
1839
|
+
return {
|
|
1840
|
+
async queryRecords(collection, where) {
|
|
1841
|
+
const rows = await store.queryCollection(collection, { where });
|
|
1842
|
+
return rows.map((row) => ({ ...row }));
|
|
1843
|
+
},
|
|
1844
|
+
async countRecords(collection, where) {
|
|
1845
|
+
return store.countCollection(collection, where);
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// src/constraints/operation-constraint-validator.ts
|
|
1851
|
+
async function validateIncomingOperationConstraints(store, op, schema) {
|
|
1852
|
+
if (!schema) {
|
|
1853
|
+
return { valid: true };
|
|
1854
|
+
}
|
|
1855
|
+
if (op.type === "delete") {
|
|
1856
|
+
return { valid: true };
|
|
1857
|
+
}
|
|
1858
|
+
const collectionDef = schema.collections[op.collection];
|
|
1859
|
+
if (!collectionDef || collectionDef.constraints.length === 0) {
|
|
1860
|
+
return { valid: true };
|
|
1861
|
+
}
|
|
1862
|
+
const candidate = await projectCandidateRecord(store, op);
|
|
1863
|
+
if (candidate === null) {
|
|
1864
|
+
return { valid: true };
|
|
1865
|
+
}
|
|
1866
|
+
const ctx = createServerConstraintContext(store);
|
|
1867
|
+
const violations = await checkConstraints(
|
|
1868
|
+
candidate,
|
|
1869
|
+
op.recordId,
|
|
1870
|
+
op.collection,
|
|
1871
|
+
collectionDef,
|
|
1872
|
+
ctx
|
|
1873
|
+
);
|
|
1874
|
+
if (violations.length === 0) {
|
|
1875
|
+
return { valid: true };
|
|
1876
|
+
}
|
|
1877
|
+
const first = violations[0];
|
|
1878
|
+
if (first === void 0) {
|
|
1879
|
+
return { valid: true };
|
|
1880
|
+
}
|
|
1881
|
+
return {
|
|
1882
|
+
valid: false,
|
|
1883
|
+
code: "CONSTRAINT_VIOLATION",
|
|
1884
|
+
message: first.message
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
async function projectCandidateRecord(store, op) {
|
|
1888
|
+
if (op.data === null) {
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
const existing = await store.findRecord(op.collection, op.recordId);
|
|
1892
|
+
const base = existing ? { ...existing } : {};
|
|
1893
|
+
if (op.type === "insert") {
|
|
1894
|
+
return { ...op.data, id: op.recordId };
|
|
1895
|
+
}
|
|
1896
|
+
return { ...base, ...op.data, id: op.recordId };
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/constraints/server-referential-context.ts
|
|
1900
|
+
function createServerReferentialContext(store) {
|
|
1901
|
+
return {
|
|
1902
|
+
async queryRecords(collection, where) {
|
|
1903
|
+
const rows = await store.queryCollection(collection, { where });
|
|
1904
|
+
return rows.map((row) => ({ ...row }));
|
|
1905
|
+
},
|
|
1906
|
+
async recordExists(collection, recordId) {
|
|
1907
|
+
const row = await store.findRecord(collection, recordId);
|
|
1908
|
+
return row !== null && row._deleted !== 1;
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// src/apply/server-side-effect-operation.ts
|
|
1914
|
+
import { HybridLogicalClock, createOperation } from "@korajs/core";
|
|
1915
|
+
async function createServerSideEffectOperation(store, parentOp, effect, schemaVersion, sequenceNumber) {
|
|
1916
|
+
const nodeId = store.getNodeId();
|
|
1917
|
+
const clock = new HybridLogicalClock(nodeId);
|
|
1918
|
+
clock.receive(parentOp.timestamp);
|
|
1919
|
+
return createOperation(
|
|
1920
|
+
{
|
|
1921
|
+
nodeId,
|
|
1922
|
+
type: effect.type === "delete" ? "delete" : "update",
|
|
1923
|
+
collection: effect.collection,
|
|
1924
|
+
recordId: effect.recordId,
|
|
1925
|
+
data: effect.data,
|
|
1926
|
+
previousData: effect.previousData,
|
|
1927
|
+
sequenceNumber,
|
|
1928
|
+
causalDeps: [parentOp.id],
|
|
1929
|
+
schemaVersion
|
|
1930
|
+
},
|
|
1931
|
+
clock
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
function nextServerSequenceNumber(store) {
|
|
1935
|
+
const nodeId = store.getNodeId();
|
|
1936
|
+
const current = store.getVersionVector().get(nodeId) ?? 0;
|
|
1937
|
+
return current + 1;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/apply/apply-server-operation.ts
|
|
1941
|
+
async function applyServerOperation(store, op, relationLookup) {
|
|
1942
|
+
const schema = store.getSchema();
|
|
1943
|
+
const lookup = relationLookup ?? (schema ? buildMergeRelationLookup(schema) : /* @__PURE__ */ new Map());
|
|
1944
|
+
const constraintCheck = await validateIncomingOperationConstraints(store, op, schema);
|
|
1945
|
+
if (!constraintCheck.valid) {
|
|
1946
|
+
return {
|
|
1947
|
+
result: "skipped",
|
|
1948
|
+
appliedOperations: [],
|
|
1949
|
+
rejection: {
|
|
1950
|
+
code: constraintCheck.code ?? "CONSTRAINT_VIOLATION",
|
|
1951
|
+
message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint`
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
if (op.type === "delete" && schema) {
|
|
1956
|
+
const refCtx = createServerReferentialContext(store);
|
|
1957
|
+
const referential = await checkReferentialIntegrityOnDelete(op, schema, refCtx, lookup);
|
|
1958
|
+
if (!referential.allowed) {
|
|
1959
|
+
return {
|
|
1960
|
+
result: "skipped",
|
|
1961
|
+
appliedOperations: [],
|
|
1962
|
+
rejection: {
|
|
1963
|
+
code: "REFERENTIAL_INTEGRITY",
|
|
1964
|
+
message: `Operation "${op.id}" violates referential integrity on "${op.collection}"`
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
const primaryResult = await store.applyRemoteOperation(op);
|
|
1969
|
+
if (primaryResult !== "applied") {
|
|
1970
|
+
return { result: primaryResult, appliedOperations: [] };
|
|
1971
|
+
}
|
|
1972
|
+
const appliedOperations = [op];
|
|
1973
|
+
let serverSeq = nextServerSequenceNumber(store);
|
|
1974
|
+
for (const effect of referential.sideEffectOps) {
|
|
1975
|
+
const sideOp = await createServerSideEffectOperation(
|
|
1976
|
+
store,
|
|
1977
|
+
op,
|
|
1978
|
+
effect,
|
|
1979
|
+
op.schemaVersion,
|
|
1980
|
+
serverSeq
|
|
1981
|
+
);
|
|
1982
|
+
serverSeq += 1;
|
|
1983
|
+
const sideResult = await store.applyRemoteOperation(sideOp);
|
|
1984
|
+
if (sideResult === "applied") {
|
|
1985
|
+
appliedOperations.push(sideOp);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
return { result: "applied", appliedOperations };
|
|
1989
|
+
}
|
|
1990
|
+
const result = await store.applyRemoteOperation(op);
|
|
1991
|
+
return {
|
|
1992
|
+
result,
|
|
1993
|
+
appliedOperations: result === "applied" ? [op] : []
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// src/scopes/resolve-session-scopes.ts
|
|
1998
|
+
import { buildScopeMap } from "@korajs/core";
|
|
1999
|
+
function resolveSessionScopes(schema, options) {
|
|
2000
|
+
const { handshakeScope, authScopes, scopeValues } = options;
|
|
2001
|
+
let resolved;
|
|
2002
|
+
if (schema && scopeValues && Object.keys(scopeValues).length > 0) {
|
|
2003
|
+
resolved = buildScopeMap(schema, scopeValues);
|
|
2004
|
+
} else if (handshakeScope) {
|
|
2005
|
+
resolved = { ...handshakeScope };
|
|
2006
|
+
} else if (authScopes) {
|
|
2007
|
+
resolved = { ...authScopes };
|
|
2008
|
+
}
|
|
2009
|
+
if (schema && !resolved && scopeValues) {
|
|
2010
|
+
resolved = buildScopeMap(schema, scopeValues);
|
|
2011
|
+
}
|
|
2012
|
+
if (authScopes) {
|
|
2013
|
+
resolved = mergeScopeMaps(resolved, authScopes, "auth");
|
|
2014
|
+
}
|
|
2015
|
+
if (handshakeScope) {
|
|
2016
|
+
resolved = mergeScopeMaps(resolved, handshakeScope, "handshake");
|
|
2017
|
+
}
|
|
2018
|
+
return resolved && Object.keys(resolved).length > 0 ? resolved : void 0;
|
|
2019
|
+
}
|
|
2020
|
+
function mergeScopeMaps(base, overlay, source) {
|
|
2021
|
+
const merged = { ...base ?? {} };
|
|
2022
|
+
for (const [collection, overlayScope] of Object.entries(overlay)) {
|
|
2023
|
+
if (source === "auth") {
|
|
2024
|
+
merged[collection] = { ...merged[collection] ?? {}, ...overlayScope };
|
|
2025
|
+
} else {
|
|
2026
|
+
merged[collection] = { ...overlayScope ?? {}, ...merged[collection] ?? {} };
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return merged;
|
|
2030
|
+
}
|
|
1638
2031
|
|
|
1639
2032
|
// src/scopes/server-scope-filter.ts
|
|
1640
2033
|
function operationMatchesScopes(op, scopes) {
|
|
@@ -1666,6 +2059,55 @@ function asRecord(value) {
|
|
|
1666
2059
|
return value;
|
|
1667
2060
|
}
|
|
1668
2061
|
|
|
2062
|
+
// src/session/operation-validation.ts
|
|
2063
|
+
var SERVER_MAX_TIMESTAMP_FUTURE_MS = 6e4;
|
|
2064
|
+
function isOperationTimestampValid(op, now = Date.now()) {
|
|
2065
|
+
return op.timestamp.wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/session/session-operation-limits.ts
|
|
2069
|
+
var DEFAULT_MAX_OPERATION_BYTES = 256 * 1024;
|
|
2070
|
+
var DEFAULT_MAX_OPS_PER_MINUTE = 600;
|
|
2071
|
+
function measureOperationBytes(op) {
|
|
2072
|
+
return Buffer.byteLength(JSON.stringify(op), "utf8");
|
|
2073
|
+
}
|
|
2074
|
+
function validateOperationSize(op, maxBytes = DEFAULT_MAX_OPERATION_BYTES) {
|
|
2075
|
+
const bytes = measureOperationBytes(op);
|
|
2076
|
+
if (bytes <= maxBytes) {
|
|
2077
|
+
return { valid: true, bytes };
|
|
2078
|
+
}
|
|
2079
|
+
return {
|
|
2080
|
+
valid: false,
|
|
2081
|
+
bytes,
|
|
2082
|
+
message: `Operation "${op.id}" exceeds max size (${String(bytes)} > ${String(maxBytes)} bytes)`
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
var SessionRateLimiter = class {
|
|
2086
|
+
constructor(maxOpsPerMinute = DEFAULT_MAX_OPS_PER_MINUTE) {
|
|
2087
|
+
this.maxOpsPerMinute = maxOpsPerMinute;
|
|
2088
|
+
}
|
|
2089
|
+
maxOpsPerMinute;
|
|
2090
|
+
windowStartMs = Date.now();
|
|
2091
|
+
count = 0;
|
|
2092
|
+
get limit() {
|
|
2093
|
+
return this.maxOpsPerMinute;
|
|
2094
|
+
}
|
|
2095
|
+
/** Record N operations and return false when the limit is exceeded. */
|
|
2096
|
+
allow(count3 = 1) {
|
|
2097
|
+
const now = Date.now();
|
|
2098
|
+
if (now - this.windowStartMs >= 6e4) {
|
|
2099
|
+
this.windowStartMs = now;
|
|
2100
|
+
this.count = 0;
|
|
2101
|
+
}
|
|
2102
|
+
this.count += count3;
|
|
2103
|
+
return this.count <= this.maxOpsPerMinute;
|
|
2104
|
+
}
|
|
2105
|
+
reset() {
|
|
2106
|
+
this.windowStartMs = Date.now();
|
|
2107
|
+
this.count = 0;
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
|
|
1669
2111
|
// src/session/client-session.ts
|
|
1670
2112
|
var DEFAULT_BATCH_SIZE = 100;
|
|
1671
2113
|
var DEFAULT_SCHEMA_VERSION = 1;
|
|
@@ -1673,6 +2115,8 @@ var ClientSession = class {
|
|
|
1673
2115
|
state = "connected";
|
|
1674
2116
|
clientNodeId = null;
|
|
1675
2117
|
authContext = null;
|
|
2118
|
+
syncQuerySubsets = [];
|
|
2119
|
+
resumeDeltaCursor = null;
|
|
1676
2120
|
sessionId;
|
|
1677
2121
|
transport;
|
|
1678
2122
|
store;
|
|
@@ -1681,9 +2125,14 @@ var ClientSession = class {
|
|
|
1681
2125
|
emitter;
|
|
1682
2126
|
batchSize;
|
|
1683
2127
|
schemaVersion;
|
|
2128
|
+
supportedSchemaVersions;
|
|
1684
2129
|
onRelay;
|
|
1685
2130
|
onAwarenessUpdate;
|
|
2131
|
+
onYjsDocUpdate;
|
|
1686
2132
|
onClose;
|
|
2133
|
+
maxOperationBytes;
|
|
2134
|
+
maxOpsPerMinute;
|
|
2135
|
+
rateLimiter;
|
|
1687
2136
|
constructor(options) {
|
|
1688
2137
|
this.sessionId = options.sessionId;
|
|
1689
2138
|
this.transport = options.transport;
|
|
@@ -1693,15 +2142,24 @@ var ClientSession = class {
|
|
|
1693
2142
|
this.emitter = options.emitter ?? null;
|
|
1694
2143
|
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
1695
2144
|
this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
2145
|
+
const supported = options.supportedSchemaVersions;
|
|
2146
|
+
this.supportedSchemaVersions = supported ?? {
|
|
2147
|
+
min: this.schemaVersion,
|
|
2148
|
+
max: this.schemaVersion
|
|
2149
|
+
};
|
|
1696
2150
|
this.onRelay = options.onRelay ?? null;
|
|
1697
2151
|
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
2152
|
+
this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
|
|
1698
2153
|
this.onClose = options.onClose ?? null;
|
|
2154
|
+
this.maxOperationBytes = options.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
|
|
2155
|
+
this.maxOpsPerMinute = options.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE;
|
|
2156
|
+
this.rateLimiter = new SessionRateLimiter(this.maxOpsPerMinute);
|
|
1699
2157
|
}
|
|
1700
2158
|
/**
|
|
1701
2159
|
* Start handling messages from the client transport.
|
|
1702
2160
|
*/
|
|
1703
2161
|
start() {
|
|
1704
|
-
this.transport.onMessage((msg) => this.
|
|
2162
|
+
this.transport.onMessage((msg) => this.enqueueMessage(msg));
|
|
1705
2163
|
this.transport.onClose((_code, _reason) => this.handleTransportClose());
|
|
1706
2164
|
this.transport.onError((_err) => {
|
|
1707
2165
|
if (this.state !== "closed") {
|
|
@@ -1716,9 +2174,7 @@ var ClientSession = class {
|
|
|
1716
2174
|
relayOperations(operations2) {
|
|
1717
2175
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
1718
2176
|
if (operations2.length === 0) return;
|
|
1719
|
-
const visibleOperations = operations2.filter(
|
|
1720
|
-
(op) => operationMatchesScopes(op, this.authContext?.scopes)
|
|
1721
|
-
);
|
|
2177
|
+
const visibleOperations = operations2.filter((op) => this.operationVisibleToClient(op));
|
|
1722
2178
|
if (visibleOperations.length === 0) return;
|
|
1723
2179
|
const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
|
|
1724
2180
|
const msg = {
|
|
@@ -1728,7 +2184,7 @@ var ClientSession = class {
|
|
|
1728
2184
|
isFinal: true,
|
|
1729
2185
|
batchIndex: 0
|
|
1730
2186
|
};
|
|
1731
|
-
this.
|
|
2187
|
+
this.sendToClient(msg);
|
|
1732
2188
|
}
|
|
1733
2189
|
/**
|
|
1734
2190
|
* Close this session.
|
|
@@ -1765,15 +2221,30 @@ var ClientSession = class {
|
|
|
1765
2221
|
return this.transport;
|
|
1766
2222
|
}
|
|
1767
2223
|
// --- Private protocol handlers ---
|
|
1768
|
-
|
|
2224
|
+
messageChain = Promise.resolve();
|
|
2225
|
+
/** Send to the client when the transport is still connected; no-op otherwise. */
|
|
2226
|
+
sendToClient(message) {
|
|
2227
|
+
if (!this.transport.isConnected()) {
|
|
2228
|
+
return false;
|
|
2229
|
+
}
|
|
2230
|
+
try {
|
|
2231
|
+
this.transport.send(message);
|
|
2232
|
+
return true;
|
|
2233
|
+
} catch {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
enqueueMessage(message) {
|
|
2238
|
+
this.messageChain = this.messageChain.then(() => this.handleMessageAsync(message)).catch((error) => this.handleMessageFailure(error));
|
|
2239
|
+
}
|
|
2240
|
+
async handleMessageAsync(message) {
|
|
1769
2241
|
switch (message.type) {
|
|
1770
2242
|
case "handshake":
|
|
1771
|
-
this.handleHandshake(message);
|
|
2243
|
+
await this.handleHandshake(message);
|
|
1772
2244
|
break;
|
|
1773
2245
|
case "operation-batch":
|
|
1774
|
-
this.handleOperationBatch(message);
|
|
2246
|
+
await this.handleOperationBatch(message);
|
|
1775
2247
|
break;
|
|
1776
|
-
// Acknowledgments from clients are noted but no action needed on server
|
|
1777
2248
|
case "acknowledgment":
|
|
1778
2249
|
break;
|
|
1779
2250
|
case "error":
|
|
@@ -1781,8 +2252,16 @@ var ClientSession = class {
|
|
|
1781
2252
|
case "awareness-update":
|
|
1782
2253
|
this.handleAwarenessUpdate(message);
|
|
1783
2254
|
break;
|
|
2255
|
+
case "yjs-doc-update":
|
|
2256
|
+
this.handleYjsDocUpdate(message);
|
|
2257
|
+
break;
|
|
1784
2258
|
}
|
|
1785
2259
|
}
|
|
2260
|
+
handleMessageFailure(error) {
|
|
2261
|
+
const reason = error instanceof Error ? error.message : "Message handling failed";
|
|
2262
|
+
this.sendError("SYNC_ERROR", reason, true);
|
|
2263
|
+
this.close(reason);
|
|
2264
|
+
}
|
|
1786
2265
|
async handleHandshake(msg) {
|
|
1787
2266
|
if (this.state !== "connected") {
|
|
1788
2267
|
this.sendError("DUPLICATE_HANDSHAKE", "Handshake already completed", false);
|
|
@@ -1800,22 +2279,43 @@ var ClientSession = class {
|
|
|
1800
2279
|
this.authContext = context;
|
|
1801
2280
|
this.state = "authenticated";
|
|
1802
2281
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
2282
|
+
const resolvedScopes = resolveSessionScopes(this.store.getSchema(), {
|
|
2283
|
+
handshakeScope: msg.syncScope,
|
|
2284
|
+
authScopes: this.authContext?.scopes
|
|
2285
|
+
});
|
|
2286
|
+
if (resolvedScopes) {
|
|
1810
2287
|
if (this.authContext) {
|
|
1811
|
-
this.authContext = { ...this.authContext, scopes:
|
|
2288
|
+
this.authContext = { ...this.authContext, scopes: resolvedScopes };
|
|
1812
2289
|
} else {
|
|
1813
|
-
this.authContext = { userId: msg.nodeId, scopes:
|
|
2290
|
+
this.authContext = { userId: msg.nodeId, scopes: resolvedScopes };
|
|
1814
2291
|
}
|
|
1815
2292
|
}
|
|
2293
|
+
if (msg.syncQueries && msg.syncQueries.length > 0) {
|
|
2294
|
+
this.syncQuerySubsets = dedupeQuerySubsets(msg.syncQueries);
|
|
2295
|
+
} else {
|
|
2296
|
+
this.syncQuerySubsets = [];
|
|
2297
|
+
}
|
|
2298
|
+
this.resumeDeltaCursor = msg.deltaCursor ? decodeDeltaCursor(msg.deltaCursor) : null;
|
|
1816
2299
|
const serverVector = this.store.getVersionVector();
|
|
1817
2300
|
const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
|
|
1818
2301
|
this.setSerializerWireFormat(selectedWireFormat);
|
|
2302
|
+
if (!isClientSchemaVersionSupported(msg.schemaVersion, this.supportedSchemaVersions)) {
|
|
2303
|
+
const { min, max } = this.supportedSchemaVersions;
|
|
2304
|
+
const response2 = {
|
|
2305
|
+
type: "handshake-response",
|
|
2306
|
+
messageId: generateUUIDv74(),
|
|
2307
|
+
nodeId: this.store.getNodeId(),
|
|
2308
|
+
versionVector: versionVectorToWire(serverVector),
|
|
2309
|
+
schemaVersion: this.schemaVersion,
|
|
2310
|
+
accepted: false,
|
|
2311
|
+
rejectReason: `${SCHEMA_MISMATCH_PREFIX}: client schema version ${msg.schemaVersion} not in supported range [${min}, ${max}]`,
|
|
2312
|
+
supportedSchemaMin: min,
|
|
2313
|
+
supportedSchemaMax: max
|
|
2314
|
+
};
|
|
2315
|
+
this.sendToClient(response2);
|
|
2316
|
+
this.close("schema version mismatch");
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
1819
2319
|
const response = {
|
|
1820
2320
|
type: "handshake-response",
|
|
1821
2321
|
messageId: generateUUIDv74(),
|
|
@@ -1828,7 +2328,7 @@ var ClientSession = class {
|
|
|
1828
2328
|
// This may differ from what the client requested if auth scopes are narrower.
|
|
1829
2329
|
...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
|
|
1830
2330
|
};
|
|
1831
|
-
this.
|
|
2331
|
+
this.sendToClient(response);
|
|
1832
2332
|
this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
|
|
1833
2333
|
this.state = "syncing";
|
|
1834
2334
|
const clientVector = wireToVersionVector(msg.versionVector);
|
|
@@ -1840,13 +2340,45 @@ var ClientSession = class {
|
|
|
1840
2340
|
const applied = [];
|
|
1841
2341
|
const rejected = [];
|
|
1842
2342
|
for (const op of operations2) {
|
|
1843
|
-
if (!
|
|
2343
|
+
if (!this.operationVisibleToClient(op)) {
|
|
1844
2344
|
rejected.push(op);
|
|
1845
2345
|
continue;
|
|
1846
2346
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2347
|
+
if (!isOperationTimestampValid(op)) {
|
|
2348
|
+
this.sendError(
|
|
2349
|
+
"INVALID_TIMESTAMP",
|
|
2350
|
+
`Operation "${op.id}" timestamp is too far in the future`,
|
|
2351
|
+
false
|
|
2352
|
+
);
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
if (!this.rateLimiter.allow(1)) {
|
|
2356
|
+
this.sendError(
|
|
2357
|
+
"RATE_LIMIT",
|
|
2358
|
+
`Session exceeded operation rate limit (${String(this.maxOpsPerMinute)} ops/min)`,
|
|
2359
|
+
true
|
|
2360
|
+
);
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
const sizeCheck = validateOperationSize(op, this.maxOperationBytes);
|
|
2364
|
+
if (!sizeCheck.valid) {
|
|
2365
|
+
this.sendError(
|
|
2366
|
+
"OPERATION_TOO_LARGE",
|
|
2367
|
+
sizeCheck.message ?? `Operation "${op.id}" is too large`,
|
|
2368
|
+
false
|
|
2369
|
+
);
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
try {
|
|
2373
|
+
const applyResult = await applyServerOperation(this.store, op);
|
|
2374
|
+
if (applyResult.rejection) {
|
|
2375
|
+
this.sendError(applyResult.rejection.code, applyResult.rejection.message, false);
|
|
2376
|
+
continue;
|
|
2377
|
+
}
|
|
2378
|
+
if (applyResult.result === "applied") {
|
|
2379
|
+
applied.push(...applyResult.appliedOperations);
|
|
2380
|
+
}
|
|
2381
|
+
} catch {
|
|
1850
2382
|
}
|
|
1851
2383
|
}
|
|
1852
2384
|
if (rejected.length > 0) {
|
|
@@ -1872,7 +2404,7 @@ var ClientSession = class {
|
|
|
1872
2404
|
acknowledgedMessageId: msg.messageId,
|
|
1873
2405
|
lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
|
|
1874
2406
|
};
|
|
1875
|
-
this.
|
|
2407
|
+
this.sendToClient(ack);
|
|
1876
2408
|
if (applied.length > 0) {
|
|
1877
2409
|
this.onRelay?.(this.sessionId, applied);
|
|
1878
2410
|
}
|
|
@@ -1884,7 +2416,7 @@ var ClientSession = class {
|
|
|
1884
2416
|
const clientSeq = clientVector.get(nodeId) ?? 0;
|
|
1885
2417
|
if (serverSeq > clientSeq) {
|
|
1886
2418
|
const ops = await this.store.getOperationRange(nodeId, clientSeq + 1, serverSeq);
|
|
1887
|
-
const visible = ops.filter((op) =>
|
|
2419
|
+
const visible = ops.filter((op) => this.operationVisibleToClient(op));
|
|
1888
2420
|
missing.push(...visible);
|
|
1889
2421
|
}
|
|
1890
2422
|
}
|
|
@@ -1894,25 +2426,42 @@ var ClientSession = class {
|
|
|
1894
2426
|
messageId: generateUUIDv74(),
|
|
1895
2427
|
operations: [],
|
|
1896
2428
|
isFinal: true,
|
|
1897
|
-
batchIndex: 0
|
|
2429
|
+
batchIndex: 0,
|
|
2430
|
+
totalBatches: 1
|
|
1898
2431
|
};
|
|
1899
|
-
this.
|
|
2432
|
+
this.sendToClient(emptyBatch);
|
|
1900
2433
|
return;
|
|
1901
2434
|
}
|
|
1902
2435
|
const sorted = topologicalSort(missing);
|
|
1903
|
-
const
|
|
2436
|
+
const afterCursor = sliceOperationsAfterCursor(sorted, this.resumeDeltaCursor);
|
|
2437
|
+
const totalBatches = Math.ceil(afterCursor.length / this.batchSize);
|
|
2438
|
+
if (afterCursor.length === 0) {
|
|
2439
|
+
const emptyBatch = {
|
|
2440
|
+
type: "operation-batch",
|
|
2441
|
+
messageId: generateUUIDv74(),
|
|
2442
|
+
operations: [],
|
|
2443
|
+
isFinal: true,
|
|
2444
|
+
batchIndex: this.resumeDeltaCursor?.batchIndex ?? 0,
|
|
2445
|
+
totalBatches: 1
|
|
2446
|
+
};
|
|
2447
|
+
this.sendToClient(emptyBatch);
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
1904
2450
|
for (let i = 0; i < totalBatches; i++) {
|
|
1905
2451
|
const start = i * this.batchSize;
|
|
1906
|
-
const batchOps =
|
|
2452
|
+
const batchOps = afterCursor.slice(start, start + this.batchSize);
|
|
1907
2453
|
const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
|
|
2454
|
+
const batchCursor = createDeltaCursorFromBatch(batchOps, i);
|
|
1908
2455
|
const batchMsg = {
|
|
1909
2456
|
type: "operation-batch",
|
|
1910
2457
|
messageId: generateUUIDv74(),
|
|
1911
2458
|
operations: serializedOps,
|
|
1912
2459
|
isFinal: i === totalBatches - 1,
|
|
1913
|
-
batchIndex: i
|
|
2460
|
+
batchIndex: i,
|
|
2461
|
+
totalBatches,
|
|
2462
|
+
...batchCursor ? { cursor: encodeDeltaCursor(batchCursor) } : {}
|
|
1914
2463
|
};
|
|
1915
|
-
this.
|
|
2464
|
+
this.sendToClient(batchMsg);
|
|
1916
2465
|
this.emitter?.emit({
|
|
1917
2466
|
type: "sync:sent",
|
|
1918
2467
|
operations: batchOps,
|
|
@@ -1920,9 +2469,18 @@ var ClientSession = class {
|
|
|
1920
2469
|
});
|
|
1921
2470
|
}
|
|
1922
2471
|
}
|
|
2472
|
+
operationVisibleToClient(op) {
|
|
2473
|
+
if (!operationMatchesScopes(op, this.authContext?.scopes)) {
|
|
2474
|
+
return false;
|
|
2475
|
+
}
|
|
2476
|
+
return operationMatchesQuerySubsets(op, this.syncQuerySubsets);
|
|
2477
|
+
}
|
|
1923
2478
|
handleAwarenessUpdate(msg) {
|
|
1924
2479
|
this.onAwarenessUpdate?.(this.sessionId, msg);
|
|
1925
2480
|
}
|
|
2481
|
+
handleYjsDocUpdate(msg) {
|
|
2482
|
+
this.onYjsDocUpdate?.(this.sessionId, msg);
|
|
2483
|
+
}
|
|
1926
2484
|
sendError(code, message, retriable) {
|
|
1927
2485
|
const errorMsg = {
|
|
1928
2486
|
type: "error",
|
|
@@ -1931,7 +2489,7 @@ var ClientSession = class {
|
|
|
1931
2489
|
message,
|
|
1932
2490
|
retriable
|
|
1933
2491
|
};
|
|
1934
|
-
this.
|
|
2492
|
+
this.sendToClient(errorMsg);
|
|
1935
2493
|
}
|
|
1936
2494
|
setSerializerWireFormat(format) {
|
|
1937
2495
|
if (typeof this.serializer.setWireFormat === "function") {
|
|
@@ -1954,6 +2512,7 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
1954
2512
|
|
|
1955
2513
|
// src/server/kora-sync-server.ts
|
|
1956
2514
|
import { SyncError as SyncError4, generateUUIDv7 as generateUUIDv76 } from "@korajs/core";
|
|
2515
|
+
import { SimpleEventEmitter } from "@korajs/core/internal";
|
|
1957
2516
|
import { JsonMessageSerializer as JsonMessageSerializer2 } from "@korajs/sync";
|
|
1958
2517
|
|
|
1959
2518
|
// src/awareness/awareness-relay.ts
|
|
@@ -2053,6 +2612,156 @@ var AwarenessRelay = class {
|
|
|
2053
2612
|
}
|
|
2054
2613
|
};
|
|
2055
2614
|
|
|
2615
|
+
// src/diagnostics/server-metrics-collector.ts
|
|
2616
|
+
function estimateByteSize(operations2) {
|
|
2617
|
+
let total = 0;
|
|
2618
|
+
for (const op of operations2) {
|
|
2619
|
+
total += JSON.stringify(op).length;
|
|
2620
|
+
}
|
|
2621
|
+
return total;
|
|
2622
|
+
}
|
|
2623
|
+
var ServerMetricsCollector = class {
|
|
2624
|
+
startedAt = Date.now();
|
|
2625
|
+
peakConnections = 0;
|
|
2626
|
+
connectionsTotal = 0;
|
|
2627
|
+
operationsReceived = 0;
|
|
2628
|
+
operationsSent = 0;
|
|
2629
|
+
bytesReceived = 0;
|
|
2630
|
+
bytesSent = 0;
|
|
2631
|
+
errorCount = 0;
|
|
2632
|
+
schemaVersion = 1;
|
|
2633
|
+
clientMetrics = /* @__PURE__ */ new Map();
|
|
2634
|
+
/** Record a new client connection. */
|
|
2635
|
+
recordConnection(sessionId) {
|
|
2636
|
+
this.connectionsTotal++;
|
|
2637
|
+
this.clientMetrics.set(sessionId, {
|
|
2638
|
+
sessionId,
|
|
2639
|
+
nodeId: null,
|
|
2640
|
+
state: "connected",
|
|
2641
|
+
connectedAt: Date.now(),
|
|
2642
|
+
operationsReceived: 0,
|
|
2643
|
+
operationsSent: 0,
|
|
2644
|
+
authContext: null
|
|
2645
|
+
});
|
|
2646
|
+
this.peakConnections = Math.max(this.peakConnections, this.clientMetrics.size);
|
|
2647
|
+
}
|
|
2648
|
+
/** Record a client disconnection. */
|
|
2649
|
+
recordDisconnection(sessionId) {
|
|
2650
|
+
this.clientMetrics.delete(sessionId);
|
|
2651
|
+
}
|
|
2652
|
+
/** Update the node ID after a handshake completes. */
|
|
2653
|
+
recordHandshake(sessionId, nodeId) {
|
|
2654
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2655
|
+
if (client) {
|
|
2656
|
+
client.nodeId = nodeId;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
/** Update session state. */
|
|
2660
|
+
updateSessionState(sessionId, state) {
|
|
2661
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2662
|
+
if (client) {
|
|
2663
|
+
client.state = state;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
/** Record authentication context for a session. */
|
|
2667
|
+
recordAuth(sessionId, authContext) {
|
|
2668
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2669
|
+
if (client) {
|
|
2670
|
+
client.authContext = authContext;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
/** Record operations received from a client. */
|
|
2674
|
+
recordReceived(sessionId, count3, byteSize) {
|
|
2675
|
+
this.operationsReceived += count3;
|
|
2676
|
+
this.bytesReceived += byteSize;
|
|
2677
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2678
|
+
if (client) {
|
|
2679
|
+
client.operationsReceived += count3;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
/** Record operations sent to a client. */
|
|
2683
|
+
recordSent(sessionId, count3, byteSize) {
|
|
2684
|
+
this.operationsSent += count3;
|
|
2685
|
+
this.bytesSent += byteSize;
|
|
2686
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2687
|
+
if (client) {
|
|
2688
|
+
client.operationsSent += count3;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
/** Record an error. */
|
|
2692
|
+
recordError() {
|
|
2693
|
+
this.errorCount++;
|
|
2694
|
+
}
|
|
2695
|
+
/** Set the schema version. */
|
|
2696
|
+
setSchemaVersion(version) {
|
|
2697
|
+
this.schemaVersion = version;
|
|
2698
|
+
}
|
|
2699
|
+
/** Return a full snapshot of current metrics. */
|
|
2700
|
+
getSnapshot(totalOperations) {
|
|
2701
|
+
return {
|
|
2702
|
+
connectedClients: this.clientMetrics.size,
|
|
2703
|
+
connectedNodeIds: Array.from(this.clientMetrics.values()).filter((c) => c.nodeId !== null).map((c) => c.nodeId),
|
|
2704
|
+
peakConnections: this.peakConnections,
|
|
2705
|
+
connectionsTotal: this.connectionsTotal,
|
|
2706
|
+
operationsReceived: this.operationsReceived,
|
|
2707
|
+
operationsSent: this.operationsSent,
|
|
2708
|
+
bytesReceived: this.bytesReceived,
|
|
2709
|
+
bytesSent: this.bytesSent,
|
|
2710
|
+
clients: Array.from(this.clientMetrics.values()),
|
|
2711
|
+
uptime: Date.now() - this.startedAt,
|
|
2712
|
+
totalOperations,
|
|
2713
|
+
errorCount: this.errorCount,
|
|
2714
|
+
schemaVersion: this.schemaVersion
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
/** Reset all metrics. */
|
|
2718
|
+
reset() {
|
|
2719
|
+
this.startedAt = Date.now();
|
|
2720
|
+
this.peakConnections = 0;
|
|
2721
|
+
this.connectionsTotal = 0;
|
|
2722
|
+
this.operationsReceived = 0;
|
|
2723
|
+
this.operationsSent = 0;
|
|
2724
|
+
this.bytesReceived = 0;
|
|
2725
|
+
this.bytesSent = 0;
|
|
2726
|
+
this.errorCount = 0;
|
|
2727
|
+
this.clientMetrics.clear();
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
|
|
2731
|
+
// src/richtext/yjs-doc-relay.ts
|
|
2732
|
+
var YjsDocRelay = class {
|
|
2733
|
+
clients = /* @__PURE__ */ new Map();
|
|
2734
|
+
addClient(sessionId, transport) {
|
|
2735
|
+
this.clients.set(sessionId, { sessionId, transport });
|
|
2736
|
+
}
|
|
2737
|
+
removeClient(sessionId) {
|
|
2738
|
+
this.clients.delete(sessionId);
|
|
2739
|
+
}
|
|
2740
|
+
handleUpdate(sourceSessionId, message) {
|
|
2741
|
+
if (!this.clients.has(sourceSessionId)) {
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
this.broadcastExcept(sourceSessionId, message);
|
|
2745
|
+
}
|
|
2746
|
+
getClientCount() {
|
|
2747
|
+
return this.clients.size;
|
|
2748
|
+
}
|
|
2749
|
+
clear() {
|
|
2750
|
+
this.clients.clear();
|
|
2751
|
+
}
|
|
2752
|
+
broadcastExcept(excludeSessionId, message) {
|
|
2753
|
+
for (const [, client] of this.clients) {
|
|
2754
|
+
if (client.sessionId === excludeSessionId) {
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2757
|
+
if (!client.transport.isConnected()) {
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
client.transport.send(message);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2056
2765
|
// src/server/kora-sync-server.ts
|
|
2057
2766
|
var DEFAULT_MAX_CONNECTIONS = 0;
|
|
2058
2767
|
var DEFAULT_BATCH_SIZE2 = 100;
|
|
@@ -2067,13 +2776,18 @@ var KoraSyncServer = class {
|
|
|
2067
2776
|
maxConnections;
|
|
2068
2777
|
batchSize;
|
|
2069
2778
|
schemaVersion;
|
|
2779
|
+
supportedSchemaVersions;
|
|
2070
2780
|
port;
|
|
2071
2781
|
host;
|
|
2072
2782
|
path;
|
|
2783
|
+
logger;
|
|
2784
|
+
metrics;
|
|
2073
2785
|
awarenessRelay = new AwarenessRelay();
|
|
2786
|
+
yjsDocRelay = new YjsDocRelay();
|
|
2074
2787
|
sessions = /* @__PURE__ */ new Map();
|
|
2075
2788
|
httpClients = /* @__PURE__ */ new Map();
|
|
2076
2789
|
httpSessionToClient = /* @__PURE__ */ new Map();
|
|
2790
|
+
serverVersion = "0.4.0";
|
|
2077
2791
|
wsServer = null;
|
|
2078
2792
|
running = false;
|
|
2079
2793
|
constructor(config) {
|
|
@@ -2084,9 +2798,80 @@ var KoraSyncServer = class {
|
|
|
2084
2798
|
this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
|
|
2085
2799
|
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
|
|
2086
2800
|
this.schemaVersion = config.schemaVersion ?? DEFAULT_SCHEMA_VERSION2;
|
|
2801
|
+
this.supportedSchemaVersions = config.supportedSchemaVersions ?? {
|
|
2802
|
+
min: this.schemaVersion,
|
|
2803
|
+
max: this.schemaVersion
|
|
2804
|
+
};
|
|
2087
2805
|
this.port = config.port;
|
|
2088
2806
|
this.host = config.host ?? DEFAULT_HOST;
|
|
2089
2807
|
this.path = config.path ?? DEFAULT_PATH;
|
|
2808
|
+
this.logger = config.logger ?? createDefaultLogger();
|
|
2809
|
+
this.metrics = config.metricsCollector ?? new ServerMetricsCollector();
|
|
2810
|
+
this.metrics.setSchemaVersion(this.schemaVersion);
|
|
2811
|
+
if (!this.emitter) {
|
|
2812
|
+
this.emitter = new SimpleEventEmitter();
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Subscribe to session-level events for metrics collection and logging.
|
|
2817
|
+
* Called when a new session is created.
|
|
2818
|
+
*/
|
|
2819
|
+
attachSessionEvents(sessionId, sessionEmitter) {
|
|
2820
|
+
sessionEmitter.on("sync:connected", (event) => {
|
|
2821
|
+
this.metrics.recordHandshake(sessionId, event.nodeId);
|
|
2822
|
+
this.logger.log({
|
|
2823
|
+
timestamp: Date.now(),
|
|
2824
|
+
level: "info",
|
|
2825
|
+
event: "session.handshake",
|
|
2826
|
+
sessionId,
|
|
2827
|
+
nodeId: event.nodeId
|
|
2828
|
+
});
|
|
2829
|
+
});
|
|
2830
|
+
sessionEmitter.on("sync:received", (event) => {
|
|
2831
|
+
const byteSize = estimateByteSize(event.operations);
|
|
2832
|
+
this.metrics.recordReceived(sessionId, event.batchSize, byteSize);
|
|
2833
|
+
this.logger.log({
|
|
2834
|
+
timestamp: Date.now(),
|
|
2835
|
+
level: "info",
|
|
2836
|
+
event: "operations.received",
|
|
2837
|
+
sessionId,
|
|
2838
|
+
count: event.batchSize,
|
|
2839
|
+
bytes: byteSize
|
|
2840
|
+
});
|
|
2841
|
+
});
|
|
2842
|
+
sessionEmitter.on("sync:sent", (event) => {
|
|
2843
|
+
const byteSize = estimateByteSize(event.operations);
|
|
2844
|
+
this.metrics.recordSent(sessionId, event.batchSize, byteSize);
|
|
2845
|
+
this.logger.log({
|
|
2846
|
+
timestamp: Date.now(),
|
|
2847
|
+
level: "info",
|
|
2848
|
+
event: "operations.sent",
|
|
2849
|
+
sessionId,
|
|
2850
|
+
count: event.batchSize,
|
|
2851
|
+
bytes: byteSize
|
|
2852
|
+
});
|
|
2853
|
+
});
|
|
2854
|
+
sessionEmitter.on("sync:disconnected", (event) => {
|
|
2855
|
+
this.logger.log({
|
|
2856
|
+
timestamp: Date.now(),
|
|
2857
|
+
level: "info",
|
|
2858
|
+
event: "session.disconnected",
|
|
2859
|
+
sessionId,
|
|
2860
|
+
details: { reason: event.reason }
|
|
2861
|
+
});
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Get the metrics collector for external access (e.g., HTTP endpoints).
|
|
2866
|
+
*/
|
|
2867
|
+
getMetricsCollector() {
|
|
2868
|
+
return this.metrics;
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Get the logger for external access (e.g., event streaming).
|
|
2872
|
+
*/
|
|
2873
|
+
getLogger() {
|
|
2874
|
+
return this.logger;
|
|
2090
2875
|
}
|
|
2091
2876
|
/**
|
|
2092
2877
|
* Start the WebSocket server in standalone mode.
|
|
@@ -2127,12 +2912,25 @@ var KoraSyncServer = class {
|
|
|
2127
2912
|
this.handleConnection(transport);
|
|
2128
2913
|
});
|
|
2129
2914
|
this.running = true;
|
|
2915
|
+
this.logger.log({
|
|
2916
|
+
timestamp: Date.now(),
|
|
2917
|
+
level: "info",
|
|
2918
|
+
event: "server.started",
|
|
2919
|
+
details: { port: this.port, host: this.host, path: this.path }
|
|
2920
|
+
});
|
|
2130
2921
|
}
|
|
2131
2922
|
/**
|
|
2132
2923
|
* Stop the server. Closes all sessions and the WebSocket server.
|
|
2133
2924
|
*/
|
|
2134
2925
|
async stop() {
|
|
2926
|
+
this.logger.log({
|
|
2927
|
+
timestamp: Date.now(),
|
|
2928
|
+
level: "info",
|
|
2929
|
+
event: "server.stopping",
|
|
2930
|
+
details: { connectedClients: this.sessions.size }
|
|
2931
|
+
});
|
|
2135
2932
|
this.awarenessRelay.clear();
|
|
2933
|
+
this.yjsDocRelay.clear();
|
|
2136
2934
|
for (const session of this.sessions.values()) {
|
|
2137
2935
|
session.close("server shutting down");
|
|
2138
2936
|
}
|
|
@@ -2146,6 +2944,11 @@ var KoraSyncServer = class {
|
|
|
2146
2944
|
this.wsServer = null;
|
|
2147
2945
|
}
|
|
2148
2946
|
this.running = false;
|
|
2947
|
+
this.logger.log({
|
|
2948
|
+
timestamp: Date.now(),
|
|
2949
|
+
level: "info",
|
|
2950
|
+
event: "server.stopped"
|
|
2951
|
+
});
|
|
2149
2952
|
}
|
|
2150
2953
|
/**
|
|
2151
2954
|
* Handle one HTTP sync request for a long-polling client.
|
|
@@ -2195,44 +2998,119 @@ var KoraSyncServer = class {
|
|
|
2195
2998
|
retriable: true
|
|
2196
2999
|
});
|
|
2197
3000
|
transport.close(4029, "max connections reached");
|
|
3001
|
+
this.metrics.recordError();
|
|
3002
|
+
this.logger.log({
|
|
3003
|
+
timestamp: Date.now(),
|
|
3004
|
+
level: "warn",
|
|
3005
|
+
event: "connection.rejected",
|
|
3006
|
+
details: { reason: "max_connections", max: this.maxConnections }
|
|
3007
|
+
});
|
|
2198
3008
|
throw new SyncError4("Maximum connections reached", {
|
|
2199
3009
|
current: this.sessions.size,
|
|
2200
3010
|
max: this.maxConnections
|
|
2201
3011
|
});
|
|
2202
3012
|
}
|
|
2203
3013
|
const sessionId = generateUUIDv76();
|
|
3014
|
+
this.metrics.recordConnection(sessionId);
|
|
3015
|
+
const sessionEmitter = new SimpleEventEmitter();
|
|
3016
|
+
sessionEmitter.on("sync:connected", (event) => {
|
|
3017
|
+
this.metrics.recordHandshake(sessionId, event.nodeId);
|
|
3018
|
+
this.metrics.updateSessionState(sessionId, "authenticated");
|
|
3019
|
+
this.logger.log({
|
|
3020
|
+
timestamp: Date.now(),
|
|
3021
|
+
level: "info",
|
|
3022
|
+
event: "session.handshake",
|
|
3023
|
+
sessionId,
|
|
3024
|
+
nodeId: event.nodeId
|
|
3025
|
+
});
|
|
3026
|
+
});
|
|
3027
|
+
sessionEmitter.on("sync:received", (event) => {
|
|
3028
|
+
const byteSize = estimateOperationByteSize(event.operations);
|
|
3029
|
+
this.metrics.recordReceived(sessionId, event.batchSize, byteSize);
|
|
3030
|
+
this.logger.log({
|
|
3031
|
+
timestamp: Date.now(),
|
|
3032
|
+
level: "info",
|
|
3033
|
+
event: "operations.received",
|
|
3034
|
+
sessionId,
|
|
3035
|
+
count: event.batchSize,
|
|
3036
|
+
bytes: byteSize
|
|
3037
|
+
});
|
|
3038
|
+
});
|
|
3039
|
+
sessionEmitter.on("sync:sent", (event) => {
|
|
3040
|
+
const byteSize = estimateOperationByteSize(event.operations);
|
|
3041
|
+
this.metrics.recordSent(sessionId, event.batchSize, byteSize);
|
|
3042
|
+
this.logger.log({
|
|
3043
|
+
timestamp: Date.now(),
|
|
3044
|
+
level: "info",
|
|
3045
|
+
event: "operations.sent",
|
|
3046
|
+
sessionId,
|
|
3047
|
+
count: event.batchSize,
|
|
3048
|
+
bytes: byteSize
|
|
3049
|
+
});
|
|
3050
|
+
});
|
|
3051
|
+
sessionEmitter.on("sync:disconnected", () => {
|
|
3052
|
+
this.logger.log({
|
|
3053
|
+
timestamp: Date.now(),
|
|
3054
|
+
level: "info",
|
|
3055
|
+
event: "session.disconnected",
|
|
3056
|
+
sessionId
|
|
3057
|
+
});
|
|
3058
|
+
});
|
|
2204
3059
|
const session = new ClientSession({
|
|
2205
3060
|
sessionId,
|
|
2206
3061
|
transport,
|
|
2207
3062
|
store: this.store,
|
|
2208
3063
|
auth: this.auth ?? void 0,
|
|
2209
3064
|
serializer: this.serializer,
|
|
2210
|
-
emitter:
|
|
3065
|
+
emitter: sessionEmitter,
|
|
2211
3066
|
batchSize: this.batchSize,
|
|
2212
3067
|
schemaVersion: this.schemaVersion,
|
|
3068
|
+
supportedSchemaVersions: this.supportedSchemaVersions,
|
|
2213
3069
|
onRelay: (sourceSessionId, operations2) => {
|
|
2214
3070
|
this.handleRelay(sourceSessionId, operations2);
|
|
2215
3071
|
},
|
|
2216
3072
|
onAwarenessUpdate: (sourceSessionId, message) => {
|
|
2217
3073
|
this.handleAwarenessRelay(sourceSessionId, message);
|
|
2218
3074
|
},
|
|
3075
|
+
onYjsDocUpdate: (sourceSessionId, message) => {
|
|
3076
|
+
this.handleYjsDocRelay(sourceSessionId, message);
|
|
3077
|
+
},
|
|
2219
3078
|
onClose: (sid) => {
|
|
2220
3079
|
this.handleSessionClose(sid);
|
|
2221
3080
|
}
|
|
2222
3081
|
});
|
|
2223
3082
|
this.sessions.set(sessionId, session);
|
|
3083
|
+
this.yjsDocRelay.addClient(sessionId, transport);
|
|
2224
3084
|
session.start();
|
|
3085
|
+
this.logger.log({
|
|
3086
|
+
timestamp: Date.now(),
|
|
3087
|
+
level: "info",
|
|
3088
|
+
event: "session.connected",
|
|
3089
|
+
sessionId,
|
|
3090
|
+
details: { totalSessions: this.sessions.size }
|
|
3091
|
+
});
|
|
2225
3092
|
return sessionId;
|
|
2226
3093
|
}
|
|
2227
3094
|
/**
|
|
2228
3095
|
* Get the current server status.
|
|
2229
3096
|
*/
|
|
2230
3097
|
async getStatus() {
|
|
3098
|
+
const totalOps = await this.store.getOperationCount();
|
|
3099
|
+
const snapshot = this.metrics.getSnapshot(totalOps);
|
|
2231
3100
|
return {
|
|
2232
3101
|
running: this.running,
|
|
2233
|
-
connectedClients:
|
|
3102
|
+
connectedClients: snapshot.connectedClients,
|
|
2234
3103
|
port: this.port ?? null,
|
|
2235
|
-
totalOperations:
|
|
3104
|
+
totalOperations: snapshot.totalOperations,
|
|
3105
|
+
uptime: snapshot.uptime,
|
|
3106
|
+
version: this.serverVersion,
|
|
3107
|
+
schemaVersion: this.schemaVersion,
|
|
3108
|
+
connectedNodeIds: snapshot.connectedNodeIds,
|
|
3109
|
+
peakConnections: snapshot.peakConnections,
|
|
3110
|
+
connectionsTotal: snapshot.connectionsTotal,
|
|
3111
|
+
operationsReceived: snapshot.operationsReceived,
|
|
3112
|
+
operationsSent: snapshot.operationsSent,
|
|
3113
|
+
errorCount: snapshot.errorCount
|
|
2236
3114
|
};
|
|
2237
3115
|
}
|
|
2238
3116
|
/**
|
|
@@ -2243,13 +3121,31 @@ var KoraSyncServer = class {
|
|
|
2243
3121
|
}
|
|
2244
3122
|
// --- Private ---
|
|
2245
3123
|
handleRelay(sourceSessionId, operations2) {
|
|
3124
|
+
const targetCount = this.sessions.size - 1;
|
|
3125
|
+
const byteSize = estimateOperationByteSize(operations2);
|
|
3126
|
+
this.metrics.recordSent(
|
|
3127
|
+
sourceSessionId,
|
|
3128
|
+
operations2.length * targetCount,
|
|
3129
|
+
byteSize * targetCount
|
|
3130
|
+
);
|
|
3131
|
+
this.logger.log({
|
|
3132
|
+
timestamp: Date.now(),
|
|
3133
|
+
level: "info",
|
|
3134
|
+
event: "operations.relayed",
|
|
3135
|
+
sessionId: sourceSessionId,
|
|
3136
|
+
count: operations2.length,
|
|
3137
|
+
bytes: byteSize * targetCount,
|
|
3138
|
+
details: { targetSessions: targetCount }
|
|
3139
|
+
});
|
|
2246
3140
|
for (const [sessionId, session] of this.sessions) {
|
|
2247
3141
|
if (sessionId === sourceSessionId) continue;
|
|
2248
3142
|
session.relayOperations(operations2);
|
|
2249
3143
|
}
|
|
2250
3144
|
}
|
|
2251
3145
|
handleSessionClose(sessionId) {
|
|
3146
|
+
this.metrics.recordDisconnection(sessionId);
|
|
2252
3147
|
this.awarenessRelay.removeClient(sessionId);
|
|
3148
|
+
this.yjsDocRelay.removeClient(sessionId);
|
|
2253
3149
|
this.sessions.delete(sessionId);
|
|
2254
3150
|
const clientId = this.httpSessionToClient.get(sessionId);
|
|
2255
3151
|
if (clientId) {
|
|
@@ -2266,6 +3162,12 @@ var KoraSyncServer = class {
|
|
|
2266
3162
|
this.awarenessRelay.addClient(sourceSessionId, message.clientId, transport);
|
|
2267
3163
|
this.awarenessRelay.handleUpdate(sourceSessionId, message);
|
|
2268
3164
|
}
|
|
3165
|
+
handleYjsDocRelay(sourceSessionId, message) {
|
|
3166
|
+
if (!this.sessions.has(sourceSessionId)) {
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
this.yjsDocRelay.handleUpdate(sourceSessionId, message);
|
|
3170
|
+
}
|
|
2269
3171
|
getOrCreateHttpClient(clientId) {
|
|
2270
3172
|
const existing = this.httpClients.get(clientId);
|
|
2271
3173
|
if (existing) {
|
|
@@ -2279,6 +3181,13 @@ var KoraSyncServer = class {
|
|
|
2279
3181
|
return client;
|
|
2280
3182
|
}
|
|
2281
3183
|
};
|
|
3184
|
+
function estimateOperationByteSize(operations2) {
|
|
3185
|
+
let total = 0;
|
|
3186
|
+
for (const op of operations2) {
|
|
3187
|
+
total += JSON.stringify(op).length;
|
|
3188
|
+
}
|
|
3189
|
+
return total;
|
|
3190
|
+
}
|
|
2282
3191
|
function normalizeHttpBody(body, contentType) {
|
|
2283
3192
|
if (body instanceof Uint8Array) {
|
|
2284
3193
|
return body;
|
|
@@ -2320,7 +3229,7 @@ var KoraAuthProvider = class {
|
|
|
2320
3229
|
this.resolveScopes = options.resolveScopes;
|
|
2321
3230
|
}
|
|
2322
3231
|
async authenticate(token) {
|
|
2323
|
-
const payload = this.tokenValidator.validateToken(token);
|
|
3232
|
+
const payload = this.tokenValidator.validateTokenWithRevocation ? await this.tokenValidator.validateTokenWithRevocation(token) : this.tokenValidator.validateToken(token);
|
|
2324
3233
|
if (payload === null) {
|
|
2325
3234
|
return null;
|
|
2326
3235
|
}
|
|
@@ -2397,9 +3306,143 @@ function createProductionServer(config) {
|
|
|
2397
3306
|
const syncPath = config.syncPath ?? "/kora-sync";
|
|
2398
3307
|
const syncServer = new KoraSyncServer({
|
|
2399
3308
|
store: config.store,
|
|
3309
|
+
enableDashboard: true,
|
|
2400
3310
|
...config.syncOptions
|
|
2401
3311
|
});
|
|
2402
3312
|
let httpServer = null;
|
|
3313
|
+
function getOperationalToken(kind) {
|
|
3314
|
+
const auth = config.operationalAuth;
|
|
3315
|
+
if (!auth) return void 0;
|
|
3316
|
+
if (kind === "metrics") return auth.metricsToken || auth.adminToken;
|
|
3317
|
+
if (kind === "backup") return auth.backupToken || auth.adminToken;
|
|
3318
|
+
return auth.adminToken;
|
|
3319
|
+
}
|
|
3320
|
+
function extractRequestToken(req) {
|
|
3321
|
+
const authorization = req.headers.authorization;
|
|
3322
|
+
if (authorization?.startsWith("Bearer ")) {
|
|
3323
|
+
return authorization.slice("Bearer ".length).trim();
|
|
3324
|
+
}
|
|
3325
|
+
const headerNames = ["x-kora-admin-token", "x-kora-metrics-token", "x-kora-backup-token"];
|
|
3326
|
+
for (const name of headerNames) {
|
|
3327
|
+
const value = req.headers[name];
|
|
3328
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
3329
|
+
if (Array.isArray(value) && typeof value[0] === "string" && value[0].length > 0) {
|
|
3330
|
+
return value[0];
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
return null;
|
|
3334
|
+
}
|
|
3335
|
+
function isOperationalRequestAllowed(req, kind) {
|
|
3336
|
+
const expected = getOperationalToken(kind);
|
|
3337
|
+
if (!expected) return true;
|
|
3338
|
+
return extractRequestToken(req) === expected;
|
|
3339
|
+
}
|
|
3340
|
+
function rejectUnauthorized(res) {
|
|
3341
|
+
res.writeHead(401, {
|
|
3342
|
+
"Content-Type": "application/json",
|
|
3343
|
+
"WWW-Authenticate": 'Bearer realm="kora"'
|
|
3344
|
+
});
|
|
3345
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
3346
|
+
}
|
|
3347
|
+
function formatPrometheusMetrics() {
|
|
3348
|
+
const status = syncServer.getMetricsCollector().getSnapshot(0);
|
|
3349
|
+
const lines = [
|
|
3350
|
+
"# HELP kora_connected_clients Current number of connected clients",
|
|
3351
|
+
"# TYPE kora_connected_clients gauge",
|
|
3352
|
+
`kora_connected_clients ${status.connectedClients}`,
|
|
3353
|
+
"",
|
|
3354
|
+
"# HELP kora_peak_connections Peak number of simultaneous connections since server start",
|
|
3355
|
+
"# TYPE kora_peak_connections gauge",
|
|
3356
|
+
`kora_peak_connections ${status.peakConnections}`,
|
|
3357
|
+
"",
|
|
3358
|
+
"# HELP kora_connections_total Total number of connections handled since server start",
|
|
3359
|
+
"# TYPE kora_connections_total counter",
|
|
3360
|
+
`kora_connections_total ${status.connectionsTotal}`,
|
|
3361
|
+
"",
|
|
3362
|
+
"# HELP kora_operations_received_total Total operations received from clients",
|
|
3363
|
+
"# TYPE kora_operations_received_total counter",
|
|
3364
|
+
`kora_operations_received_total ${status.operationsReceived}`,
|
|
3365
|
+
"",
|
|
3366
|
+
"# HELP kora_operations_sent_total Total operations sent to clients",
|
|
3367
|
+
"# TYPE kora_operations_sent_total counter",
|
|
3368
|
+
`kora_operations_sent_total ${status.operationsSent}`,
|
|
3369
|
+
"",
|
|
3370
|
+
"# HELP kora_bytes_received_total Total bytes received from clients",
|
|
3371
|
+
"# TYPE kora_bytes_received_total counter",
|
|
3372
|
+
`kora_bytes_received_total ${status.bytesReceived}`,
|
|
3373
|
+
"",
|
|
3374
|
+
"# HELP kora_bytes_sent_total Total bytes sent to clients",
|
|
3375
|
+
"# TYPE kora_bytes_sent_total counter",
|
|
3376
|
+
`kora_bytes_sent_total ${status.bytesSent}`,
|
|
3377
|
+
"",
|
|
3378
|
+
"# HELP kora_errors_total Total errors since server start",
|
|
3379
|
+
"# TYPE kora_errors_total counter",
|
|
3380
|
+
`kora_errors_total ${status.errorCount}`,
|
|
3381
|
+
"",
|
|
3382
|
+
"# HELP kora_uptime_seconds Server uptime in seconds",
|
|
3383
|
+
"# TYPE kora_uptime_seconds gauge",
|
|
3384
|
+
`kora_uptime_seconds ${Math.floor(status.uptime / 1e3)}`,
|
|
3385
|
+
"",
|
|
3386
|
+
"# HELP kora_schema_version Schema version the server expects",
|
|
3387
|
+
"# TYPE kora_schema_version gauge",
|
|
3388
|
+
`kora_schema_version ${status.schemaVersion}`,
|
|
3389
|
+
""
|
|
3390
|
+
];
|
|
3391
|
+
return lines.join("\n");
|
|
3392
|
+
}
|
|
3393
|
+
function readBodyBuffer(req) {
|
|
3394
|
+
return new Promise((resolve) => {
|
|
3395
|
+
const chunks = [];
|
|
3396
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
3397
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
async function readJsonBody(req) {
|
|
3401
|
+
const buffer = await readBodyBuffer(req);
|
|
3402
|
+
if (buffer.byteLength === 0) return void 0;
|
|
3403
|
+
try {
|
|
3404
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
3405
|
+
} catch {
|
|
3406
|
+
return void 0;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
function matchesRoutePrefix(pathname, prefix) {
|
|
3410
|
+
const normalizedPrefix = normalizeRoutePath(prefix);
|
|
3411
|
+
return pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`);
|
|
3412
|
+
}
|
|
3413
|
+
function normalizeRoutePath(path) {
|
|
3414
|
+
const prefixed = path.startsWith("/") ? path : `/${path}`;
|
|
3415
|
+
return prefixed.length > 1 ? prefixed.replace(/\/+$/, "") : prefixed;
|
|
3416
|
+
}
|
|
3417
|
+
function getClientIp(req) {
|
|
3418
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
3419
|
+
if (typeof forwarded === "string" && forwarded.length > 0) {
|
|
3420
|
+
return forwarded.split(",")[0]?.trim();
|
|
3421
|
+
}
|
|
3422
|
+
return req.socket.remoteAddress;
|
|
3423
|
+
}
|
|
3424
|
+
function getQuery(url) {
|
|
3425
|
+
const query = {};
|
|
3426
|
+
for (const [key, value] of url.searchParams) {
|
|
3427
|
+
const existing = query[key];
|
|
3428
|
+
if (existing === void 0) {
|
|
3429
|
+
query[key] = value;
|
|
3430
|
+
} else if (Array.isArray(existing)) {
|
|
3431
|
+
existing.push(value);
|
|
3432
|
+
} else {
|
|
3433
|
+
query[key] = [existing, value];
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
return query;
|
|
3437
|
+
}
|
|
3438
|
+
function writeJsonResponse(res, result) {
|
|
3439
|
+
const headers = {
|
|
3440
|
+
"Content-Type": "application/json",
|
|
3441
|
+
...result.headers ?? {}
|
|
3442
|
+
};
|
|
3443
|
+
res.writeHead(result.status, headers);
|
|
3444
|
+
res.end(JSON.stringify(result.body ?? null));
|
|
3445
|
+
}
|
|
2403
3446
|
return {
|
|
2404
3447
|
async start() {
|
|
2405
3448
|
const { createServer } = await import("http");
|
|
@@ -2407,13 +3450,137 @@ function createProductionServer(config) {
|
|
|
2407
3450
|
const { extname, join, resolve } = await import("path");
|
|
2408
3451
|
const { WebSocketServer } = await import("ws");
|
|
2409
3452
|
const distDir = resolve(staticDir);
|
|
2410
|
-
httpServer = createServer((req, res) => {
|
|
3453
|
+
httpServer = createServer(async (req, res) => {
|
|
2411
3454
|
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
2412
3455
|
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
2413
3456
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
2414
3457
|
if (url.pathname === "/health") {
|
|
3458
|
+
const status = await syncServer.getStatus();
|
|
3459
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3460
|
+
res.end(
|
|
3461
|
+
JSON.stringify({
|
|
3462
|
+
status: "ok",
|
|
3463
|
+
version: status.version,
|
|
3464
|
+
uptime: status.uptime,
|
|
3465
|
+
connectedClients: status.connectedClients,
|
|
3466
|
+
totalOperations: status.totalOperations,
|
|
3467
|
+
timestamp: Date.now()
|
|
3468
|
+
})
|
|
3469
|
+
);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
if (url.pathname === "/__kora/status") {
|
|
3473
|
+
if (!isOperationalRequestAllowed(req, "admin")) {
|
|
3474
|
+
rejectUnauthorized(res);
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
const status = await syncServer.getStatus();
|
|
2415
3478
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2416
|
-
res.end(JSON.stringify(
|
|
3479
|
+
res.end(JSON.stringify(status, null, 2));
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
if (url.pathname === "/__kora/metrics") {
|
|
3483
|
+
if (!isOperationalRequestAllowed(req, "metrics")) {
|
|
3484
|
+
rejectUnauthorized(res);
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
3487
|
+
res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" });
|
|
3488
|
+
res.end(formatPrometheusMetrics());
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
if (url.pathname === "/__kora/events") {
|
|
3492
|
+
if (!isOperationalRequestAllowed(req, "admin")) {
|
|
3493
|
+
rejectUnauthorized(res);
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
res.writeHead(200, {
|
|
3497
|
+
"Content-Type": "text/event-stream",
|
|
3498
|
+
"Cache-Control": "no-cache",
|
|
3499
|
+
Connection: "keep-alive",
|
|
3500
|
+
"X-Accel-Buffering": "no"
|
|
3501
|
+
});
|
|
3502
|
+
const status = await syncServer.getStatus();
|
|
3503
|
+
res.write(`event: status
|
|
3504
|
+
data: ${JSON.stringify(status)}
|
|
3505
|
+
|
|
3506
|
+
`);
|
|
3507
|
+
const interval = setInterval(async () => {
|
|
3508
|
+
try {
|
|
3509
|
+
const s = await syncServer.getStatus();
|
|
3510
|
+
res.write(`event: status
|
|
3511
|
+
data: ${JSON.stringify(s)}
|
|
3512
|
+
|
|
3513
|
+
`);
|
|
3514
|
+
} catch {
|
|
3515
|
+
}
|
|
3516
|
+
}, 2e3);
|
|
3517
|
+
req.on("close", () => {
|
|
3518
|
+
clearInterval(interval);
|
|
3519
|
+
});
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
if (url.pathname === "/__kora" || url.pathname === "/__kora/") {
|
|
3523
|
+
if (!isOperationalRequestAllowed(req, "admin")) {
|
|
3524
|
+
rejectUnauthorized(res);
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
const status = await syncServer.getStatus();
|
|
3528
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3529
|
+
res.end(renderDashboardHtml(status));
|
|
3530
|
+
return;
|
|
3531
|
+
}
|
|
3532
|
+
if (url.pathname === "/__kora/backup/export" && req.method === "POST") {
|
|
3533
|
+
if (!isOperationalRequestAllowed(req, "backup")) {
|
|
3534
|
+
rejectUnauthorized(res);
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
3537
|
+
try {
|
|
3538
|
+
const backup = await config.store.exportBackup();
|
|
3539
|
+
res.writeHead(200, {
|
|
3540
|
+
"Content-Type": "application/octet-stream",
|
|
3541
|
+
"Content-Disposition": `attachment; filename="kora-backup-${Date.now()}.kora"`,
|
|
3542
|
+
"Content-Length": String(backup.byteLength)
|
|
3543
|
+
});
|
|
3544
|
+
res.end(Buffer.from(backup));
|
|
3545
|
+
} catch (error) {
|
|
3546
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
3547
|
+
res.end(JSON.stringify({ error: "Backup failed", message: error.message }));
|
|
3548
|
+
}
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
3551
|
+
if (url.pathname === "/__kora/backup/import" && req.method === "POST") {
|
|
3552
|
+
if (!isOperationalRequestAllowed(req, "backup")) {
|
|
3553
|
+
rejectUnauthorized(res);
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
try {
|
|
3557
|
+
const body = await readBodyBuffer(req);
|
|
3558
|
+
const merge = url.searchParams.get("merge") === "true";
|
|
3559
|
+
const result = await config.store.importBackup(
|
|
3560
|
+
new Uint8Array(body.buffer, body.byteOffset, body.byteLength),
|
|
3561
|
+
merge
|
|
3562
|
+
);
|
|
3563
|
+
res.writeHead(result.success ? 200 : 400, { "Content-Type": "application/json" });
|
|
3564
|
+
res.end(JSON.stringify(result));
|
|
3565
|
+
} catch (error) {
|
|
3566
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
3567
|
+
res.end(JSON.stringify({ error: "Restore failed", message: error.message }));
|
|
3568
|
+
}
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
const customRoute = config.httpRoutes?.find(
|
|
3572
|
+
(route) => matchesRoutePrefix(url.pathname, route.path)
|
|
3573
|
+
);
|
|
3574
|
+
if (customRoute) {
|
|
3575
|
+
const result = await customRoute.handle({
|
|
3576
|
+
method: req.method ?? "GET",
|
|
3577
|
+
path: url.pathname,
|
|
3578
|
+
body: await readJsonBody(req),
|
|
3579
|
+
headers: req.headers,
|
|
3580
|
+
query: getQuery(url),
|
|
3581
|
+
ip: getClientIp(req)
|
|
3582
|
+
});
|
|
3583
|
+
writeJsonResponse(res, result);
|
|
2417
3584
|
return;
|
|
2418
3585
|
}
|
|
2419
3586
|
let filePath = join(distDir, url.pathname);
|
|
@@ -2475,6 +3642,82 @@ function createProductionServer(config) {
|
|
|
2475
3642
|
}
|
|
2476
3643
|
};
|
|
2477
3644
|
}
|
|
3645
|
+
function renderDashboardHtml(status) {
|
|
3646
|
+
const version = status.version;
|
|
3647
|
+
const uptime = formatUptime(status.uptime);
|
|
3648
|
+
return `<!DOCTYPE html>
|
|
3649
|
+
<html lang="en">
|
|
3650
|
+
<head>
|
|
3651
|
+
<meta charset="UTF-8">
|
|
3652
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3653
|
+
<title>Kora Dashboard</title>
|
|
3654
|
+
<style>
|
|
3655
|
+
* { box-sizing: border-box; margin: 0; padding: 0 }
|
|
3656
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem }
|
|
3657
|
+
h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.25rem }
|
|
3658
|
+
.subtitle { color: #64748b; margin-bottom: 2rem; font-size: 0.875rem }
|
|
3659
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem }
|
|
3660
|
+
.card { background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; padding: 1.25rem }
|
|
3661
|
+
.card .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; margin-bottom: 0.5rem }
|
|
3662
|
+
.card .value { font-size: 1.75rem; font-weight: 700; color: #38bdf8 }
|
|
3663
|
+
.card .value.green { color: #4ade80 }
|
|
3664
|
+
.card .value.red { color: #f87171 }
|
|
3665
|
+
.card .value.yellow { color: #fbbf24 }
|
|
3666
|
+
.section-title { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; margin-top: 1.5rem }
|
|
3667
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.875rem }
|
|
3668
|
+
th { text-align: left; padding: 0.5rem 0.75rem; color: #64748b; font-weight: 500; border-bottom: 1px solid #334155 }
|
|
3669
|
+
td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #1e293b }
|
|
3670
|
+
.status-dot { display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; margin-right: 0.375rem }
|
|
3671
|
+
.status-dot.running { background: #4ade80 }
|
|
3672
|
+
.status-dot.stopped { background: #f87171 }
|
|
3673
|
+
</style>
|
|
3674
|
+
</head>
|
|
3675
|
+
<body>
|
|
3676
|
+
<h1>Kora Sync Server</h1>
|
|
3677
|
+
<p class="subtitle">v${version} · <span class="status-dot running"></span>Running</p>
|
|
3678
|
+
<div class="grid">
|
|
3679
|
+
<div class="card"><div class="label">Uptime</div><div class="value">${uptime}</div></div>
|
|
3680
|
+
<div class="card"><div class="label">Connected Clients</div><div class="value" id="connectedClients">${status.connectedClients}</div></div>
|
|
3681
|
+
<div class="card"><div class="label">Total Operations</div><div class="value" id="totalOperations">${status.totalOperations}</div></div>
|
|
3682
|
+
<div class="card"><div class="label">Peak Connections</div><div class="value green" id="peakConnections">${status.peakConnections}</div></div>
|
|
3683
|
+
<div class="card"><div class="label">Ops Received</div><div class="value" id="opsReceived">${status.operationsReceived}</div></div>
|
|
3684
|
+
<div class="card"><div class="label">Ops Sent</div><div class="value" id="opsSent">${status.operationsSent}</div></div>
|
|
3685
|
+
<div class="card"><div class="label">Errors</div><div class="value ${status.errorCount > 0 ? "red" : "green"}" id="errors">${status.errorCount}</div></div>
|
|
3686
|
+
<div class="card"><div class="label">Schema Version</div><div class="value">${status.schemaVersion}</div></div>
|
|
3687
|
+
</div>
|
|
3688
|
+
<script>
|
|
3689
|
+
(function() {
|
|
3690
|
+
const es = new EventSource('/__kora/events');
|
|
3691
|
+
es.addEventListener('status', (e) => {
|
|
3692
|
+
const s = JSON.parse(e.data);
|
|
3693
|
+
for (const [id, val] of Object.entries({
|
|
3694
|
+
connectedClients: s.connectedClients,
|
|
3695
|
+
totalOperations: s.totalOperations,
|
|
3696
|
+
peakConnections: s.peakConnections,
|
|
3697
|
+
opsReceived: s.operationsReceived,
|
|
3698
|
+
opsSent: s.operationsSent,
|
|
3699
|
+
errors: s.errorCount,
|
|
3700
|
+
})) {
|
|
3701
|
+
const el = document.getElementById(id);
|
|
3702
|
+
if (el) { el.textContent = String(val); el.className = 'value' + (id === 'errors' && val > 0 ? ' red' : id === 'errors' ? ' green' : ''); }
|
|
3703
|
+
}
|
|
3704
|
+
});
|
|
3705
|
+
es.onerror = () => { setTimeout(() => document.location.reload(), 5000); };
|
|
3706
|
+
})();
|
|
3707
|
+
</script>
|
|
3708
|
+
</body>
|
|
3709
|
+
</html>`;
|
|
3710
|
+
}
|
|
3711
|
+
function formatUptime(ms) {
|
|
3712
|
+
const seconds = Math.floor(ms / 1e3);
|
|
3713
|
+
const minutes = Math.floor(seconds / 60);
|
|
3714
|
+
const hours = Math.floor(minutes / 60);
|
|
3715
|
+
const parts = [];
|
|
3716
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
3717
|
+
if (minutes % 60 > 0) parts.push(`${minutes % 60}m`);
|
|
3718
|
+
parts.push(`${seconds % 60}s`);
|
|
3719
|
+
return parts.join(" ");
|
|
3720
|
+
}
|
|
2478
3721
|
export {
|
|
2479
3722
|
AwarenessRelay,
|
|
2480
3723
|
ClientSession,
|
|
@@ -2488,9 +3731,13 @@ export {
|
|
|
2488
3731
|
SqliteServerStore,
|
|
2489
3732
|
TokenAuthProvider,
|
|
2490
3733
|
WsServerTransport,
|
|
3734
|
+
createDefaultLogger,
|
|
3735
|
+
createJsonLogger,
|
|
2491
3736
|
createKoraServer,
|
|
2492
3737
|
createPostgresServerStore,
|
|
3738
|
+
createPrettyLogger,
|
|
2493
3739
|
createProductionServer,
|
|
3740
|
+
createSilentLogger,
|
|
2494
3741
|
createSqliteServerStore
|
|
2495
3742
|
};
|
|
2496
3743
|
//# sourceMappingURL=index.js.map
|