@korajs/server 0.3.3 → 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 +43 -0
- package/dist/index.cjs +1691 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +442 -9
- package/dist/index.d.ts +442 -9
- package/dist/index.js +1517 -100
- 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.cjs
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,34 +30,230 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
34
|
+
var getImportMetaUrl, importMetaUrl;
|
|
35
|
+
var init_cjs_shims = __esm({
|
|
36
|
+
"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
39
|
+
importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// src/store/server-backup.ts
|
|
44
|
+
var server_backup_exports = {};
|
|
45
|
+
__export(server_backup_exports, {
|
|
46
|
+
buildServerBackup: () => buildServerBackup,
|
|
47
|
+
parseServerBackup: () => parseServerBackup
|
|
48
|
+
});
|
|
49
|
+
function encodeSection(name, content) {
|
|
50
|
+
const nameBytes = new TextEncoder().encode(name);
|
|
51
|
+
const header = new Uint8Array(8);
|
|
52
|
+
const dv = new DataView(header.buffer);
|
|
53
|
+
dv.setUint32(0, nameBytes.length, true);
|
|
54
|
+
dv.setUint32(4, content.length, true);
|
|
55
|
+
const result = new Uint8Array(header.length + nameBytes.length + content.length);
|
|
56
|
+
result.set(header, 0);
|
|
57
|
+
result.set(nameBytes, 8);
|
|
58
|
+
result.set(content, 8 + nameBytes.length);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function encodeJsonSection(name, data) {
|
|
62
|
+
return encodeSection(name, new TextEncoder().encode(JSON.stringify(data)));
|
|
63
|
+
}
|
|
64
|
+
function parseSections(data) {
|
|
65
|
+
const sections = [];
|
|
66
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
67
|
+
let offset = 0;
|
|
68
|
+
while (offset + 8 <= data.byteLength) {
|
|
69
|
+
const nameLen = dv.getUint32(offset, true);
|
|
70
|
+
const contentLen = dv.getUint32(offset + 4, true);
|
|
71
|
+
offset += 8;
|
|
72
|
+
if (offset + nameLen + contentLen > data.byteLength) break;
|
|
73
|
+
const name = new TextDecoder().decode(data.slice(offset, offset + nameLen));
|
|
74
|
+
offset += nameLen;
|
|
75
|
+
const content = data.slice(offset, offset + contentLen);
|
|
76
|
+
offset += contentLen;
|
|
77
|
+
sections.push({ name, content });
|
|
78
|
+
}
|
|
79
|
+
return sections;
|
|
80
|
+
}
|
|
81
|
+
function findSection(sections, name) {
|
|
82
|
+
for (const s of sections) {
|
|
83
|
+
if (s.name === name) return s.content;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function parseJsonSection(sections, name) {
|
|
88
|
+
const content = findSection(sections, name);
|
|
89
|
+
if (!content) return null;
|
|
90
|
+
return JSON.parse(new TextDecoder().decode(content));
|
|
91
|
+
}
|
|
92
|
+
async function computeSha256(data) {
|
|
93
|
+
const digestInput = new Uint8Array(data);
|
|
94
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", digestInput);
|
|
95
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
96
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
97
|
+
}
|
|
98
|
+
async function buildServerBackup(nodeId, operations2, versionVector) {
|
|
99
|
+
const sections = [];
|
|
100
|
+
let allContentForChecksum = new Uint8Array(0);
|
|
101
|
+
const addSection = (name, data) => {
|
|
102
|
+
sections.push(data);
|
|
103
|
+
const newLen = allContentForChecksum.length + data.length;
|
|
104
|
+
const combined = new Uint8Array(newLen);
|
|
105
|
+
combined.set(allContentForChecksum, 0);
|
|
106
|
+
combined.set(data, allContentForChecksum.length);
|
|
107
|
+
allContentForChecksum = combined;
|
|
108
|
+
};
|
|
109
|
+
const vvObj = {};
|
|
110
|
+
for (const [nid, seq] of versionVector) {
|
|
111
|
+
vvObj[nid] = seq;
|
|
112
|
+
}
|
|
113
|
+
addSection("version_vector", encodeJsonSection("version_vector", vvObj));
|
|
114
|
+
const opLines = `${operations2.map((op) => JSON.stringify(op)).join("\n")}
|
|
115
|
+
`;
|
|
116
|
+
addSection("operations", encodeSection("operations", new TextEncoder().encode(opLines)));
|
|
117
|
+
const checksumHex = await computeSha256(allContentForChecksum);
|
|
118
|
+
const manifest = {
|
|
119
|
+
version: BACKUP_VERSION,
|
|
120
|
+
createdAt: Date.now(),
|
|
121
|
+
nodeId,
|
|
122
|
+
schemaVersion: 1,
|
|
123
|
+
operationCount: operations2.length,
|
|
124
|
+
collections: [],
|
|
125
|
+
includesRecords: false,
|
|
126
|
+
checksum: checksumHex
|
|
127
|
+
};
|
|
128
|
+
const manifestSection = encodeJsonSection("manifest", manifest);
|
|
129
|
+
const checksumSection = encodeSection("checksum", new TextEncoder().encode(checksumHex));
|
|
130
|
+
const totalLen = manifestSection.length + allContentForChecksum.length + checksumSection.length;
|
|
131
|
+
const result = new Uint8Array(totalLen);
|
|
132
|
+
let pos = 0;
|
|
133
|
+
result.set(manifestSection, pos);
|
|
134
|
+
pos += manifestSection.length;
|
|
135
|
+
result.set(allContentForChecksum, pos);
|
|
136
|
+
pos += allContentForChecksum.length;
|
|
137
|
+
result.set(checksumSection, pos);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
function parseServerBackup(data) {
|
|
141
|
+
const sections = parseSections(data);
|
|
142
|
+
const opsContent = findSection(sections, "operations");
|
|
143
|
+
let operations2 = [];
|
|
144
|
+
if (opsContent) {
|
|
145
|
+
const text3 = new TextDecoder().decode(opsContent);
|
|
146
|
+
const lines = text3.trim().split("\n").filter((l) => l.length > 0);
|
|
147
|
+
operations2 = lines.map((line) => JSON.parse(line));
|
|
148
|
+
}
|
|
149
|
+
const vvData = parseJsonSection(sections, "version_vector");
|
|
150
|
+
const versionVector = /* @__PURE__ */ new Map();
|
|
151
|
+
if (vvData) {
|
|
152
|
+
for (const [nid, seq] of Object.entries(vvData)) {
|
|
153
|
+
versionVector.set(nid, seq);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { operations: operations2, versionVector };
|
|
157
|
+
}
|
|
158
|
+
var BACKUP_VERSION;
|
|
159
|
+
var init_server_backup = __esm({
|
|
160
|
+
"src/store/server-backup.ts"() {
|
|
161
|
+
"use strict";
|
|
162
|
+
init_cjs_shims();
|
|
163
|
+
BACKUP_VERSION = 1;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
30
167
|
// src/index.ts
|
|
31
168
|
var index_exports = {};
|
|
32
169
|
__export(index_exports, {
|
|
170
|
+
AwarenessRelay: () => AwarenessRelay,
|
|
33
171
|
ClientSession: () => ClientSession,
|
|
34
172
|
HttpServerTransport: () => HttpServerTransport,
|
|
35
173
|
KoraAuthProvider: () => KoraAuthProvider,
|
|
36
174
|
KoraSyncServer: () => KoraSyncServer,
|
|
37
175
|
MemoryServerStore: () => MemoryServerStore,
|
|
176
|
+
MixedAuthProvider: () => MixedAuthProvider,
|
|
38
177
|
NoAuthProvider: () => NoAuthProvider,
|
|
39
178
|
PostgresServerStore: () => PostgresServerStore,
|
|
40
179
|
SqliteServerStore: () => SqliteServerStore,
|
|
41
180
|
TokenAuthProvider: () => TokenAuthProvider,
|
|
42
181
|
WsServerTransport: () => WsServerTransport,
|
|
182
|
+
createDefaultLogger: () => createDefaultLogger,
|
|
183
|
+
createJsonLogger: () => createJsonLogger,
|
|
43
184
|
createKoraServer: () => createKoraServer,
|
|
44
185
|
createPostgresServerStore: () => createPostgresServerStore,
|
|
186
|
+
createPrettyLogger: () => createPrettyLogger,
|
|
45
187
|
createProductionServer: () => createProductionServer,
|
|
188
|
+
createSilentLogger: () => createSilentLogger,
|
|
46
189
|
createSqliteServerStore: () => createSqliteServerStore
|
|
47
190
|
});
|
|
48
191
|
module.exports = __toCommonJS(index_exports);
|
|
192
|
+
init_cjs_shims();
|
|
49
193
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
194
|
+
// src/logging/structured-logger.ts
|
|
195
|
+
init_cjs_shims();
|
|
196
|
+
function defaultJsonSerializer(entry) {
|
|
197
|
+
return JSON.stringify(entry);
|
|
198
|
+
}
|
|
199
|
+
function defaultPrettySerializer(entry) {
|
|
200
|
+
const time = new Date(entry.timestamp).toISOString().slice(11, 23);
|
|
201
|
+
const levelTag = { info: " INFO", warn: " WARN", error: "ERROR" }[entry.level];
|
|
202
|
+
const node = entry.nodeId ? ` [${entry.nodeId.slice(0, 8)}]` : "";
|
|
203
|
+
const session = entry.sessionId ? ` [session:${entry.sessionId.slice(0, 8)}]` : "";
|
|
204
|
+
const dur = entry.duration !== void 0 ? ` +${entry.duration}ms` : "";
|
|
205
|
+
const count3 = entry.count !== void 0 ? ` (${entry.count})` : "";
|
|
206
|
+
const err = entry.error ? ` \u2014 ${entry.error}` : "";
|
|
207
|
+
return `${time} ${levelTag}${node}${session} ${entry.event}${count3}${dur}${err}`;
|
|
208
|
+
}
|
|
209
|
+
function createJsonLogger(writer) {
|
|
210
|
+
const out = writer ?? {
|
|
211
|
+
info: (s) => process.stdout.write(`${s}
|
|
212
|
+
`),
|
|
213
|
+
error: (s) => process.stderr.write(`${s}
|
|
214
|
+
`)
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
log(entry) {
|
|
218
|
+
const line = defaultJsonSerializer(entry);
|
|
219
|
+
if (entry.level === "error") {
|
|
220
|
+
out.error(line);
|
|
221
|
+
} else {
|
|
222
|
+
out.info(line);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function createPrettyLogger(writer) {
|
|
228
|
+
const out = writer ?? { write: (s) => process.stdout.write(`${s}
|
|
229
|
+
`) };
|
|
230
|
+
return {
|
|
231
|
+
log(entry) {
|
|
232
|
+
out.write(defaultPrettySerializer(entry));
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function createSilentLogger() {
|
|
237
|
+
return { log() {
|
|
238
|
+
} };
|
|
239
|
+
}
|
|
240
|
+
function createDefaultLogger() {
|
|
241
|
+
switch (process.env.NODE_ENV) {
|
|
242
|
+
case "production":
|
|
243
|
+
return createJsonLogger();
|
|
244
|
+
case "test":
|
|
245
|
+
return createSilentLogger();
|
|
246
|
+
default:
|
|
247
|
+
return createPrettyLogger();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
53
250
|
|
|
54
251
|
// src/store/memory-server-store.ts
|
|
252
|
+
init_cjs_shims();
|
|
55
253
|
var import_core = require("@korajs/core");
|
|
56
254
|
|
|
57
255
|
// src/store/materialization.ts
|
|
256
|
+
init_cjs_shims();
|
|
58
257
|
function fieldTypeToSql(descriptor, dialect) {
|
|
59
258
|
switch (descriptor.kind) {
|
|
60
259
|
case "string":
|
|
@@ -116,9 +315,7 @@ ALTER TABLE ${name} ADD COLUMN ${colDef}`);
|
|
|
116
315
|
`CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${name} (${indexField})`
|
|
117
316
|
);
|
|
118
317
|
}
|
|
119
|
-
statements.push(
|
|
120
|
-
`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`
|
|
121
|
-
);
|
|
318
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`);
|
|
122
319
|
return statements;
|
|
123
320
|
}
|
|
124
321
|
function generateAllCollectionDDL(schema, dialect) {
|
|
@@ -212,6 +409,9 @@ var MemoryServerStore = class {
|
|
|
212
409
|
getNodeId() {
|
|
213
410
|
return this.nodeId;
|
|
214
411
|
}
|
|
412
|
+
getSchema() {
|
|
413
|
+
return this.schema;
|
|
414
|
+
}
|
|
215
415
|
async setSchema(schema) {
|
|
216
416
|
this.assertOpen();
|
|
217
417
|
this.schema = schema;
|
|
@@ -233,7 +433,7 @@ var MemoryServerStore = class {
|
|
|
233
433
|
if (op.sequenceNumber > currentSeq) {
|
|
234
434
|
this.versionVector.set(op.nodeId, op.sequenceNumber);
|
|
235
435
|
}
|
|
236
|
-
if (this.schema
|
|
436
|
+
if (this.schema?.collections[op.collection]) {
|
|
237
437
|
this.rebuildMaterializedRecord(op.collection, op.recordId);
|
|
238
438
|
}
|
|
239
439
|
return "applied";
|
|
@@ -250,7 +450,7 @@ var MemoryServerStore = class {
|
|
|
250
450
|
}
|
|
251
451
|
async materializeCollection(collection) {
|
|
252
452
|
this.assertOpen();
|
|
253
|
-
if (this.schema
|
|
453
|
+
if (this.schema?.collections[collection]) {
|
|
254
454
|
return this.queryCollection(collection);
|
|
255
455
|
}
|
|
256
456
|
return this.materializeFromOps(collection);
|
|
@@ -259,13 +459,14 @@ var MemoryServerStore = class {
|
|
|
259
459
|
this.assertOpen();
|
|
260
460
|
this.assertSchema();
|
|
261
461
|
this.assertCollection(collection);
|
|
462
|
+
const schema = this.schema;
|
|
262
463
|
if (options?.where) {
|
|
263
464
|
for (const key of Object.keys(options.where)) {
|
|
264
|
-
validateFieldName(collection, key,
|
|
465
|
+
validateFieldName(collection, key, schema);
|
|
265
466
|
}
|
|
266
467
|
}
|
|
267
468
|
if (options?.orderBy) {
|
|
268
|
-
validateFieldName(collection, options.orderBy,
|
|
469
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
269
470
|
}
|
|
270
471
|
const collectionMap = this.materializedRecords.get(collection);
|
|
271
472
|
if (!collectionMap) return [];
|
|
@@ -332,9 +533,10 @@ var MemoryServerStore = class {
|
|
|
332
533
|
this.assertOpen();
|
|
333
534
|
this.assertSchema();
|
|
334
535
|
this.assertCollection(collection);
|
|
536
|
+
const schema = this.schema;
|
|
335
537
|
if (where) {
|
|
336
538
|
for (const key of Object.keys(where)) {
|
|
337
|
-
validateFieldName(collection, key,
|
|
539
|
+
validateFieldName(collection, key, schema);
|
|
338
540
|
}
|
|
339
541
|
}
|
|
340
542
|
const collectionMap = this.materializedRecords.get(collection);
|
|
@@ -359,6 +561,49 @@ var MemoryServerStore = class {
|
|
|
359
561
|
async close() {
|
|
360
562
|
this.closed = true;
|
|
361
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Wipes all in-memory state. For tests and E2E isolation only.
|
|
566
|
+
*/
|
|
567
|
+
resetForTests() {
|
|
568
|
+
this.assertOpen();
|
|
569
|
+
this.operations.length = 0;
|
|
570
|
+
this.operationIndex.clear();
|
|
571
|
+
this.versionVector.clear();
|
|
572
|
+
this.materializedRecords.clear();
|
|
573
|
+
this.schema = null;
|
|
574
|
+
}
|
|
575
|
+
async exportBackup() {
|
|
576
|
+
this.assertOpen();
|
|
577
|
+
const { buildServerBackup: buildServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
|
|
578
|
+
return buildServerBackup2(this.nodeId, this.operations, this.versionVector);
|
|
579
|
+
}
|
|
580
|
+
async importBackup(data, merge) {
|
|
581
|
+
this.assertOpen();
|
|
582
|
+
const { parseServerBackup: parseServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
|
|
583
|
+
const { operations: operations2, versionVector } = parseServerBackup2(data);
|
|
584
|
+
if (merge) {
|
|
585
|
+
let restored = 0;
|
|
586
|
+
for (const op of operations2) {
|
|
587
|
+
const result = await this.applyRemoteOperation(op);
|
|
588
|
+
if (result === "applied") restored++;
|
|
589
|
+
}
|
|
590
|
+
return { operationsRestored: restored, success: true };
|
|
591
|
+
}
|
|
592
|
+
this.operations.length = 0;
|
|
593
|
+
this.operationIndex.clear();
|
|
594
|
+
this.versionVector.clear();
|
|
595
|
+
for (const [nid, seq] of versionVector) {
|
|
596
|
+
this.versionVector.set(nid, seq);
|
|
597
|
+
}
|
|
598
|
+
for (const op of operations2) {
|
|
599
|
+
this.operations.push(op);
|
|
600
|
+
this.operationIndex.set(op.id, op);
|
|
601
|
+
if (this.schema?.collections[op.collection]) {
|
|
602
|
+
this.rebuildMaterializedRecord(op.collection, op.recordId);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return { operationsRestored: operations2.length, success: true };
|
|
606
|
+
}
|
|
362
607
|
// --- Testing helpers (not on interface) ---
|
|
363
608
|
/**
|
|
364
609
|
* Get all stored operations (for test assertions).
|
|
@@ -370,7 +615,7 @@ var MemoryServerStore = class {
|
|
|
370
615
|
// Materialization internals
|
|
371
616
|
// ---------------------------------------------------------------------------
|
|
372
617
|
rebuildMaterializedRecord(collection, recordId) {
|
|
373
|
-
const collectionDef = this.schema
|
|
618
|
+
const collectionDef = this.schema?.collections[collection];
|
|
374
619
|
if (!collectionDef) return;
|
|
375
620
|
let collectionMap = this.materializedRecords.get(collection);
|
|
376
621
|
if (!collectionMap) {
|
|
@@ -378,8 +623,10 @@ var MemoryServerStore = class {
|
|
|
378
623
|
this.materializedRecords.set(collection, collectionMap);
|
|
379
624
|
}
|
|
380
625
|
const recordOps = this.operations.filter((op) => op.collection === collection && op.recordId === recordId).sort((a, b) => {
|
|
381
|
-
if (a.timestamp.wallTime !== b.timestamp.wallTime)
|
|
382
|
-
|
|
626
|
+
if (a.timestamp.wallTime !== b.timestamp.wallTime)
|
|
627
|
+
return a.timestamp.wallTime - b.timestamp.wallTime;
|
|
628
|
+
if (a.timestamp.logical !== b.timestamp.logical)
|
|
629
|
+
return a.timestamp.logical - b.timestamp.logical;
|
|
383
630
|
return a.sequenceNumber - b.sequenceNumber;
|
|
384
631
|
});
|
|
385
632
|
const parsedOps = recordOps.map((op) => ({
|
|
@@ -431,8 +678,10 @@ var MemoryServerStore = class {
|
|
|
431
678
|
// ---------------------------------------------------------------------------
|
|
432
679
|
materializeFromOps(collection) {
|
|
433
680
|
const collectionOps = this.operations.filter((op) => op.collection === collection).sort((a, b) => {
|
|
434
|
-
if (a.timestamp.wallTime !== b.timestamp.wallTime)
|
|
435
|
-
|
|
681
|
+
if (a.timestamp.wallTime !== b.timestamp.wallTime)
|
|
682
|
+
return a.timestamp.wallTime - b.timestamp.wallTime;
|
|
683
|
+
if (a.timestamp.logical !== b.timestamp.logical)
|
|
684
|
+
return a.timestamp.logical - b.timestamp.logical;
|
|
436
685
|
return a.sequenceNumber - b.sequenceNumber;
|
|
437
686
|
});
|
|
438
687
|
const records = /* @__PURE__ */ new Map();
|
|
@@ -478,19 +727,22 @@ var MemoryServerStore = class {
|
|
|
478
727
|
}
|
|
479
728
|
}
|
|
480
729
|
assertCollection(collection) {
|
|
481
|
-
|
|
730
|
+
const schema = this.schema;
|
|
731
|
+
if (!schema.collections[collection]) {
|
|
482
732
|
throw new Error(
|
|
483
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
733
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
484
734
|
);
|
|
485
735
|
}
|
|
486
736
|
}
|
|
487
737
|
};
|
|
488
738
|
|
|
489
739
|
// src/store/postgres-server-store.ts
|
|
740
|
+
init_cjs_shims();
|
|
490
741
|
var import_core2 = require("@korajs/core");
|
|
491
742
|
var import_drizzle_orm = require("drizzle-orm");
|
|
492
743
|
|
|
493
744
|
// src/store/drizzle-pg-schema.ts
|
|
745
|
+
init_cjs_shims();
|
|
494
746
|
var import_pg_core = require("drizzle-orm/pg-core");
|
|
495
747
|
var pgOperations = (0, import_pg_core.pgTable)(
|
|
496
748
|
"operations",
|
|
@@ -545,6 +797,9 @@ var PostgresServerStore = class {
|
|
|
545
797
|
getNodeId() {
|
|
546
798
|
return this.nodeId;
|
|
547
799
|
}
|
|
800
|
+
getSchema() {
|
|
801
|
+
return this.schema;
|
|
802
|
+
}
|
|
548
803
|
async setSchema(schema) {
|
|
549
804
|
this.assertOpen();
|
|
550
805
|
await this.ready;
|
|
@@ -590,7 +845,7 @@ var PostgresServerStore = class {
|
|
|
590
845
|
lastSeenAt: import_drizzle_orm.sql`${now}`
|
|
591
846
|
}
|
|
592
847
|
});
|
|
593
|
-
if (this.schema
|
|
848
|
+
if (this.schema?.collections[op.collection]) {
|
|
594
849
|
await this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
|
|
595
850
|
}
|
|
596
851
|
});
|
|
@@ -604,10 +859,7 @@ var PostgresServerStore = class {
|
|
|
604
859
|
this.assertOpen();
|
|
605
860
|
await this.ready;
|
|
606
861
|
const rows = await this.db.select().from(pgOperations).where(
|
|
607
|
-
(0, import_drizzle_orm.and)(
|
|
608
|
-
(0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId),
|
|
609
|
-
(0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq)
|
|
610
|
-
)
|
|
862
|
+
(0, import_drizzle_orm.and)((0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId), (0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq))
|
|
611
863
|
).orderBy((0, import_drizzle_orm.asc)(pgOperations.sequenceNumber));
|
|
612
864
|
return rows.map((row) => this.deserializeOperation(row));
|
|
613
865
|
}
|
|
@@ -620,7 +872,7 @@ var PostgresServerStore = class {
|
|
|
620
872
|
async materializeCollection(collection) {
|
|
621
873
|
this.assertOpen();
|
|
622
874
|
await this.ready;
|
|
623
|
-
if (this.schema
|
|
875
|
+
if (this.schema?.collections[collection]) {
|
|
624
876
|
return this.queryCollection(collection);
|
|
625
877
|
}
|
|
626
878
|
return this.materializeFromOpsLog(collection);
|
|
@@ -630,14 +882,15 @@ var PostgresServerStore = class {
|
|
|
630
882
|
await this.ready;
|
|
631
883
|
this.assertSchema();
|
|
632
884
|
this.assertCollection(collection);
|
|
633
|
-
const
|
|
885
|
+
const schema = this.schema;
|
|
886
|
+
const collectionDef = schema.collections[collection];
|
|
634
887
|
if (options?.where) {
|
|
635
888
|
for (const key of Object.keys(options.where)) {
|
|
636
|
-
validateFieldName(collection, key,
|
|
889
|
+
validateFieldName(collection, key, schema);
|
|
637
890
|
}
|
|
638
891
|
}
|
|
639
892
|
if (options?.orderBy) {
|
|
640
|
-
validateFieldName(collection, options.orderBy,
|
|
893
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
641
894
|
}
|
|
642
895
|
const query = this.buildSelectQuery(collection, options);
|
|
643
896
|
const rows = await this.db.execute(query);
|
|
@@ -648,7 +901,8 @@ var PostgresServerStore = class {
|
|
|
648
901
|
await this.ready;
|
|
649
902
|
this.assertSchema();
|
|
650
903
|
this.assertCollection(collection);
|
|
651
|
-
const
|
|
904
|
+
const schema = this.schema;
|
|
905
|
+
const collectionDef = schema.collections[collection];
|
|
652
906
|
const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
653
907
|
const rows = await this.db.execute(query);
|
|
654
908
|
if (rows.length === 0) return null;
|
|
@@ -659,9 +913,10 @@ var PostgresServerStore = class {
|
|
|
659
913
|
await this.ready;
|
|
660
914
|
this.assertSchema();
|
|
661
915
|
this.assertCollection(collection);
|
|
916
|
+
const schema = this.schema;
|
|
662
917
|
if (where) {
|
|
663
918
|
for (const key of Object.keys(where)) {
|
|
664
|
-
validateFieldName(collection, key,
|
|
919
|
+
validateFieldName(collection, key, schema);
|
|
665
920
|
}
|
|
666
921
|
}
|
|
667
922
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
@@ -673,6 +928,45 @@ var PostgresServerStore = class {
|
|
|
673
928
|
async close() {
|
|
674
929
|
this.closed = true;
|
|
675
930
|
}
|
|
931
|
+
async exportBackup() {
|
|
932
|
+
this.assertOpen();
|
|
933
|
+
await this.ready;
|
|
934
|
+
const { buildServerBackup: buildServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
|
|
935
|
+
const rows = await this.db.select().from(pgOperations).orderBy((0, import_drizzle_orm.asc)(pgOperations.sequenceNumber));
|
|
936
|
+
const operations2 = rows.map((row) => this.deserializeOperation(row));
|
|
937
|
+
return buildServerBackup2(this.nodeId, operations2, this.versionVector);
|
|
938
|
+
}
|
|
939
|
+
async importBackup(data, merge) {
|
|
940
|
+
this.assertOpen();
|
|
941
|
+
await this.ready;
|
|
942
|
+
const { parseServerBackup: parseServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
|
|
943
|
+
const { operations: operations2, versionVector } = parseServerBackup2(data);
|
|
944
|
+
if (merge) {
|
|
945
|
+
let restored = 0;
|
|
946
|
+
for (const op of operations2) {
|
|
947
|
+
const result = await this.applyRemoteOperation(op);
|
|
948
|
+
if (result === "applied") restored++;
|
|
949
|
+
}
|
|
950
|
+
return { operationsRestored: restored, success: true };
|
|
951
|
+
}
|
|
952
|
+
const now = Date.now();
|
|
953
|
+
await this.db.transaction(async (tx) => {
|
|
954
|
+
await tx.delete(pgOperations);
|
|
955
|
+
await tx.delete(pgSyncState);
|
|
956
|
+
for (const [nid, seq] of versionVector) {
|
|
957
|
+
await tx.insert(pgSyncState).values({ nodeId: nid, maxSequenceNumber: seq, lastSeenAt: now }).onConflictDoNothing({ target: pgSyncState.nodeId });
|
|
958
|
+
}
|
|
959
|
+
for (const op of operations2) {
|
|
960
|
+
const row = this.serializeOperation(op, now);
|
|
961
|
+
await tx.insert(pgOperations).values(row).onConflictDoNothing({ target: pgOperations.id });
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
this.versionVector.clear();
|
|
965
|
+
for (const [nid, seq] of versionVector) {
|
|
966
|
+
this.versionVector.set(nid, seq);
|
|
967
|
+
}
|
|
968
|
+
return { operationsRestored: operations2.length, success: true };
|
|
969
|
+
}
|
|
676
970
|
// ---------------------------------------------------------------------------
|
|
677
971
|
// Materialization internals
|
|
678
972
|
// ---------------------------------------------------------------------------
|
|
@@ -681,18 +975,13 @@ var PostgresServerStore = class {
|
|
|
681
975
|
* all operations for that record.
|
|
682
976
|
*/
|
|
683
977
|
async rebuildMaterializedRecord(txOrDb, collection, recordId) {
|
|
684
|
-
const collectionDef = this.schema
|
|
978
|
+
const collectionDef = this.schema?.collections[collection];
|
|
685
979
|
if (!collectionDef) return;
|
|
686
980
|
const ops = await txOrDb.select({
|
|
687
981
|
type: pgOperations.type,
|
|
688
982
|
data: pgOperations.data,
|
|
689
983
|
wallTime: pgOperations.wallTime
|
|
690
|
-
}).from(pgOperations).where(
|
|
691
|
-
(0, import_drizzle_orm.and)(
|
|
692
|
-
(0, import_drizzle_orm.eq)(pgOperations.collection, collection),
|
|
693
|
-
(0, import_drizzle_orm.eq)(pgOperations.recordId, recordId)
|
|
694
|
-
)
|
|
695
|
-
).orderBy(
|
|
984
|
+
}).from(pgOperations).where((0, import_drizzle_orm.and)((0, import_drizzle_orm.eq)(pgOperations.collection, collection), (0, import_drizzle_orm.eq)(pgOperations.recordId, recordId))).orderBy(
|
|
696
985
|
(0, import_drizzle_orm.asc)(pgOperations.wallTime),
|
|
697
986
|
(0, import_drizzle_orm.asc)(pgOperations.logical),
|
|
698
987
|
(0, import_drizzle_orm.asc)(pgOperations.sequenceNumber)
|
|
@@ -762,7 +1051,7 @@ var PostgresServerStore = class {
|
|
|
762
1051
|
* Backfill a single collection's materialized table from operations.
|
|
763
1052
|
*/
|
|
764
1053
|
async backfillCollection(collectionName) {
|
|
765
|
-
const collectionDef = this.schema
|
|
1054
|
+
const collectionDef = this.schema?.collections[collectionName];
|
|
766
1055
|
if (!collectionDef) return;
|
|
767
1056
|
const allOps = await this.db.select({
|
|
768
1057
|
recordId: pgOperations.recordId,
|
|
@@ -819,9 +1108,7 @@ var PostgresServerStore = class {
|
|
|
819
1108
|
options?.where ?? {},
|
|
820
1109
|
options?.includeDeleted ?? false
|
|
821
1110
|
);
|
|
822
|
-
const parts = [
|
|
823
|
-
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`
|
|
824
|
-
];
|
|
1111
|
+
const parts = [import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`];
|
|
825
1112
|
if (options?.orderBy) {
|
|
826
1113
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
827
1114
|
parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
@@ -934,12 +1221,8 @@ var PostgresServerStore = class {
|
|
|
934
1221
|
await this.db.execute(
|
|
935
1222
|
import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
|
|
936
1223
|
);
|
|
937
|
-
await this.db.execute(
|
|
938
|
-
|
|
939
|
-
);
|
|
940
|
-
await this.db.execute(
|
|
941
|
-
import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`
|
|
942
|
-
);
|
|
1224
|
+
await this.db.execute(import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`);
|
|
1225
|
+
await this.db.execute(import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`);
|
|
943
1226
|
await this.db.execute(
|
|
944
1227
|
import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection_record ON operations (collection, record_id)`
|
|
945
1228
|
);
|
|
@@ -1007,9 +1290,10 @@ var PostgresServerStore = class {
|
|
|
1007
1290
|
}
|
|
1008
1291
|
}
|
|
1009
1292
|
assertCollection(collection) {
|
|
1010
|
-
|
|
1293
|
+
const schema = this.schema;
|
|
1294
|
+
if (!schema.collections[collection]) {
|
|
1011
1295
|
throw new Error(
|
|
1012
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
1296
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
1013
1297
|
);
|
|
1014
1298
|
}
|
|
1015
1299
|
}
|
|
@@ -1037,11 +1321,13 @@ async function loadPostgresDeps() {
|
|
|
1037
1321
|
}
|
|
1038
1322
|
|
|
1039
1323
|
// src/store/sqlite-server-store.ts
|
|
1324
|
+
init_cjs_shims();
|
|
1040
1325
|
var import_node_module = require("module");
|
|
1041
1326
|
var import_core3 = require("@korajs/core");
|
|
1042
1327
|
var import_drizzle_orm2 = require("drizzle-orm");
|
|
1043
1328
|
|
|
1044
1329
|
// src/store/drizzle-schema.ts
|
|
1330
|
+
init_cjs_shims();
|
|
1045
1331
|
var import_sqlite_core = require("drizzle-orm/sqlite-core");
|
|
1046
1332
|
var operations = (0, import_sqlite_core.sqliteTable)(
|
|
1047
1333
|
"operations",
|
|
@@ -1100,6 +1386,9 @@ var SqliteServerStore = class {
|
|
|
1100
1386
|
getNodeId() {
|
|
1101
1387
|
return this.nodeId;
|
|
1102
1388
|
}
|
|
1389
|
+
getSchema() {
|
|
1390
|
+
return this.schema;
|
|
1391
|
+
}
|
|
1103
1392
|
async setSchema(schema) {
|
|
1104
1393
|
this.assertOpen();
|
|
1105
1394
|
this.schema = schema;
|
|
@@ -1142,7 +1431,7 @@ var SqliteServerStore = class {
|
|
|
1142
1431
|
lastSeenAt: import_drizzle_orm2.sql`${now}`
|
|
1143
1432
|
}
|
|
1144
1433
|
}).run();
|
|
1145
|
-
if (this.schema
|
|
1434
|
+
if (this.schema?.collections[op.collection]) {
|
|
1146
1435
|
this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
|
|
1147
1436
|
}
|
|
1148
1437
|
return "applied";
|
|
@@ -1161,7 +1450,7 @@ var SqliteServerStore = class {
|
|
|
1161
1450
|
}
|
|
1162
1451
|
async materializeCollection(collection) {
|
|
1163
1452
|
this.assertOpen();
|
|
1164
|
-
if (this.schema
|
|
1453
|
+
if (this.schema?.collections[collection]) {
|
|
1165
1454
|
return this.queryCollection(collection);
|
|
1166
1455
|
}
|
|
1167
1456
|
return this.materializeFromOpsLog(collection);
|
|
@@ -1170,14 +1459,15 @@ var SqliteServerStore = class {
|
|
|
1170
1459
|
this.assertOpen();
|
|
1171
1460
|
this.assertSchema();
|
|
1172
1461
|
this.assertCollection(collection);
|
|
1173
|
-
const
|
|
1462
|
+
const schema = this.schema;
|
|
1463
|
+
const collectionDef = schema.collections[collection];
|
|
1174
1464
|
if (options?.where) {
|
|
1175
1465
|
for (const key of Object.keys(options.where)) {
|
|
1176
|
-
validateFieldName(collection, key,
|
|
1466
|
+
validateFieldName(collection, key, schema);
|
|
1177
1467
|
}
|
|
1178
1468
|
}
|
|
1179
1469
|
if (options?.orderBy) {
|
|
1180
|
-
validateFieldName(collection, options.orderBy,
|
|
1470
|
+
validateFieldName(collection, options.orderBy, schema);
|
|
1181
1471
|
}
|
|
1182
1472
|
const query = this.buildSelectQuery(collection, options);
|
|
1183
1473
|
const rows = this.db.all(query);
|
|
@@ -1187,7 +1477,8 @@ var SqliteServerStore = class {
|
|
|
1187
1477
|
this.assertOpen();
|
|
1188
1478
|
this.assertSchema();
|
|
1189
1479
|
this.assertCollection(collection);
|
|
1190
|
-
const
|
|
1480
|
+
const schema = this.schema;
|
|
1481
|
+
const collectionDef = schema.collections[collection];
|
|
1191
1482
|
const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
1192
1483
|
const rows = this.db.all(query);
|
|
1193
1484
|
if (rows.length === 0) return null;
|
|
@@ -1197,9 +1488,10 @@ var SqliteServerStore = class {
|
|
|
1197
1488
|
this.assertOpen();
|
|
1198
1489
|
this.assertSchema();
|
|
1199
1490
|
this.assertCollection(collection);
|
|
1491
|
+
const schema = this.schema;
|
|
1200
1492
|
if (where) {
|
|
1201
1493
|
for (const key of Object.keys(where)) {
|
|
1202
|
-
validateFieldName(collection, key,
|
|
1494
|
+
validateFieldName(collection, key, schema);
|
|
1203
1495
|
}
|
|
1204
1496
|
}
|
|
1205
1497
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
@@ -1210,6 +1502,39 @@ var SqliteServerStore = class {
|
|
|
1210
1502
|
async close() {
|
|
1211
1503
|
this.closed = true;
|
|
1212
1504
|
}
|
|
1505
|
+
async exportBackup() {
|
|
1506
|
+
this.assertOpen();
|
|
1507
|
+
const { buildServerBackup: buildServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
|
|
1508
|
+
const rows = this.db.select().from(operations).all();
|
|
1509
|
+
const deserialized = rows.map((row) => this.deserializeOperation(row));
|
|
1510
|
+
const vv = this.getVersionVector();
|
|
1511
|
+
return buildServerBackup2(this.nodeId, deserialized, vv);
|
|
1512
|
+
}
|
|
1513
|
+
async importBackup(data, merge) {
|
|
1514
|
+
this.assertOpen();
|
|
1515
|
+
const { parseServerBackup: parseServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
|
|
1516
|
+
const { operations: ops, versionVector } = parseServerBackup2(data);
|
|
1517
|
+
if (merge) {
|
|
1518
|
+
let restored = 0;
|
|
1519
|
+
for (const op of ops) {
|
|
1520
|
+
const result = await this.applyRemoteOperation(op);
|
|
1521
|
+
if (result === "applied") restored++;
|
|
1522
|
+
}
|
|
1523
|
+
return { operationsRestored: restored, success: true };
|
|
1524
|
+
}
|
|
1525
|
+
this.db.transaction((tx) => {
|
|
1526
|
+
tx.run(import_drizzle_orm2.sql.raw("DELETE FROM operations"));
|
|
1527
|
+
tx.run(import_drizzle_orm2.sql.raw("DELETE FROM sync_state"));
|
|
1528
|
+
for (const [nid, seq] of versionVector) {
|
|
1529
|
+
tx.insert(syncState).values({ nodeId: nid, maxSequenceNumber: seq, lastSeenAt: Date.now() }).run();
|
|
1530
|
+
}
|
|
1531
|
+
for (const op of ops) {
|
|
1532
|
+
const row = this.serializeOperation(op, Date.now());
|
|
1533
|
+
tx.insert(operations).values(row).run();
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
return { operationsRestored: ops.length, success: true };
|
|
1537
|
+
}
|
|
1213
1538
|
// ---------------------------------------------------------------------------
|
|
1214
1539
|
// Materialization internals
|
|
1215
1540
|
// ---------------------------------------------------------------------------
|
|
@@ -1219,18 +1544,13 @@ var SqliteServerStore = class {
|
|
|
1219
1544
|
* transaction for atomic dual-write.
|
|
1220
1545
|
*/
|
|
1221
1546
|
rebuildMaterializedRecord(txOrDb, collection, recordId) {
|
|
1222
|
-
const collectionDef = this.schema
|
|
1547
|
+
const collectionDef = this.schema?.collections[collection];
|
|
1223
1548
|
if (!collectionDef) return;
|
|
1224
1549
|
const ops = txOrDb.select({
|
|
1225
1550
|
type: operations.type,
|
|
1226
1551
|
data: operations.data,
|
|
1227
1552
|
wallTime: operations.wallTime
|
|
1228
|
-
}).from(operations).where(
|
|
1229
|
-
(0, import_drizzle_orm2.and)(
|
|
1230
|
-
(0, import_drizzle_orm2.eq)(operations.collection, collection),
|
|
1231
|
-
(0, import_drizzle_orm2.eq)(operations.recordId, recordId)
|
|
1232
|
-
)
|
|
1233
|
-
).orderBy((0, import_drizzle_orm2.asc)(operations.wallTime), (0, import_drizzle_orm2.asc)(operations.logical), (0, import_drizzle_orm2.asc)(operations.sequenceNumber)).all();
|
|
1553
|
+
}).from(operations).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(operations.collection, collection), (0, import_drizzle_orm2.eq)(operations.recordId, recordId))).orderBy((0, import_drizzle_orm2.asc)(operations.wallTime), (0, import_drizzle_orm2.asc)(operations.logical), (0, import_drizzle_orm2.asc)(operations.sequenceNumber)).all();
|
|
1234
1554
|
const parsedOps = ops.map((op) => ({
|
|
1235
1555
|
type: op.type,
|
|
1236
1556
|
data: op.data !== null ? JSON.parse(op.data) : null
|
|
@@ -1299,7 +1619,7 @@ var SqliteServerStore = class {
|
|
|
1299
1619
|
* Backfill a single collection's materialized table from operations.
|
|
1300
1620
|
*/
|
|
1301
1621
|
backfillCollection(collectionName) {
|
|
1302
|
-
const collectionDef = this.schema
|
|
1622
|
+
const collectionDef = this.schema?.collections[collectionName];
|
|
1303
1623
|
if (!collectionDef) return;
|
|
1304
1624
|
const allOps = this.db.select({
|
|
1305
1625
|
recordId: operations.recordId,
|
|
@@ -1354,9 +1674,7 @@ var SqliteServerStore = class {
|
|
|
1354
1674
|
options?.where ?? {},
|
|
1355
1675
|
options?.includeDeleted ?? false
|
|
1356
1676
|
);
|
|
1357
|
-
const parts = [
|
|
1358
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`
|
|
1359
|
-
];
|
|
1677
|
+
const parts = [import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`];
|
|
1360
1678
|
if (options?.orderBy) {
|
|
1361
1679
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
1362
1680
|
parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
@@ -1531,9 +1849,10 @@ var SqliteServerStore = class {
|
|
|
1531
1849
|
}
|
|
1532
1850
|
}
|
|
1533
1851
|
assertCollection(collection) {
|
|
1534
|
-
|
|
1852
|
+
const schema = this.schema;
|
|
1853
|
+
if (!schema.collections[collection]) {
|
|
1535
1854
|
throw new Error(
|
|
1536
|
-
`Unknown collection "${collection}". Available: ${Object.keys(
|
|
1855
|
+
`Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
|
|
1537
1856
|
);
|
|
1538
1857
|
}
|
|
1539
1858
|
}
|
|
@@ -1549,6 +1868,7 @@ function createSqliteServerStore(options) {
|
|
|
1549
1868
|
}
|
|
1550
1869
|
|
|
1551
1870
|
// src/transport/http-server-transport.ts
|
|
1871
|
+
init_cjs_shims();
|
|
1552
1872
|
var import_core4 = require("@korajs/core");
|
|
1553
1873
|
var HttpServerTransport = class {
|
|
1554
1874
|
serializer;
|
|
@@ -1630,6 +1950,7 @@ var HttpServerTransport = class {
|
|
|
1630
1950
|
};
|
|
1631
1951
|
|
|
1632
1952
|
// src/transport/ws-server-transport.ts
|
|
1953
|
+
init_cjs_shims();
|
|
1633
1954
|
var import_core5 = require("@korajs/core");
|
|
1634
1955
|
var import_sync = require("@korajs/sync");
|
|
1635
1956
|
var WS_OPEN = 1;
|
|
@@ -1693,11 +2014,220 @@ var WsServerTransport = class {
|
|
|
1693
2014
|
};
|
|
1694
2015
|
|
|
1695
2016
|
// src/session/client-session.ts
|
|
1696
|
-
|
|
2017
|
+
init_cjs_shims();
|
|
2018
|
+
var import_core8 = require("@korajs/core");
|
|
1697
2019
|
var import_internal = require("@korajs/core/internal");
|
|
1698
2020
|
var import_sync2 = require("@korajs/sync");
|
|
1699
2021
|
|
|
2022
|
+
// src/apply/apply-server-operation.ts
|
|
2023
|
+
init_cjs_shims();
|
|
2024
|
+
var import_merge2 = require("@korajs/merge");
|
|
2025
|
+
|
|
2026
|
+
// src/constraints/operation-constraint-validator.ts
|
|
2027
|
+
init_cjs_shims();
|
|
2028
|
+
var import_merge = require("@korajs/merge");
|
|
2029
|
+
|
|
2030
|
+
// src/constraints/server-constraint-context.ts
|
|
2031
|
+
init_cjs_shims();
|
|
2032
|
+
function createServerConstraintContext(store) {
|
|
2033
|
+
return {
|
|
2034
|
+
async queryRecords(collection, where) {
|
|
2035
|
+
const rows = await store.queryCollection(collection, { where });
|
|
2036
|
+
return rows.map((row) => ({ ...row }));
|
|
2037
|
+
},
|
|
2038
|
+
async countRecords(collection, where) {
|
|
2039
|
+
return store.countCollection(collection, where);
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// src/constraints/operation-constraint-validator.ts
|
|
2045
|
+
async function validateIncomingOperationConstraints(store, op, schema) {
|
|
2046
|
+
if (!schema) {
|
|
2047
|
+
return { valid: true };
|
|
2048
|
+
}
|
|
2049
|
+
if (op.type === "delete") {
|
|
2050
|
+
return { valid: true };
|
|
2051
|
+
}
|
|
2052
|
+
const collectionDef = schema.collections[op.collection];
|
|
2053
|
+
if (!collectionDef || collectionDef.constraints.length === 0) {
|
|
2054
|
+
return { valid: true };
|
|
2055
|
+
}
|
|
2056
|
+
const candidate = await projectCandidateRecord(store, op);
|
|
2057
|
+
if (candidate === null) {
|
|
2058
|
+
return { valid: true };
|
|
2059
|
+
}
|
|
2060
|
+
const ctx = createServerConstraintContext(store);
|
|
2061
|
+
const violations = await (0, import_merge.checkConstraints)(
|
|
2062
|
+
candidate,
|
|
2063
|
+
op.recordId,
|
|
2064
|
+
op.collection,
|
|
2065
|
+
collectionDef,
|
|
2066
|
+
ctx
|
|
2067
|
+
);
|
|
2068
|
+
if (violations.length === 0) {
|
|
2069
|
+
return { valid: true };
|
|
2070
|
+
}
|
|
2071
|
+
const first = violations[0];
|
|
2072
|
+
if (first === void 0) {
|
|
2073
|
+
return { valid: true };
|
|
2074
|
+
}
|
|
2075
|
+
return {
|
|
2076
|
+
valid: false,
|
|
2077
|
+
code: "CONSTRAINT_VIOLATION",
|
|
2078
|
+
message: first.message
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
async function projectCandidateRecord(store, op) {
|
|
2082
|
+
if (op.data === null) {
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
const existing = await store.findRecord(op.collection, op.recordId);
|
|
2086
|
+
const base = existing ? { ...existing } : {};
|
|
2087
|
+
if (op.type === "insert") {
|
|
2088
|
+
return { ...op.data, id: op.recordId };
|
|
2089
|
+
}
|
|
2090
|
+
return { ...base, ...op.data, id: op.recordId };
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// src/constraints/server-referential-context.ts
|
|
2094
|
+
init_cjs_shims();
|
|
2095
|
+
function createServerReferentialContext(store) {
|
|
2096
|
+
return {
|
|
2097
|
+
async queryRecords(collection, where) {
|
|
2098
|
+
const rows = await store.queryCollection(collection, { where });
|
|
2099
|
+
return rows.map((row) => ({ ...row }));
|
|
2100
|
+
},
|
|
2101
|
+
async recordExists(collection, recordId) {
|
|
2102
|
+
const row = await store.findRecord(collection, recordId);
|
|
2103
|
+
return row !== null && row._deleted !== 1;
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/apply/server-side-effect-operation.ts
|
|
2109
|
+
init_cjs_shims();
|
|
2110
|
+
var import_core6 = require("@korajs/core");
|
|
2111
|
+
async function createServerSideEffectOperation(store, parentOp, effect, schemaVersion, sequenceNumber) {
|
|
2112
|
+
const nodeId = store.getNodeId();
|
|
2113
|
+
const clock = new import_core6.HybridLogicalClock(nodeId);
|
|
2114
|
+
clock.receive(parentOp.timestamp);
|
|
2115
|
+
return (0, import_core6.createOperation)(
|
|
2116
|
+
{
|
|
2117
|
+
nodeId,
|
|
2118
|
+
type: effect.type === "delete" ? "delete" : "update",
|
|
2119
|
+
collection: effect.collection,
|
|
2120
|
+
recordId: effect.recordId,
|
|
2121
|
+
data: effect.data,
|
|
2122
|
+
previousData: effect.previousData,
|
|
2123
|
+
sequenceNumber,
|
|
2124
|
+
causalDeps: [parentOp.id],
|
|
2125
|
+
schemaVersion
|
|
2126
|
+
},
|
|
2127
|
+
clock
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
function nextServerSequenceNumber(store) {
|
|
2131
|
+
const nodeId = store.getNodeId();
|
|
2132
|
+
const current = store.getVersionVector().get(nodeId) ?? 0;
|
|
2133
|
+
return current + 1;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// src/apply/apply-server-operation.ts
|
|
2137
|
+
async function applyServerOperation(store, op, relationLookup) {
|
|
2138
|
+
const schema = store.getSchema();
|
|
2139
|
+
const lookup = relationLookup ?? (schema ? (0, import_merge2.buildMergeRelationLookup)(schema) : /* @__PURE__ */ new Map());
|
|
2140
|
+
const constraintCheck = await validateIncomingOperationConstraints(store, op, schema);
|
|
2141
|
+
if (!constraintCheck.valid) {
|
|
2142
|
+
return {
|
|
2143
|
+
result: "skipped",
|
|
2144
|
+
appliedOperations: [],
|
|
2145
|
+
rejection: {
|
|
2146
|
+
code: constraintCheck.code ?? "CONSTRAINT_VIOLATION",
|
|
2147
|
+
message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint`
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
if (op.type === "delete" && schema) {
|
|
2152
|
+
const refCtx = createServerReferentialContext(store);
|
|
2153
|
+
const referential = await (0, import_merge2.checkReferentialIntegrityOnDelete)(op, schema, refCtx, lookup);
|
|
2154
|
+
if (!referential.allowed) {
|
|
2155
|
+
return {
|
|
2156
|
+
result: "skipped",
|
|
2157
|
+
appliedOperations: [],
|
|
2158
|
+
rejection: {
|
|
2159
|
+
code: "REFERENTIAL_INTEGRITY",
|
|
2160
|
+
message: `Operation "${op.id}" violates referential integrity on "${op.collection}"`
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
const primaryResult = await store.applyRemoteOperation(op);
|
|
2165
|
+
if (primaryResult !== "applied") {
|
|
2166
|
+
return { result: primaryResult, appliedOperations: [] };
|
|
2167
|
+
}
|
|
2168
|
+
const appliedOperations = [op];
|
|
2169
|
+
let serverSeq = nextServerSequenceNumber(store);
|
|
2170
|
+
for (const effect of referential.sideEffectOps) {
|
|
2171
|
+
const sideOp = await createServerSideEffectOperation(
|
|
2172
|
+
store,
|
|
2173
|
+
op,
|
|
2174
|
+
effect,
|
|
2175
|
+
op.schemaVersion,
|
|
2176
|
+
serverSeq
|
|
2177
|
+
);
|
|
2178
|
+
serverSeq += 1;
|
|
2179
|
+
const sideResult = await store.applyRemoteOperation(sideOp);
|
|
2180
|
+
if (sideResult === "applied") {
|
|
2181
|
+
appliedOperations.push(sideOp);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
return { result: "applied", appliedOperations };
|
|
2185
|
+
}
|
|
2186
|
+
const result = await store.applyRemoteOperation(op);
|
|
2187
|
+
return {
|
|
2188
|
+
result,
|
|
2189
|
+
appliedOperations: result === "applied" ? [op] : []
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// src/scopes/resolve-session-scopes.ts
|
|
2194
|
+
init_cjs_shims();
|
|
2195
|
+
var import_core7 = require("@korajs/core");
|
|
2196
|
+
function resolveSessionScopes(schema, options) {
|
|
2197
|
+
const { handshakeScope, authScopes, scopeValues } = options;
|
|
2198
|
+
let resolved;
|
|
2199
|
+
if (schema && scopeValues && Object.keys(scopeValues).length > 0) {
|
|
2200
|
+
resolved = (0, import_core7.buildScopeMap)(schema, scopeValues);
|
|
2201
|
+
} else if (handshakeScope) {
|
|
2202
|
+
resolved = { ...handshakeScope };
|
|
2203
|
+
} else if (authScopes) {
|
|
2204
|
+
resolved = { ...authScopes };
|
|
2205
|
+
}
|
|
2206
|
+
if (schema && !resolved && scopeValues) {
|
|
2207
|
+
resolved = (0, import_core7.buildScopeMap)(schema, scopeValues);
|
|
2208
|
+
}
|
|
2209
|
+
if (authScopes) {
|
|
2210
|
+
resolved = mergeScopeMaps(resolved, authScopes, "auth");
|
|
2211
|
+
}
|
|
2212
|
+
if (handshakeScope) {
|
|
2213
|
+
resolved = mergeScopeMaps(resolved, handshakeScope, "handshake");
|
|
2214
|
+
}
|
|
2215
|
+
return resolved && Object.keys(resolved).length > 0 ? resolved : void 0;
|
|
2216
|
+
}
|
|
2217
|
+
function mergeScopeMaps(base, overlay, source) {
|
|
2218
|
+
const merged = { ...base ?? {} };
|
|
2219
|
+
for (const [collection, overlayScope] of Object.entries(overlay)) {
|
|
2220
|
+
if (source === "auth") {
|
|
2221
|
+
merged[collection] = { ...merged[collection] ?? {}, ...overlayScope };
|
|
2222
|
+
} else {
|
|
2223
|
+
merged[collection] = { ...overlayScope ?? {}, ...merged[collection] ?? {} };
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return merged;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
1700
2229
|
// src/scopes/server-scope-filter.ts
|
|
2230
|
+
init_cjs_shims();
|
|
1701
2231
|
function operationMatchesScopes(op, scopes) {
|
|
1702
2232
|
if (!scopes) return true;
|
|
1703
2233
|
const collectionScope = scopes[op.collection];
|
|
@@ -1727,6 +2257,57 @@ function asRecord(value) {
|
|
|
1727
2257
|
return value;
|
|
1728
2258
|
}
|
|
1729
2259
|
|
|
2260
|
+
// src/session/operation-validation.ts
|
|
2261
|
+
init_cjs_shims();
|
|
2262
|
+
var SERVER_MAX_TIMESTAMP_FUTURE_MS = 6e4;
|
|
2263
|
+
function isOperationTimestampValid(op, now = Date.now()) {
|
|
2264
|
+
return op.timestamp.wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// src/session/session-operation-limits.ts
|
|
2268
|
+
init_cjs_shims();
|
|
2269
|
+
var DEFAULT_MAX_OPERATION_BYTES = 256 * 1024;
|
|
2270
|
+
var DEFAULT_MAX_OPS_PER_MINUTE = 600;
|
|
2271
|
+
function measureOperationBytes(op) {
|
|
2272
|
+
return Buffer.byteLength(JSON.stringify(op), "utf8");
|
|
2273
|
+
}
|
|
2274
|
+
function validateOperationSize(op, maxBytes = DEFAULT_MAX_OPERATION_BYTES) {
|
|
2275
|
+
const bytes = measureOperationBytes(op);
|
|
2276
|
+
if (bytes <= maxBytes) {
|
|
2277
|
+
return { valid: true, bytes };
|
|
2278
|
+
}
|
|
2279
|
+
return {
|
|
2280
|
+
valid: false,
|
|
2281
|
+
bytes,
|
|
2282
|
+
message: `Operation "${op.id}" exceeds max size (${String(bytes)} > ${String(maxBytes)} bytes)`
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
var SessionRateLimiter = class {
|
|
2286
|
+
constructor(maxOpsPerMinute = DEFAULT_MAX_OPS_PER_MINUTE) {
|
|
2287
|
+
this.maxOpsPerMinute = maxOpsPerMinute;
|
|
2288
|
+
}
|
|
2289
|
+
maxOpsPerMinute;
|
|
2290
|
+
windowStartMs = Date.now();
|
|
2291
|
+
count = 0;
|
|
2292
|
+
get limit() {
|
|
2293
|
+
return this.maxOpsPerMinute;
|
|
2294
|
+
}
|
|
2295
|
+
/** Record N operations and return false when the limit is exceeded. */
|
|
2296
|
+
allow(count3 = 1) {
|
|
2297
|
+
const now = Date.now();
|
|
2298
|
+
if (now - this.windowStartMs >= 6e4) {
|
|
2299
|
+
this.windowStartMs = now;
|
|
2300
|
+
this.count = 0;
|
|
2301
|
+
}
|
|
2302
|
+
this.count += count3;
|
|
2303
|
+
return this.count <= this.maxOpsPerMinute;
|
|
2304
|
+
}
|
|
2305
|
+
reset() {
|
|
2306
|
+
this.windowStartMs = Date.now();
|
|
2307
|
+
this.count = 0;
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
|
|
1730
2311
|
// src/session/client-session.ts
|
|
1731
2312
|
var DEFAULT_BATCH_SIZE = 100;
|
|
1732
2313
|
var DEFAULT_SCHEMA_VERSION = 1;
|
|
@@ -1734,6 +2315,8 @@ var ClientSession = class {
|
|
|
1734
2315
|
state = "connected";
|
|
1735
2316
|
clientNodeId = null;
|
|
1736
2317
|
authContext = null;
|
|
2318
|
+
syncQuerySubsets = [];
|
|
2319
|
+
resumeDeltaCursor = null;
|
|
1737
2320
|
sessionId;
|
|
1738
2321
|
transport;
|
|
1739
2322
|
store;
|
|
@@ -1742,8 +2325,14 @@ var ClientSession = class {
|
|
|
1742
2325
|
emitter;
|
|
1743
2326
|
batchSize;
|
|
1744
2327
|
schemaVersion;
|
|
2328
|
+
supportedSchemaVersions;
|
|
1745
2329
|
onRelay;
|
|
2330
|
+
onAwarenessUpdate;
|
|
2331
|
+
onYjsDocUpdate;
|
|
1746
2332
|
onClose;
|
|
2333
|
+
maxOperationBytes;
|
|
2334
|
+
maxOpsPerMinute;
|
|
2335
|
+
rateLimiter;
|
|
1747
2336
|
constructor(options) {
|
|
1748
2337
|
this.sessionId = options.sessionId;
|
|
1749
2338
|
this.transport = options.transport;
|
|
@@ -1753,14 +2342,24 @@ var ClientSession = class {
|
|
|
1753
2342
|
this.emitter = options.emitter ?? null;
|
|
1754
2343
|
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
1755
2344
|
this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
2345
|
+
const supported = options.supportedSchemaVersions;
|
|
2346
|
+
this.supportedSchemaVersions = supported ?? {
|
|
2347
|
+
min: this.schemaVersion,
|
|
2348
|
+
max: this.schemaVersion
|
|
2349
|
+
};
|
|
1756
2350
|
this.onRelay = options.onRelay ?? null;
|
|
2351
|
+
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
2352
|
+
this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
|
|
1757
2353
|
this.onClose = options.onClose ?? null;
|
|
2354
|
+
this.maxOperationBytes = options.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
|
|
2355
|
+
this.maxOpsPerMinute = options.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE;
|
|
2356
|
+
this.rateLimiter = new SessionRateLimiter(this.maxOpsPerMinute);
|
|
1758
2357
|
}
|
|
1759
2358
|
/**
|
|
1760
2359
|
* Start handling messages from the client transport.
|
|
1761
2360
|
*/
|
|
1762
2361
|
start() {
|
|
1763
|
-
this.transport.onMessage((msg) => this.
|
|
2362
|
+
this.transport.onMessage((msg) => this.enqueueMessage(msg));
|
|
1764
2363
|
this.transport.onClose((_code, _reason) => this.handleTransportClose());
|
|
1765
2364
|
this.transport.onError((_err) => {
|
|
1766
2365
|
if (this.state !== "closed") {
|
|
@@ -1775,19 +2374,17 @@ var ClientSession = class {
|
|
|
1775
2374
|
relayOperations(operations2) {
|
|
1776
2375
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
1777
2376
|
if (operations2.length === 0) return;
|
|
1778
|
-
const visibleOperations = operations2.filter(
|
|
1779
|
-
(op) => operationMatchesScopes(op, this.authContext?.scopes)
|
|
1780
|
-
);
|
|
2377
|
+
const visibleOperations = operations2.filter((op) => this.operationVisibleToClient(op));
|
|
1781
2378
|
if (visibleOperations.length === 0) return;
|
|
1782
2379
|
const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
|
|
1783
2380
|
const msg = {
|
|
1784
2381
|
type: "operation-batch",
|
|
1785
|
-
messageId: (0,
|
|
2382
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
1786
2383
|
operations: serializedOps,
|
|
1787
2384
|
isFinal: true,
|
|
1788
2385
|
batchIndex: 0
|
|
1789
2386
|
};
|
|
1790
|
-
this.
|
|
2387
|
+
this.sendToClient(msg);
|
|
1791
2388
|
}
|
|
1792
2389
|
/**
|
|
1793
2390
|
* Close this session.
|
|
@@ -1816,22 +2413,55 @@ var ClientSession = class {
|
|
|
1816
2413
|
isStreaming() {
|
|
1817
2414
|
return this.state === "streaming";
|
|
1818
2415
|
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Get the transport for this session.
|
|
2418
|
+
* Used by the awareness relay to send messages to this client.
|
|
2419
|
+
*/
|
|
2420
|
+
getTransport() {
|
|
2421
|
+
return this.transport;
|
|
2422
|
+
}
|
|
1819
2423
|
// --- Private protocol handlers ---
|
|
1820
|
-
|
|
2424
|
+
messageChain = Promise.resolve();
|
|
2425
|
+
/** Send to the client when the transport is still connected; no-op otherwise. */
|
|
2426
|
+
sendToClient(message) {
|
|
2427
|
+
if (!this.transport.isConnected()) {
|
|
2428
|
+
return false;
|
|
2429
|
+
}
|
|
2430
|
+
try {
|
|
2431
|
+
this.transport.send(message);
|
|
2432
|
+
return true;
|
|
2433
|
+
} catch {
|
|
2434
|
+
return false;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
enqueueMessage(message) {
|
|
2438
|
+
this.messageChain = this.messageChain.then(() => this.handleMessageAsync(message)).catch((error) => this.handleMessageFailure(error));
|
|
2439
|
+
}
|
|
2440
|
+
async handleMessageAsync(message) {
|
|
1821
2441
|
switch (message.type) {
|
|
1822
2442
|
case "handshake":
|
|
1823
|
-
this.handleHandshake(message);
|
|
2443
|
+
await this.handleHandshake(message);
|
|
1824
2444
|
break;
|
|
1825
2445
|
case "operation-batch":
|
|
1826
|
-
this.handleOperationBatch(message);
|
|
2446
|
+
await this.handleOperationBatch(message);
|
|
1827
2447
|
break;
|
|
1828
|
-
// Acknowledgments from clients are noted but no action needed on server
|
|
1829
2448
|
case "acknowledgment":
|
|
1830
2449
|
break;
|
|
1831
2450
|
case "error":
|
|
1832
2451
|
break;
|
|
2452
|
+
case "awareness-update":
|
|
2453
|
+
this.handleAwarenessUpdate(message);
|
|
2454
|
+
break;
|
|
2455
|
+
case "yjs-doc-update":
|
|
2456
|
+
this.handleYjsDocUpdate(message);
|
|
2457
|
+
break;
|
|
1833
2458
|
}
|
|
1834
2459
|
}
|
|
2460
|
+
handleMessageFailure(error) {
|
|
2461
|
+
const reason = error instanceof Error ? error.message : "Message handling failed";
|
|
2462
|
+
this.sendError("SYNC_ERROR", reason, true);
|
|
2463
|
+
this.close(reason);
|
|
2464
|
+
}
|
|
1835
2465
|
async handleHandshake(msg) {
|
|
1836
2466
|
if (this.state !== "connected") {
|
|
1837
2467
|
this.sendError("DUPLICATE_HANDSHAKE", "Handshake already completed", false);
|
|
@@ -1849,19 +2479,56 @@ var ClientSession = class {
|
|
|
1849
2479
|
this.authContext = context;
|
|
1850
2480
|
this.state = "authenticated";
|
|
1851
2481
|
}
|
|
2482
|
+
const resolvedScopes = resolveSessionScopes(this.store.getSchema(), {
|
|
2483
|
+
handshakeScope: msg.syncScope,
|
|
2484
|
+
authScopes: this.authContext?.scopes
|
|
2485
|
+
});
|
|
2486
|
+
if (resolvedScopes) {
|
|
2487
|
+
if (this.authContext) {
|
|
2488
|
+
this.authContext = { ...this.authContext, scopes: resolvedScopes };
|
|
2489
|
+
} else {
|
|
2490
|
+
this.authContext = { userId: msg.nodeId, scopes: resolvedScopes };
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
if (msg.syncQueries && msg.syncQueries.length > 0) {
|
|
2494
|
+
this.syncQuerySubsets = (0, import_sync2.dedupeQuerySubsets)(msg.syncQueries);
|
|
2495
|
+
} else {
|
|
2496
|
+
this.syncQuerySubsets = [];
|
|
2497
|
+
}
|
|
2498
|
+
this.resumeDeltaCursor = msg.deltaCursor ? (0, import_sync2.decodeDeltaCursor)(msg.deltaCursor) : null;
|
|
1852
2499
|
const serverVector = this.store.getVersionVector();
|
|
1853
2500
|
const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
|
|
1854
2501
|
this.setSerializerWireFormat(selectedWireFormat);
|
|
2502
|
+
if (!(0, import_sync2.isClientSchemaVersionSupported)(msg.schemaVersion, this.supportedSchemaVersions)) {
|
|
2503
|
+
const { min, max } = this.supportedSchemaVersions;
|
|
2504
|
+
const response2 = {
|
|
2505
|
+
type: "handshake-response",
|
|
2506
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
2507
|
+
nodeId: this.store.getNodeId(),
|
|
2508
|
+
versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
|
|
2509
|
+
schemaVersion: this.schemaVersion,
|
|
2510
|
+
accepted: false,
|
|
2511
|
+
rejectReason: `${import_sync2.SCHEMA_MISMATCH_PREFIX}: client schema version ${msg.schemaVersion} not in supported range [${min}, ${max}]`,
|
|
2512
|
+
supportedSchemaMin: min,
|
|
2513
|
+
supportedSchemaMax: max
|
|
2514
|
+
};
|
|
2515
|
+
this.sendToClient(response2);
|
|
2516
|
+
this.close("schema version mismatch");
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
1855
2519
|
const response = {
|
|
1856
2520
|
type: "handshake-response",
|
|
1857
|
-
messageId: (0,
|
|
2521
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
1858
2522
|
nodeId: this.store.getNodeId(),
|
|
1859
2523
|
versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
|
|
1860
2524
|
schemaVersion: this.schemaVersion,
|
|
1861
2525
|
accepted: true,
|
|
1862
|
-
selectedWireFormat
|
|
2526
|
+
selectedWireFormat,
|
|
2527
|
+
// Confirm the accepted scope so the client knows what data will be synced.
|
|
2528
|
+
// This may differ from what the client requested if auth scopes are narrower.
|
|
2529
|
+
...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
|
|
1863
2530
|
};
|
|
1864
|
-
this.
|
|
2531
|
+
this.sendToClient(response);
|
|
1865
2532
|
this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
|
|
1866
2533
|
this.state = "syncing";
|
|
1867
2534
|
const clientVector = (0, import_sync2.wireToVersionVector)(msg.versionVector);
|
|
@@ -1871,13 +2538,56 @@ var ClientSession = class {
|
|
|
1871
2538
|
async handleOperationBatch(msg) {
|
|
1872
2539
|
const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
1873
2540
|
const applied = [];
|
|
2541
|
+
const rejected = [];
|
|
1874
2542
|
for (const op of operations2) {
|
|
1875
|
-
if (!
|
|
2543
|
+
if (!this.operationVisibleToClient(op)) {
|
|
2544
|
+
rejected.push(op);
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
if (!isOperationTimestampValid(op)) {
|
|
2548
|
+
this.sendError(
|
|
2549
|
+
"INVALID_TIMESTAMP",
|
|
2550
|
+
`Operation "${op.id}" timestamp is too far in the future`,
|
|
2551
|
+
false
|
|
2552
|
+
);
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
if (!this.rateLimiter.allow(1)) {
|
|
2556
|
+
this.sendError(
|
|
2557
|
+
"RATE_LIMIT",
|
|
2558
|
+
`Session exceeded operation rate limit (${String(this.maxOpsPerMinute)} ops/min)`,
|
|
2559
|
+
true
|
|
2560
|
+
);
|
|
1876
2561
|
continue;
|
|
1877
2562
|
}
|
|
1878
|
-
const
|
|
1879
|
-
if (
|
|
1880
|
-
|
|
2563
|
+
const sizeCheck = validateOperationSize(op, this.maxOperationBytes);
|
|
2564
|
+
if (!sizeCheck.valid) {
|
|
2565
|
+
this.sendError(
|
|
2566
|
+
"OPERATION_TOO_LARGE",
|
|
2567
|
+
sizeCheck.message ?? `Operation "${op.id}" is too large`,
|
|
2568
|
+
false
|
|
2569
|
+
);
|
|
2570
|
+
continue;
|
|
2571
|
+
}
|
|
2572
|
+
try {
|
|
2573
|
+
const applyResult = await applyServerOperation(this.store, op);
|
|
2574
|
+
if (applyResult.rejection) {
|
|
2575
|
+
this.sendError(applyResult.rejection.code, applyResult.rejection.message, false);
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
if (applyResult.result === "applied") {
|
|
2579
|
+
applied.push(...applyResult.appliedOperations);
|
|
2580
|
+
}
|
|
2581
|
+
} catch {
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
if (rejected.length > 0) {
|
|
2585
|
+
for (const op of rejected) {
|
|
2586
|
+
this.sendError(
|
|
2587
|
+
"SCOPE_VIOLATION",
|
|
2588
|
+
`Operation "${op.id}" in collection "${op.collection}" is outside the client's sync scope`,
|
|
2589
|
+
false
|
|
2590
|
+
);
|
|
1881
2591
|
}
|
|
1882
2592
|
}
|
|
1883
2593
|
if (operations2.length > 0) {
|
|
@@ -1890,11 +2600,11 @@ var ClientSession = class {
|
|
|
1890
2600
|
const lastOp = operations2[operations2.length - 1];
|
|
1891
2601
|
const ack = {
|
|
1892
2602
|
type: "acknowledgment",
|
|
1893
|
-
messageId: (0,
|
|
2603
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
1894
2604
|
acknowledgedMessageId: msg.messageId,
|
|
1895
2605
|
lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
|
|
1896
2606
|
};
|
|
1897
|
-
this.
|
|
2607
|
+
this.sendToClient(ack);
|
|
1898
2608
|
if (applied.length > 0) {
|
|
1899
2609
|
this.onRelay?.(this.sessionId, applied);
|
|
1900
2610
|
}
|
|
@@ -1906,35 +2616,52 @@ var ClientSession = class {
|
|
|
1906
2616
|
const clientSeq = clientVector.get(nodeId) ?? 0;
|
|
1907
2617
|
if (serverSeq > clientSeq) {
|
|
1908
2618
|
const ops = await this.store.getOperationRange(nodeId, clientSeq + 1, serverSeq);
|
|
1909
|
-
const visible = ops.filter((op) =>
|
|
2619
|
+
const visible = ops.filter((op) => this.operationVisibleToClient(op));
|
|
1910
2620
|
missing.push(...visible);
|
|
1911
2621
|
}
|
|
1912
2622
|
}
|
|
1913
2623
|
if (missing.length === 0) {
|
|
1914
2624
|
const emptyBatch = {
|
|
1915
2625
|
type: "operation-batch",
|
|
1916
|
-
messageId: (0,
|
|
2626
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
1917
2627
|
operations: [],
|
|
1918
2628
|
isFinal: true,
|
|
1919
|
-
batchIndex: 0
|
|
2629
|
+
batchIndex: 0,
|
|
2630
|
+
totalBatches: 1
|
|
1920
2631
|
};
|
|
1921
|
-
this.
|
|
2632
|
+
this.sendToClient(emptyBatch);
|
|
1922
2633
|
return;
|
|
1923
2634
|
}
|
|
1924
2635
|
const sorted = (0, import_internal.topologicalSort)(missing);
|
|
1925
|
-
const
|
|
2636
|
+
const afterCursor = (0, import_sync2.sliceOperationsAfterCursor)(sorted, this.resumeDeltaCursor);
|
|
2637
|
+
const totalBatches = Math.ceil(afterCursor.length / this.batchSize);
|
|
2638
|
+
if (afterCursor.length === 0) {
|
|
2639
|
+
const emptyBatch = {
|
|
2640
|
+
type: "operation-batch",
|
|
2641
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
2642
|
+
operations: [],
|
|
2643
|
+
isFinal: true,
|
|
2644
|
+
batchIndex: this.resumeDeltaCursor?.batchIndex ?? 0,
|
|
2645
|
+
totalBatches: 1
|
|
2646
|
+
};
|
|
2647
|
+
this.sendToClient(emptyBatch);
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
1926
2650
|
for (let i = 0; i < totalBatches; i++) {
|
|
1927
2651
|
const start = i * this.batchSize;
|
|
1928
|
-
const batchOps =
|
|
2652
|
+
const batchOps = afterCursor.slice(start, start + this.batchSize);
|
|
1929
2653
|
const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
|
|
2654
|
+
const batchCursor = (0, import_sync2.createDeltaCursorFromBatch)(batchOps, i);
|
|
1930
2655
|
const batchMsg = {
|
|
1931
2656
|
type: "operation-batch",
|
|
1932
|
-
messageId: (0,
|
|
2657
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
1933
2658
|
operations: serializedOps,
|
|
1934
2659
|
isFinal: i === totalBatches - 1,
|
|
1935
|
-
batchIndex: i
|
|
2660
|
+
batchIndex: i,
|
|
2661
|
+
totalBatches,
|
|
2662
|
+
...batchCursor ? { cursor: (0, import_sync2.encodeDeltaCursor)(batchCursor) } : {}
|
|
1936
2663
|
};
|
|
1937
|
-
this.
|
|
2664
|
+
this.sendToClient(batchMsg);
|
|
1938
2665
|
this.emitter?.emit({
|
|
1939
2666
|
type: "sync:sent",
|
|
1940
2667
|
operations: batchOps,
|
|
@@ -1942,15 +2669,27 @@ var ClientSession = class {
|
|
|
1942
2669
|
});
|
|
1943
2670
|
}
|
|
1944
2671
|
}
|
|
2672
|
+
operationVisibleToClient(op) {
|
|
2673
|
+
if (!operationMatchesScopes(op, this.authContext?.scopes)) {
|
|
2674
|
+
return false;
|
|
2675
|
+
}
|
|
2676
|
+
return (0, import_sync2.operationMatchesQuerySubsets)(op, this.syncQuerySubsets);
|
|
2677
|
+
}
|
|
2678
|
+
handleAwarenessUpdate(msg) {
|
|
2679
|
+
this.onAwarenessUpdate?.(this.sessionId, msg);
|
|
2680
|
+
}
|
|
2681
|
+
handleYjsDocUpdate(msg) {
|
|
2682
|
+
this.onYjsDocUpdate?.(this.sessionId, msg);
|
|
2683
|
+
}
|
|
1945
2684
|
sendError(code, message, retriable) {
|
|
1946
2685
|
const errorMsg = {
|
|
1947
2686
|
type: "error",
|
|
1948
|
-
messageId: (0,
|
|
2687
|
+
messageId: (0, import_core8.generateUUIDv7)(),
|
|
1949
2688
|
code,
|
|
1950
2689
|
message,
|
|
1951
2690
|
retriable
|
|
1952
2691
|
};
|
|
1953
|
-
this.
|
|
2692
|
+
this.sendToClient(errorMsg);
|
|
1954
2693
|
}
|
|
1955
2694
|
setSerializerWireFormat(format) {
|
|
1956
2695
|
if (typeof this.serializer.setWireFormat === "function") {
|
|
@@ -1972,8 +2711,262 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
1972
2711
|
}
|
|
1973
2712
|
|
|
1974
2713
|
// src/server/kora-sync-server.ts
|
|
1975
|
-
|
|
2714
|
+
init_cjs_shims();
|
|
2715
|
+
var import_core10 = require("@korajs/core");
|
|
2716
|
+
var import_internal2 = require("@korajs/core/internal");
|
|
1976
2717
|
var import_sync3 = require("@korajs/sync");
|
|
2718
|
+
|
|
2719
|
+
// src/awareness/awareness-relay.ts
|
|
2720
|
+
init_cjs_shims();
|
|
2721
|
+
var import_core9 = require("@korajs/core");
|
|
2722
|
+
var AwarenessRelay = class {
|
|
2723
|
+
clients = /* @__PURE__ */ new Map();
|
|
2724
|
+
/**
|
|
2725
|
+
* Register a client for awareness broadcasting.
|
|
2726
|
+
*
|
|
2727
|
+
* @param sessionId - Unique session identifier
|
|
2728
|
+
* @param clientId - Client-assigned awareness ID
|
|
2729
|
+
* @param transport - Transport for sending messages to this client
|
|
2730
|
+
*/
|
|
2731
|
+
addClient(sessionId, clientId, transport) {
|
|
2732
|
+
this.clients.set(sessionId, {
|
|
2733
|
+
sessionId,
|
|
2734
|
+
clientId,
|
|
2735
|
+
transport,
|
|
2736
|
+
state: null
|
|
2737
|
+
});
|
|
2738
|
+
const existingStates = {};
|
|
2739
|
+
let hasStates = false;
|
|
2740
|
+
for (const [, client] of this.clients) {
|
|
2741
|
+
if (client.sessionId === sessionId) continue;
|
|
2742
|
+
if (client.state) {
|
|
2743
|
+
existingStates[String(client.clientId)] = client.state;
|
|
2744
|
+
hasStates = true;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
if (hasStates) {
|
|
2748
|
+
const catchUpMsg = {
|
|
2749
|
+
type: "awareness-update",
|
|
2750
|
+
messageId: (0, import_core9.generateUUIDv7)(),
|
|
2751
|
+
clientId: 0,
|
|
2752
|
+
// Server-sourced
|
|
2753
|
+
states: existingStates
|
|
2754
|
+
};
|
|
2755
|
+
transport.send(catchUpMsg);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Remove a client and broadcast its removal to all remaining clients.
|
|
2760
|
+
*
|
|
2761
|
+
* @param sessionId - Session ID of the disconnecting client
|
|
2762
|
+
*/
|
|
2763
|
+
removeClient(sessionId) {
|
|
2764
|
+
const client = this.clients.get(sessionId);
|
|
2765
|
+
if (!client) return;
|
|
2766
|
+
this.clients.delete(sessionId);
|
|
2767
|
+
if (client.state === null) return;
|
|
2768
|
+
const removalStates = {
|
|
2769
|
+
[String(client.clientId)]: null
|
|
2770
|
+
};
|
|
2771
|
+
const msg = {
|
|
2772
|
+
type: "awareness-update",
|
|
2773
|
+
messageId: (0, import_core9.generateUUIDv7)(),
|
|
2774
|
+
clientId: client.clientId,
|
|
2775
|
+
states: removalStates
|
|
2776
|
+
};
|
|
2777
|
+
this.broadcastExcept(sessionId, msg);
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Handle an incoming awareness update from a client.
|
|
2781
|
+
* Stores the state and relays to all other connected clients.
|
|
2782
|
+
*
|
|
2783
|
+
* @param sessionId - Session ID of the sending client
|
|
2784
|
+
* @param message - The awareness update message
|
|
2785
|
+
*/
|
|
2786
|
+
handleUpdate(sessionId, message) {
|
|
2787
|
+
const sender = this.clients.get(sessionId);
|
|
2788
|
+
if (!sender) return;
|
|
2789
|
+
const senderState = message.states[String(message.clientId)];
|
|
2790
|
+
if (senderState !== void 0) {
|
|
2791
|
+
sender.state = senderState;
|
|
2792
|
+
}
|
|
2793
|
+
this.broadcastExcept(sessionId, message);
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Get the number of registered awareness clients.
|
|
2797
|
+
*/
|
|
2798
|
+
getClientCount() {
|
|
2799
|
+
return this.clients.size;
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Remove all clients and clear all state.
|
|
2803
|
+
*/
|
|
2804
|
+
clear() {
|
|
2805
|
+
this.clients.clear();
|
|
2806
|
+
}
|
|
2807
|
+
// --- Private ---
|
|
2808
|
+
broadcastExcept(excludeSessionId, message) {
|
|
2809
|
+
for (const [, client] of this.clients) {
|
|
2810
|
+
if (client.sessionId === excludeSessionId) continue;
|
|
2811
|
+
if (!client.transport.isConnected()) continue;
|
|
2812
|
+
client.transport.send(message);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
|
|
2817
|
+
// src/diagnostics/server-metrics-collector.ts
|
|
2818
|
+
init_cjs_shims();
|
|
2819
|
+
function estimateByteSize(operations2) {
|
|
2820
|
+
let total = 0;
|
|
2821
|
+
for (const op of operations2) {
|
|
2822
|
+
total += JSON.stringify(op).length;
|
|
2823
|
+
}
|
|
2824
|
+
return total;
|
|
2825
|
+
}
|
|
2826
|
+
var ServerMetricsCollector = class {
|
|
2827
|
+
startedAt = Date.now();
|
|
2828
|
+
peakConnections = 0;
|
|
2829
|
+
connectionsTotal = 0;
|
|
2830
|
+
operationsReceived = 0;
|
|
2831
|
+
operationsSent = 0;
|
|
2832
|
+
bytesReceived = 0;
|
|
2833
|
+
bytesSent = 0;
|
|
2834
|
+
errorCount = 0;
|
|
2835
|
+
schemaVersion = 1;
|
|
2836
|
+
clientMetrics = /* @__PURE__ */ new Map();
|
|
2837
|
+
/** Record a new client connection. */
|
|
2838
|
+
recordConnection(sessionId) {
|
|
2839
|
+
this.connectionsTotal++;
|
|
2840
|
+
this.clientMetrics.set(sessionId, {
|
|
2841
|
+
sessionId,
|
|
2842
|
+
nodeId: null,
|
|
2843
|
+
state: "connected",
|
|
2844
|
+
connectedAt: Date.now(),
|
|
2845
|
+
operationsReceived: 0,
|
|
2846
|
+
operationsSent: 0,
|
|
2847
|
+
authContext: null
|
|
2848
|
+
});
|
|
2849
|
+
this.peakConnections = Math.max(this.peakConnections, this.clientMetrics.size);
|
|
2850
|
+
}
|
|
2851
|
+
/** Record a client disconnection. */
|
|
2852
|
+
recordDisconnection(sessionId) {
|
|
2853
|
+
this.clientMetrics.delete(sessionId);
|
|
2854
|
+
}
|
|
2855
|
+
/** Update the node ID after a handshake completes. */
|
|
2856
|
+
recordHandshake(sessionId, nodeId) {
|
|
2857
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2858
|
+
if (client) {
|
|
2859
|
+
client.nodeId = nodeId;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
/** Update session state. */
|
|
2863
|
+
updateSessionState(sessionId, state) {
|
|
2864
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2865
|
+
if (client) {
|
|
2866
|
+
client.state = state;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
/** Record authentication context for a session. */
|
|
2870
|
+
recordAuth(sessionId, authContext) {
|
|
2871
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2872
|
+
if (client) {
|
|
2873
|
+
client.authContext = authContext;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
/** Record operations received from a client. */
|
|
2877
|
+
recordReceived(sessionId, count3, byteSize) {
|
|
2878
|
+
this.operationsReceived += count3;
|
|
2879
|
+
this.bytesReceived += byteSize;
|
|
2880
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2881
|
+
if (client) {
|
|
2882
|
+
client.operationsReceived += count3;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
/** Record operations sent to a client. */
|
|
2886
|
+
recordSent(sessionId, count3, byteSize) {
|
|
2887
|
+
this.operationsSent += count3;
|
|
2888
|
+
this.bytesSent += byteSize;
|
|
2889
|
+
const client = this.clientMetrics.get(sessionId);
|
|
2890
|
+
if (client) {
|
|
2891
|
+
client.operationsSent += count3;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
/** Record an error. */
|
|
2895
|
+
recordError() {
|
|
2896
|
+
this.errorCount++;
|
|
2897
|
+
}
|
|
2898
|
+
/** Set the schema version. */
|
|
2899
|
+
setSchemaVersion(version) {
|
|
2900
|
+
this.schemaVersion = version;
|
|
2901
|
+
}
|
|
2902
|
+
/** Return a full snapshot of current metrics. */
|
|
2903
|
+
getSnapshot(totalOperations) {
|
|
2904
|
+
return {
|
|
2905
|
+
connectedClients: this.clientMetrics.size,
|
|
2906
|
+
connectedNodeIds: Array.from(this.clientMetrics.values()).filter((c) => c.nodeId !== null).map((c) => c.nodeId),
|
|
2907
|
+
peakConnections: this.peakConnections,
|
|
2908
|
+
connectionsTotal: this.connectionsTotal,
|
|
2909
|
+
operationsReceived: this.operationsReceived,
|
|
2910
|
+
operationsSent: this.operationsSent,
|
|
2911
|
+
bytesReceived: this.bytesReceived,
|
|
2912
|
+
bytesSent: this.bytesSent,
|
|
2913
|
+
clients: Array.from(this.clientMetrics.values()),
|
|
2914
|
+
uptime: Date.now() - this.startedAt,
|
|
2915
|
+
totalOperations,
|
|
2916
|
+
errorCount: this.errorCount,
|
|
2917
|
+
schemaVersion: this.schemaVersion
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
/** Reset all metrics. */
|
|
2921
|
+
reset() {
|
|
2922
|
+
this.startedAt = Date.now();
|
|
2923
|
+
this.peakConnections = 0;
|
|
2924
|
+
this.connectionsTotal = 0;
|
|
2925
|
+
this.operationsReceived = 0;
|
|
2926
|
+
this.operationsSent = 0;
|
|
2927
|
+
this.bytesReceived = 0;
|
|
2928
|
+
this.bytesSent = 0;
|
|
2929
|
+
this.errorCount = 0;
|
|
2930
|
+
this.clientMetrics.clear();
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
|
|
2934
|
+
// src/richtext/yjs-doc-relay.ts
|
|
2935
|
+
init_cjs_shims();
|
|
2936
|
+
var YjsDocRelay = class {
|
|
2937
|
+
clients = /* @__PURE__ */ new Map();
|
|
2938
|
+
addClient(sessionId, transport) {
|
|
2939
|
+
this.clients.set(sessionId, { sessionId, transport });
|
|
2940
|
+
}
|
|
2941
|
+
removeClient(sessionId) {
|
|
2942
|
+
this.clients.delete(sessionId);
|
|
2943
|
+
}
|
|
2944
|
+
handleUpdate(sourceSessionId, message) {
|
|
2945
|
+
if (!this.clients.has(sourceSessionId)) {
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
this.broadcastExcept(sourceSessionId, message);
|
|
2949
|
+
}
|
|
2950
|
+
getClientCount() {
|
|
2951
|
+
return this.clients.size;
|
|
2952
|
+
}
|
|
2953
|
+
clear() {
|
|
2954
|
+
this.clients.clear();
|
|
2955
|
+
}
|
|
2956
|
+
broadcastExcept(excludeSessionId, message) {
|
|
2957
|
+
for (const [, client] of this.clients) {
|
|
2958
|
+
if (client.sessionId === excludeSessionId) {
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
if (!client.transport.isConnected()) {
|
|
2962
|
+
continue;
|
|
2963
|
+
}
|
|
2964
|
+
client.transport.send(message);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
|
|
2969
|
+
// src/server/kora-sync-server.ts
|
|
1977
2970
|
var DEFAULT_MAX_CONNECTIONS = 0;
|
|
1978
2971
|
var DEFAULT_BATCH_SIZE2 = 100;
|
|
1979
2972
|
var DEFAULT_SCHEMA_VERSION2 = 1;
|
|
@@ -1987,12 +2980,18 @@ var KoraSyncServer = class {
|
|
|
1987
2980
|
maxConnections;
|
|
1988
2981
|
batchSize;
|
|
1989
2982
|
schemaVersion;
|
|
2983
|
+
supportedSchemaVersions;
|
|
1990
2984
|
port;
|
|
1991
2985
|
host;
|
|
1992
2986
|
path;
|
|
2987
|
+
logger;
|
|
2988
|
+
metrics;
|
|
2989
|
+
awarenessRelay = new AwarenessRelay();
|
|
2990
|
+
yjsDocRelay = new YjsDocRelay();
|
|
1993
2991
|
sessions = /* @__PURE__ */ new Map();
|
|
1994
2992
|
httpClients = /* @__PURE__ */ new Map();
|
|
1995
2993
|
httpSessionToClient = /* @__PURE__ */ new Map();
|
|
2994
|
+
serverVersion = "0.4.0";
|
|
1996
2995
|
wsServer = null;
|
|
1997
2996
|
running = false;
|
|
1998
2997
|
constructor(config) {
|
|
@@ -2003,9 +3002,80 @@ var KoraSyncServer = class {
|
|
|
2003
3002
|
this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
|
|
2004
3003
|
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
|
|
2005
3004
|
this.schemaVersion = config.schemaVersion ?? DEFAULT_SCHEMA_VERSION2;
|
|
3005
|
+
this.supportedSchemaVersions = config.supportedSchemaVersions ?? {
|
|
3006
|
+
min: this.schemaVersion,
|
|
3007
|
+
max: this.schemaVersion
|
|
3008
|
+
};
|
|
2006
3009
|
this.port = config.port;
|
|
2007
3010
|
this.host = config.host ?? DEFAULT_HOST;
|
|
2008
3011
|
this.path = config.path ?? DEFAULT_PATH;
|
|
3012
|
+
this.logger = config.logger ?? createDefaultLogger();
|
|
3013
|
+
this.metrics = config.metricsCollector ?? new ServerMetricsCollector();
|
|
3014
|
+
this.metrics.setSchemaVersion(this.schemaVersion);
|
|
3015
|
+
if (!this.emitter) {
|
|
3016
|
+
this.emitter = new import_internal2.SimpleEventEmitter();
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Subscribe to session-level events for metrics collection and logging.
|
|
3021
|
+
* Called when a new session is created.
|
|
3022
|
+
*/
|
|
3023
|
+
attachSessionEvents(sessionId, sessionEmitter) {
|
|
3024
|
+
sessionEmitter.on("sync:connected", (event) => {
|
|
3025
|
+
this.metrics.recordHandshake(sessionId, event.nodeId);
|
|
3026
|
+
this.logger.log({
|
|
3027
|
+
timestamp: Date.now(),
|
|
3028
|
+
level: "info",
|
|
3029
|
+
event: "session.handshake",
|
|
3030
|
+
sessionId,
|
|
3031
|
+
nodeId: event.nodeId
|
|
3032
|
+
});
|
|
3033
|
+
});
|
|
3034
|
+
sessionEmitter.on("sync:received", (event) => {
|
|
3035
|
+
const byteSize = estimateByteSize(event.operations);
|
|
3036
|
+
this.metrics.recordReceived(sessionId, event.batchSize, byteSize);
|
|
3037
|
+
this.logger.log({
|
|
3038
|
+
timestamp: Date.now(),
|
|
3039
|
+
level: "info",
|
|
3040
|
+
event: "operations.received",
|
|
3041
|
+
sessionId,
|
|
3042
|
+
count: event.batchSize,
|
|
3043
|
+
bytes: byteSize
|
|
3044
|
+
});
|
|
3045
|
+
});
|
|
3046
|
+
sessionEmitter.on("sync:sent", (event) => {
|
|
3047
|
+
const byteSize = estimateByteSize(event.operations);
|
|
3048
|
+
this.metrics.recordSent(sessionId, event.batchSize, byteSize);
|
|
3049
|
+
this.logger.log({
|
|
3050
|
+
timestamp: Date.now(),
|
|
3051
|
+
level: "info",
|
|
3052
|
+
event: "operations.sent",
|
|
3053
|
+
sessionId,
|
|
3054
|
+
count: event.batchSize,
|
|
3055
|
+
bytes: byteSize
|
|
3056
|
+
});
|
|
3057
|
+
});
|
|
3058
|
+
sessionEmitter.on("sync:disconnected", (event) => {
|
|
3059
|
+
this.logger.log({
|
|
3060
|
+
timestamp: Date.now(),
|
|
3061
|
+
level: "info",
|
|
3062
|
+
event: "session.disconnected",
|
|
3063
|
+
sessionId,
|
|
3064
|
+
details: { reason: event.reason }
|
|
3065
|
+
});
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
/**
|
|
3069
|
+
* Get the metrics collector for external access (e.g., HTTP endpoints).
|
|
3070
|
+
*/
|
|
3071
|
+
getMetricsCollector() {
|
|
3072
|
+
return this.metrics;
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Get the logger for external access (e.g., event streaming).
|
|
3076
|
+
*/
|
|
3077
|
+
getLogger() {
|
|
3078
|
+
return this.logger;
|
|
2009
3079
|
}
|
|
2010
3080
|
/**
|
|
2011
3081
|
* Start the WebSocket server in standalone mode.
|
|
@@ -2014,10 +3084,10 @@ var KoraSyncServer = class {
|
|
|
2014
3084
|
*/
|
|
2015
3085
|
async start(wsServerImpl) {
|
|
2016
3086
|
if (this.running) {
|
|
2017
|
-
throw new
|
|
3087
|
+
throw new import_core10.SyncError("Server is already running", { port: this.port });
|
|
2018
3088
|
}
|
|
2019
3089
|
if (!wsServerImpl && this.port === void 0) {
|
|
2020
|
-
throw new
|
|
3090
|
+
throw new import_core10.SyncError(
|
|
2021
3091
|
"Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
|
|
2022
3092
|
{}
|
|
2023
3093
|
);
|
|
@@ -2046,11 +3116,25 @@ var KoraSyncServer = class {
|
|
|
2046
3116
|
this.handleConnection(transport);
|
|
2047
3117
|
});
|
|
2048
3118
|
this.running = true;
|
|
3119
|
+
this.logger.log({
|
|
3120
|
+
timestamp: Date.now(),
|
|
3121
|
+
level: "info",
|
|
3122
|
+
event: "server.started",
|
|
3123
|
+
details: { port: this.port, host: this.host, path: this.path }
|
|
3124
|
+
});
|
|
2049
3125
|
}
|
|
2050
3126
|
/**
|
|
2051
3127
|
* Stop the server. Closes all sessions and the WebSocket server.
|
|
2052
3128
|
*/
|
|
2053
3129
|
async stop() {
|
|
3130
|
+
this.logger.log({
|
|
3131
|
+
timestamp: Date.now(),
|
|
3132
|
+
level: "info",
|
|
3133
|
+
event: "server.stopping",
|
|
3134
|
+
details: { connectedClients: this.sessions.size }
|
|
3135
|
+
});
|
|
3136
|
+
this.awarenessRelay.clear();
|
|
3137
|
+
this.yjsDocRelay.clear();
|
|
2054
3138
|
for (const session of this.sessions.values()) {
|
|
2055
3139
|
session.close("server shutting down");
|
|
2056
3140
|
}
|
|
@@ -2064,6 +3148,11 @@ var KoraSyncServer = class {
|
|
|
2064
3148
|
this.wsServer = null;
|
|
2065
3149
|
}
|
|
2066
3150
|
this.running = false;
|
|
3151
|
+
this.logger.log({
|
|
3152
|
+
timestamp: Date.now(),
|
|
3153
|
+
level: "info",
|
|
3154
|
+
event: "server.stopped"
|
|
3155
|
+
});
|
|
2067
3156
|
}
|
|
2068
3157
|
/**
|
|
2069
3158
|
* Handle one HTTP sync request for a long-polling client.
|
|
@@ -2107,47 +3196,125 @@ var KoraSyncServer = class {
|
|
|
2107
3196
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
2108
3197
|
transport.send({
|
|
2109
3198
|
type: "error",
|
|
2110
|
-
messageId: (0,
|
|
3199
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2111
3200
|
code: "MAX_CONNECTIONS",
|
|
2112
3201
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
2113
3202
|
retriable: true
|
|
2114
3203
|
});
|
|
2115
3204
|
transport.close(4029, "max connections reached");
|
|
2116
|
-
|
|
3205
|
+
this.metrics.recordError();
|
|
3206
|
+
this.logger.log({
|
|
3207
|
+
timestamp: Date.now(),
|
|
3208
|
+
level: "warn",
|
|
3209
|
+
event: "connection.rejected",
|
|
3210
|
+
details: { reason: "max_connections", max: this.maxConnections }
|
|
3211
|
+
});
|
|
3212
|
+
throw new import_core10.SyncError("Maximum connections reached", {
|
|
2117
3213
|
current: this.sessions.size,
|
|
2118
3214
|
max: this.maxConnections
|
|
2119
3215
|
});
|
|
2120
3216
|
}
|
|
2121
|
-
const sessionId = (0,
|
|
3217
|
+
const sessionId = (0, import_core10.generateUUIDv7)();
|
|
3218
|
+
this.metrics.recordConnection(sessionId);
|
|
3219
|
+
const sessionEmitter = new import_internal2.SimpleEventEmitter();
|
|
3220
|
+
sessionEmitter.on("sync:connected", (event) => {
|
|
3221
|
+
this.metrics.recordHandshake(sessionId, event.nodeId);
|
|
3222
|
+
this.metrics.updateSessionState(sessionId, "authenticated");
|
|
3223
|
+
this.logger.log({
|
|
3224
|
+
timestamp: Date.now(),
|
|
3225
|
+
level: "info",
|
|
3226
|
+
event: "session.handshake",
|
|
3227
|
+
sessionId,
|
|
3228
|
+
nodeId: event.nodeId
|
|
3229
|
+
});
|
|
3230
|
+
});
|
|
3231
|
+
sessionEmitter.on("sync:received", (event) => {
|
|
3232
|
+
const byteSize = estimateOperationByteSize(event.operations);
|
|
3233
|
+
this.metrics.recordReceived(sessionId, event.batchSize, byteSize);
|
|
3234
|
+
this.logger.log({
|
|
3235
|
+
timestamp: Date.now(),
|
|
3236
|
+
level: "info",
|
|
3237
|
+
event: "operations.received",
|
|
3238
|
+
sessionId,
|
|
3239
|
+
count: event.batchSize,
|
|
3240
|
+
bytes: byteSize
|
|
3241
|
+
});
|
|
3242
|
+
});
|
|
3243
|
+
sessionEmitter.on("sync:sent", (event) => {
|
|
3244
|
+
const byteSize = estimateOperationByteSize(event.operations);
|
|
3245
|
+
this.metrics.recordSent(sessionId, event.batchSize, byteSize);
|
|
3246
|
+
this.logger.log({
|
|
3247
|
+
timestamp: Date.now(),
|
|
3248
|
+
level: "info",
|
|
3249
|
+
event: "operations.sent",
|
|
3250
|
+
sessionId,
|
|
3251
|
+
count: event.batchSize,
|
|
3252
|
+
bytes: byteSize
|
|
3253
|
+
});
|
|
3254
|
+
});
|
|
3255
|
+
sessionEmitter.on("sync:disconnected", () => {
|
|
3256
|
+
this.logger.log({
|
|
3257
|
+
timestamp: Date.now(),
|
|
3258
|
+
level: "info",
|
|
3259
|
+
event: "session.disconnected",
|
|
3260
|
+
sessionId
|
|
3261
|
+
});
|
|
3262
|
+
});
|
|
2122
3263
|
const session = new ClientSession({
|
|
2123
3264
|
sessionId,
|
|
2124
3265
|
transport,
|
|
2125
3266
|
store: this.store,
|
|
2126
3267
|
auth: this.auth ?? void 0,
|
|
2127
3268
|
serializer: this.serializer,
|
|
2128
|
-
emitter:
|
|
3269
|
+
emitter: sessionEmitter,
|
|
2129
3270
|
batchSize: this.batchSize,
|
|
2130
3271
|
schemaVersion: this.schemaVersion,
|
|
3272
|
+
supportedSchemaVersions: this.supportedSchemaVersions,
|
|
2131
3273
|
onRelay: (sourceSessionId, operations2) => {
|
|
2132
3274
|
this.handleRelay(sourceSessionId, operations2);
|
|
2133
3275
|
},
|
|
3276
|
+
onAwarenessUpdate: (sourceSessionId, message) => {
|
|
3277
|
+
this.handleAwarenessRelay(sourceSessionId, message);
|
|
3278
|
+
},
|
|
3279
|
+
onYjsDocUpdate: (sourceSessionId, message) => {
|
|
3280
|
+
this.handleYjsDocRelay(sourceSessionId, message);
|
|
3281
|
+
},
|
|
2134
3282
|
onClose: (sid) => {
|
|
2135
3283
|
this.handleSessionClose(sid);
|
|
2136
3284
|
}
|
|
2137
3285
|
});
|
|
2138
3286
|
this.sessions.set(sessionId, session);
|
|
3287
|
+
this.yjsDocRelay.addClient(sessionId, transport);
|
|
2139
3288
|
session.start();
|
|
3289
|
+
this.logger.log({
|
|
3290
|
+
timestamp: Date.now(),
|
|
3291
|
+
level: "info",
|
|
3292
|
+
event: "session.connected",
|
|
3293
|
+
sessionId,
|
|
3294
|
+
details: { totalSessions: this.sessions.size }
|
|
3295
|
+
});
|
|
2140
3296
|
return sessionId;
|
|
2141
3297
|
}
|
|
2142
3298
|
/**
|
|
2143
3299
|
* Get the current server status.
|
|
2144
3300
|
*/
|
|
2145
3301
|
async getStatus() {
|
|
3302
|
+
const totalOps = await this.store.getOperationCount();
|
|
3303
|
+
const snapshot = this.metrics.getSnapshot(totalOps);
|
|
2146
3304
|
return {
|
|
2147
3305
|
running: this.running,
|
|
2148
|
-
connectedClients:
|
|
3306
|
+
connectedClients: snapshot.connectedClients,
|
|
2149
3307
|
port: this.port ?? null,
|
|
2150
|
-
totalOperations:
|
|
3308
|
+
totalOperations: snapshot.totalOperations,
|
|
3309
|
+
uptime: snapshot.uptime,
|
|
3310
|
+
version: this.serverVersion,
|
|
3311
|
+
schemaVersion: this.schemaVersion,
|
|
3312
|
+
connectedNodeIds: snapshot.connectedNodeIds,
|
|
3313
|
+
peakConnections: snapshot.peakConnections,
|
|
3314
|
+
connectionsTotal: snapshot.connectionsTotal,
|
|
3315
|
+
operationsReceived: snapshot.operationsReceived,
|
|
3316
|
+
operationsSent: snapshot.operationsSent,
|
|
3317
|
+
errorCount: snapshot.errorCount
|
|
2151
3318
|
};
|
|
2152
3319
|
}
|
|
2153
3320
|
/**
|
|
@@ -2158,12 +3325,31 @@ var KoraSyncServer = class {
|
|
|
2158
3325
|
}
|
|
2159
3326
|
// --- Private ---
|
|
2160
3327
|
handleRelay(sourceSessionId, operations2) {
|
|
3328
|
+
const targetCount = this.sessions.size - 1;
|
|
3329
|
+
const byteSize = estimateOperationByteSize(operations2);
|
|
3330
|
+
this.metrics.recordSent(
|
|
3331
|
+
sourceSessionId,
|
|
3332
|
+
operations2.length * targetCount,
|
|
3333
|
+
byteSize * targetCount
|
|
3334
|
+
);
|
|
3335
|
+
this.logger.log({
|
|
3336
|
+
timestamp: Date.now(),
|
|
3337
|
+
level: "info",
|
|
3338
|
+
event: "operations.relayed",
|
|
3339
|
+
sessionId: sourceSessionId,
|
|
3340
|
+
count: operations2.length,
|
|
3341
|
+
bytes: byteSize * targetCount,
|
|
3342
|
+
details: { targetSessions: targetCount }
|
|
3343
|
+
});
|
|
2161
3344
|
for (const [sessionId, session] of this.sessions) {
|
|
2162
3345
|
if (sessionId === sourceSessionId) continue;
|
|
2163
3346
|
session.relayOperations(operations2);
|
|
2164
3347
|
}
|
|
2165
3348
|
}
|
|
2166
3349
|
handleSessionClose(sessionId) {
|
|
3350
|
+
this.metrics.recordDisconnection(sessionId);
|
|
3351
|
+
this.awarenessRelay.removeClient(sessionId);
|
|
3352
|
+
this.yjsDocRelay.removeClient(sessionId);
|
|
2167
3353
|
this.sessions.delete(sessionId);
|
|
2168
3354
|
const clientId = this.httpSessionToClient.get(sessionId);
|
|
2169
3355
|
if (clientId) {
|
|
@@ -2171,6 +3357,21 @@ var KoraSyncServer = class {
|
|
|
2171
3357
|
this.httpClients.delete(clientId);
|
|
2172
3358
|
}
|
|
2173
3359
|
}
|
|
3360
|
+
handleAwarenessRelay(sourceSessionId, message) {
|
|
3361
|
+
const session = this.sessions.get(sourceSessionId);
|
|
3362
|
+
if (!session) return;
|
|
3363
|
+
const transport = session.getTransport();
|
|
3364
|
+
if (!this.awarenessRelay.getClientCount() || !transport) {
|
|
3365
|
+
}
|
|
3366
|
+
this.awarenessRelay.addClient(sourceSessionId, message.clientId, transport);
|
|
3367
|
+
this.awarenessRelay.handleUpdate(sourceSessionId, message);
|
|
3368
|
+
}
|
|
3369
|
+
handleYjsDocRelay(sourceSessionId, message) {
|
|
3370
|
+
if (!this.sessions.has(sourceSessionId)) {
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
this.yjsDocRelay.handleUpdate(sourceSessionId, message);
|
|
3374
|
+
}
|
|
2174
3375
|
getOrCreateHttpClient(clientId) {
|
|
2175
3376
|
const existing = this.httpClients.get(clientId);
|
|
2176
3377
|
if (existing) {
|
|
@@ -2184,6 +3385,13 @@ var KoraSyncServer = class {
|
|
|
2184
3385
|
return client;
|
|
2185
3386
|
}
|
|
2186
3387
|
};
|
|
3388
|
+
function estimateOperationByteSize(operations2) {
|
|
3389
|
+
let total = 0;
|
|
3390
|
+
for (const op of operations2) {
|
|
3391
|
+
total += JSON.stringify(op).length;
|
|
3392
|
+
}
|
|
3393
|
+
return total;
|
|
3394
|
+
}
|
|
2187
3395
|
function normalizeHttpBody(body, contentType) {
|
|
2188
3396
|
if (body instanceof Uint8Array) {
|
|
2189
3397
|
return body;
|
|
@@ -2195,6 +3403,7 @@ function normalizeHttpBody(body, contentType) {
|
|
|
2195
3403
|
}
|
|
2196
3404
|
|
|
2197
3405
|
// src/auth/no-auth.ts
|
|
3406
|
+
init_cjs_shims();
|
|
2198
3407
|
var NoAuthProvider = class {
|
|
2199
3408
|
async authenticate(_token) {
|
|
2200
3409
|
return { userId: "anonymous" };
|
|
@@ -2202,6 +3411,7 @@ var NoAuthProvider = class {
|
|
|
2202
3411
|
};
|
|
2203
3412
|
|
|
2204
3413
|
// src/auth/token-auth.ts
|
|
3414
|
+
init_cjs_shims();
|
|
2205
3415
|
var TokenAuthProvider = class {
|
|
2206
3416
|
validate;
|
|
2207
3417
|
constructor(options) {
|
|
@@ -2213,6 +3423,7 @@ var TokenAuthProvider = class {
|
|
|
2213
3423
|
};
|
|
2214
3424
|
|
|
2215
3425
|
// src/auth/kora-auth-provider.ts
|
|
3426
|
+
init_cjs_shims();
|
|
2216
3427
|
var KoraAuthProvider = class {
|
|
2217
3428
|
tokenValidator;
|
|
2218
3429
|
userLookup;
|
|
@@ -2225,7 +3436,7 @@ var KoraAuthProvider = class {
|
|
|
2225
3436
|
this.resolveScopes = options.resolveScopes;
|
|
2226
3437
|
}
|
|
2227
3438
|
async authenticate(token) {
|
|
2228
|
-
const payload = this.tokenValidator.validateToken(token);
|
|
3439
|
+
const payload = this.tokenValidator.validateTokenWithRevocation ? await this.tokenValidator.validateTokenWithRevocation(token) : this.tokenValidator.validateToken(token);
|
|
2229
3440
|
if (payload === null) {
|
|
2230
3441
|
return null;
|
|
2231
3442
|
}
|
|
@@ -2252,12 +3463,39 @@ var KoraAuthProvider = class {
|
|
|
2252
3463
|
}
|
|
2253
3464
|
};
|
|
2254
3465
|
|
|
3466
|
+
// src/auth/mixed-auth-provider.ts
|
|
3467
|
+
init_cjs_shims();
|
|
3468
|
+
var MixedAuthProvider = class {
|
|
3469
|
+
primary;
|
|
3470
|
+
anonymousScopes;
|
|
3471
|
+
anonymousPrefix;
|
|
3472
|
+
anonymousCounter = 0;
|
|
3473
|
+
constructor(options) {
|
|
3474
|
+
this.primary = options.primary;
|
|
3475
|
+
this.anonymousScopes = options.anonymousScopes;
|
|
3476
|
+
this.anonymousPrefix = options.anonymousPrefix ?? "anon";
|
|
3477
|
+
}
|
|
3478
|
+
async authenticate(token) {
|
|
3479
|
+
if (token) {
|
|
3480
|
+
const ctx = await this.primary.authenticate(token);
|
|
3481
|
+
if (ctx) return ctx;
|
|
3482
|
+
}
|
|
3483
|
+
this.anonymousCounter++;
|
|
3484
|
+
return {
|
|
3485
|
+
userId: `${this.anonymousPrefix}-${Date.now()}-${this.anonymousCounter}`,
|
|
3486
|
+
scopes: this.anonymousScopes
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
};
|
|
3490
|
+
|
|
2255
3491
|
// src/server/create-server.ts
|
|
3492
|
+
init_cjs_shims();
|
|
2256
3493
|
function createKoraServer(config) {
|
|
2257
3494
|
return new KoraSyncServer(config);
|
|
2258
3495
|
}
|
|
2259
3496
|
|
|
2260
3497
|
// src/server/production-server.ts
|
|
3498
|
+
init_cjs_shims();
|
|
2261
3499
|
var MIME_TYPES = {
|
|
2262
3500
|
".html": "text/html",
|
|
2263
3501
|
".js": "text/javascript",
|
|
@@ -2278,9 +3516,143 @@ function createProductionServer(config) {
|
|
|
2278
3516
|
const syncPath = config.syncPath ?? "/kora-sync";
|
|
2279
3517
|
const syncServer = new KoraSyncServer({
|
|
2280
3518
|
store: config.store,
|
|
3519
|
+
enableDashboard: true,
|
|
2281
3520
|
...config.syncOptions
|
|
2282
3521
|
});
|
|
2283
3522
|
let httpServer = null;
|
|
3523
|
+
function getOperationalToken(kind) {
|
|
3524
|
+
const auth = config.operationalAuth;
|
|
3525
|
+
if (!auth) return void 0;
|
|
3526
|
+
if (kind === "metrics") return auth.metricsToken || auth.adminToken;
|
|
3527
|
+
if (kind === "backup") return auth.backupToken || auth.adminToken;
|
|
3528
|
+
return auth.adminToken;
|
|
3529
|
+
}
|
|
3530
|
+
function extractRequestToken(req) {
|
|
3531
|
+
const authorization = req.headers.authorization;
|
|
3532
|
+
if (authorization?.startsWith("Bearer ")) {
|
|
3533
|
+
return authorization.slice("Bearer ".length).trim();
|
|
3534
|
+
}
|
|
3535
|
+
const headerNames = ["x-kora-admin-token", "x-kora-metrics-token", "x-kora-backup-token"];
|
|
3536
|
+
for (const name of headerNames) {
|
|
3537
|
+
const value = req.headers[name];
|
|
3538
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
3539
|
+
if (Array.isArray(value) && typeof value[0] === "string" && value[0].length > 0) {
|
|
3540
|
+
return value[0];
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
function isOperationalRequestAllowed(req, kind) {
|
|
3546
|
+
const expected = getOperationalToken(kind);
|
|
3547
|
+
if (!expected) return true;
|
|
3548
|
+
return extractRequestToken(req) === expected;
|
|
3549
|
+
}
|
|
3550
|
+
function rejectUnauthorized(res) {
|
|
3551
|
+
res.writeHead(401, {
|
|
3552
|
+
"Content-Type": "application/json",
|
|
3553
|
+
"WWW-Authenticate": 'Bearer realm="kora"'
|
|
3554
|
+
});
|
|
3555
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
3556
|
+
}
|
|
3557
|
+
function formatPrometheusMetrics() {
|
|
3558
|
+
const status = syncServer.getMetricsCollector().getSnapshot(0);
|
|
3559
|
+
const lines = [
|
|
3560
|
+
"# HELP kora_connected_clients Current number of connected clients",
|
|
3561
|
+
"# TYPE kora_connected_clients gauge",
|
|
3562
|
+
`kora_connected_clients ${status.connectedClients}`,
|
|
3563
|
+
"",
|
|
3564
|
+
"# HELP kora_peak_connections Peak number of simultaneous connections since server start",
|
|
3565
|
+
"# TYPE kora_peak_connections gauge",
|
|
3566
|
+
`kora_peak_connections ${status.peakConnections}`,
|
|
3567
|
+
"",
|
|
3568
|
+
"# HELP kora_connections_total Total number of connections handled since server start",
|
|
3569
|
+
"# TYPE kora_connections_total counter",
|
|
3570
|
+
`kora_connections_total ${status.connectionsTotal}`,
|
|
3571
|
+
"",
|
|
3572
|
+
"# HELP kora_operations_received_total Total operations received from clients",
|
|
3573
|
+
"# TYPE kora_operations_received_total counter",
|
|
3574
|
+
`kora_operations_received_total ${status.operationsReceived}`,
|
|
3575
|
+
"",
|
|
3576
|
+
"# HELP kora_operations_sent_total Total operations sent to clients",
|
|
3577
|
+
"# TYPE kora_operations_sent_total counter",
|
|
3578
|
+
`kora_operations_sent_total ${status.operationsSent}`,
|
|
3579
|
+
"",
|
|
3580
|
+
"# HELP kora_bytes_received_total Total bytes received from clients",
|
|
3581
|
+
"# TYPE kora_bytes_received_total counter",
|
|
3582
|
+
`kora_bytes_received_total ${status.bytesReceived}`,
|
|
3583
|
+
"",
|
|
3584
|
+
"# HELP kora_bytes_sent_total Total bytes sent to clients",
|
|
3585
|
+
"# TYPE kora_bytes_sent_total counter",
|
|
3586
|
+
`kora_bytes_sent_total ${status.bytesSent}`,
|
|
3587
|
+
"",
|
|
3588
|
+
"# HELP kora_errors_total Total errors since server start",
|
|
3589
|
+
"# TYPE kora_errors_total counter",
|
|
3590
|
+
`kora_errors_total ${status.errorCount}`,
|
|
3591
|
+
"",
|
|
3592
|
+
"# HELP kora_uptime_seconds Server uptime in seconds",
|
|
3593
|
+
"# TYPE kora_uptime_seconds gauge",
|
|
3594
|
+
`kora_uptime_seconds ${Math.floor(status.uptime / 1e3)}`,
|
|
3595
|
+
"",
|
|
3596
|
+
"# HELP kora_schema_version Schema version the server expects",
|
|
3597
|
+
"# TYPE kora_schema_version gauge",
|
|
3598
|
+
`kora_schema_version ${status.schemaVersion}`,
|
|
3599
|
+
""
|
|
3600
|
+
];
|
|
3601
|
+
return lines.join("\n");
|
|
3602
|
+
}
|
|
3603
|
+
function readBodyBuffer(req) {
|
|
3604
|
+
return new Promise((resolve) => {
|
|
3605
|
+
const chunks = [];
|
|
3606
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
3607
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
async function readJsonBody(req) {
|
|
3611
|
+
const buffer = await readBodyBuffer(req);
|
|
3612
|
+
if (buffer.byteLength === 0) return void 0;
|
|
3613
|
+
try {
|
|
3614
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
3615
|
+
} catch {
|
|
3616
|
+
return void 0;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
function matchesRoutePrefix(pathname, prefix) {
|
|
3620
|
+
const normalizedPrefix = normalizeRoutePath(prefix);
|
|
3621
|
+
return pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`);
|
|
3622
|
+
}
|
|
3623
|
+
function normalizeRoutePath(path) {
|
|
3624
|
+
const prefixed = path.startsWith("/") ? path : `/${path}`;
|
|
3625
|
+
return prefixed.length > 1 ? prefixed.replace(/\/+$/, "") : prefixed;
|
|
3626
|
+
}
|
|
3627
|
+
function getClientIp(req) {
|
|
3628
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
3629
|
+
if (typeof forwarded === "string" && forwarded.length > 0) {
|
|
3630
|
+
return forwarded.split(",")[0]?.trim();
|
|
3631
|
+
}
|
|
3632
|
+
return req.socket.remoteAddress;
|
|
3633
|
+
}
|
|
3634
|
+
function getQuery(url) {
|
|
3635
|
+
const query = {};
|
|
3636
|
+
for (const [key, value] of url.searchParams) {
|
|
3637
|
+
const existing = query[key];
|
|
3638
|
+
if (existing === void 0) {
|
|
3639
|
+
query[key] = value;
|
|
3640
|
+
} else if (Array.isArray(existing)) {
|
|
3641
|
+
existing.push(value);
|
|
3642
|
+
} else {
|
|
3643
|
+
query[key] = [existing, value];
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
return query;
|
|
3647
|
+
}
|
|
3648
|
+
function writeJsonResponse(res, result) {
|
|
3649
|
+
const headers = {
|
|
3650
|
+
"Content-Type": "application/json",
|
|
3651
|
+
...result.headers ?? {}
|
|
3652
|
+
};
|
|
3653
|
+
res.writeHead(result.status, headers);
|
|
3654
|
+
res.end(JSON.stringify(result.body ?? null));
|
|
3655
|
+
}
|
|
2284
3656
|
return {
|
|
2285
3657
|
async start() {
|
|
2286
3658
|
const { createServer } = await import("http");
|
|
@@ -2288,13 +3660,137 @@ function createProductionServer(config) {
|
|
|
2288
3660
|
const { extname, join, resolve } = await import("path");
|
|
2289
3661
|
const { WebSocketServer } = await import("ws");
|
|
2290
3662
|
const distDir = resolve(staticDir);
|
|
2291
|
-
httpServer = createServer((req, res) => {
|
|
3663
|
+
httpServer = createServer(async (req, res) => {
|
|
2292
3664
|
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
2293
3665
|
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
2294
3666
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
2295
3667
|
if (url.pathname === "/health") {
|
|
3668
|
+
const status = await syncServer.getStatus();
|
|
3669
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3670
|
+
res.end(
|
|
3671
|
+
JSON.stringify({
|
|
3672
|
+
status: "ok",
|
|
3673
|
+
version: status.version,
|
|
3674
|
+
uptime: status.uptime,
|
|
3675
|
+
connectedClients: status.connectedClients,
|
|
3676
|
+
totalOperations: status.totalOperations,
|
|
3677
|
+
timestamp: Date.now()
|
|
3678
|
+
})
|
|
3679
|
+
);
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
if (url.pathname === "/__kora/status") {
|
|
3683
|
+
if (!isOperationalRequestAllowed(req, "admin")) {
|
|
3684
|
+
rejectUnauthorized(res);
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
const status = await syncServer.getStatus();
|
|
2296
3688
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2297
|
-
res.end(JSON.stringify(
|
|
3689
|
+
res.end(JSON.stringify(status, null, 2));
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
if (url.pathname === "/__kora/metrics") {
|
|
3693
|
+
if (!isOperationalRequestAllowed(req, "metrics")) {
|
|
3694
|
+
rejectUnauthorized(res);
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" });
|
|
3698
|
+
res.end(formatPrometheusMetrics());
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
if (url.pathname === "/__kora/events") {
|
|
3702
|
+
if (!isOperationalRequestAllowed(req, "admin")) {
|
|
3703
|
+
rejectUnauthorized(res);
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3706
|
+
res.writeHead(200, {
|
|
3707
|
+
"Content-Type": "text/event-stream",
|
|
3708
|
+
"Cache-Control": "no-cache",
|
|
3709
|
+
Connection: "keep-alive",
|
|
3710
|
+
"X-Accel-Buffering": "no"
|
|
3711
|
+
});
|
|
3712
|
+
const status = await syncServer.getStatus();
|
|
3713
|
+
res.write(`event: status
|
|
3714
|
+
data: ${JSON.stringify(status)}
|
|
3715
|
+
|
|
3716
|
+
`);
|
|
3717
|
+
const interval = setInterval(async () => {
|
|
3718
|
+
try {
|
|
3719
|
+
const s = await syncServer.getStatus();
|
|
3720
|
+
res.write(`event: status
|
|
3721
|
+
data: ${JSON.stringify(s)}
|
|
3722
|
+
|
|
3723
|
+
`);
|
|
3724
|
+
} catch {
|
|
3725
|
+
}
|
|
3726
|
+
}, 2e3);
|
|
3727
|
+
req.on("close", () => {
|
|
3728
|
+
clearInterval(interval);
|
|
3729
|
+
});
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
if (url.pathname === "/__kora" || url.pathname === "/__kora/") {
|
|
3733
|
+
if (!isOperationalRequestAllowed(req, "admin")) {
|
|
3734
|
+
rejectUnauthorized(res);
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3737
|
+
const status = await syncServer.getStatus();
|
|
3738
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3739
|
+
res.end(renderDashboardHtml(status));
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
if (url.pathname === "/__kora/backup/export" && req.method === "POST") {
|
|
3743
|
+
if (!isOperationalRequestAllowed(req, "backup")) {
|
|
3744
|
+
rejectUnauthorized(res);
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
try {
|
|
3748
|
+
const backup = await config.store.exportBackup();
|
|
3749
|
+
res.writeHead(200, {
|
|
3750
|
+
"Content-Type": "application/octet-stream",
|
|
3751
|
+
"Content-Disposition": `attachment; filename="kora-backup-${Date.now()}.kora"`,
|
|
3752
|
+
"Content-Length": String(backup.byteLength)
|
|
3753
|
+
});
|
|
3754
|
+
res.end(Buffer.from(backup));
|
|
3755
|
+
} catch (error) {
|
|
3756
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
3757
|
+
res.end(JSON.stringify({ error: "Backup failed", message: error.message }));
|
|
3758
|
+
}
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
if (url.pathname === "/__kora/backup/import" && req.method === "POST") {
|
|
3762
|
+
if (!isOperationalRequestAllowed(req, "backup")) {
|
|
3763
|
+
rejectUnauthorized(res);
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
try {
|
|
3767
|
+
const body = await readBodyBuffer(req);
|
|
3768
|
+
const merge = url.searchParams.get("merge") === "true";
|
|
3769
|
+
const result = await config.store.importBackup(
|
|
3770
|
+
new Uint8Array(body.buffer, body.byteOffset, body.byteLength),
|
|
3771
|
+
merge
|
|
3772
|
+
);
|
|
3773
|
+
res.writeHead(result.success ? 200 : 400, { "Content-Type": "application/json" });
|
|
3774
|
+
res.end(JSON.stringify(result));
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
3777
|
+
res.end(JSON.stringify({ error: "Restore failed", message: error.message }));
|
|
3778
|
+
}
|
|
3779
|
+
return;
|
|
3780
|
+
}
|
|
3781
|
+
const customRoute = config.httpRoutes?.find(
|
|
3782
|
+
(route) => matchesRoutePrefix(url.pathname, route.path)
|
|
3783
|
+
);
|
|
3784
|
+
if (customRoute) {
|
|
3785
|
+
const result = await customRoute.handle({
|
|
3786
|
+
method: req.method ?? "GET",
|
|
3787
|
+
path: url.pathname,
|
|
3788
|
+
body: await readJsonBody(req),
|
|
3789
|
+
headers: req.headers,
|
|
3790
|
+
query: getQuery(url),
|
|
3791
|
+
ip: getClientIp(req)
|
|
3792
|
+
});
|
|
3793
|
+
writeJsonResponse(res, result);
|
|
2298
3794
|
return;
|
|
2299
3795
|
}
|
|
2300
3796
|
let filePath = join(distDir, url.pathname);
|
|
@@ -2340,7 +3836,7 @@ function createProductionServer(config) {
|
|
|
2340
3836
|
}
|
|
2341
3837
|
});
|
|
2342
3838
|
return new Promise((resolve2) => {
|
|
2343
|
-
httpServer
|
|
3839
|
+
httpServer?.listen(port, "0.0.0.0", () => {
|
|
2344
3840
|
resolve2(`http://localhost:${port}`);
|
|
2345
3841
|
});
|
|
2346
3842
|
});
|
|
@@ -2349,28 +3845,110 @@ function createProductionServer(config) {
|
|
|
2349
3845
|
await syncServer.stop();
|
|
2350
3846
|
if (httpServer) {
|
|
2351
3847
|
await new Promise((resolve) => {
|
|
2352
|
-
httpServer
|
|
3848
|
+
httpServer?.close(() => resolve());
|
|
2353
3849
|
});
|
|
2354
3850
|
httpServer = null;
|
|
2355
3851
|
}
|
|
2356
3852
|
}
|
|
2357
3853
|
};
|
|
2358
3854
|
}
|
|
3855
|
+
function renderDashboardHtml(status) {
|
|
3856
|
+
const version = status.version;
|
|
3857
|
+
const uptime = formatUptime(status.uptime);
|
|
3858
|
+
return `<!DOCTYPE html>
|
|
3859
|
+
<html lang="en">
|
|
3860
|
+
<head>
|
|
3861
|
+
<meta charset="UTF-8">
|
|
3862
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3863
|
+
<title>Kora Dashboard</title>
|
|
3864
|
+
<style>
|
|
3865
|
+
* { box-sizing: border-box; margin: 0; padding: 0 }
|
|
3866
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem }
|
|
3867
|
+
h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.25rem }
|
|
3868
|
+
.subtitle { color: #64748b; margin-bottom: 2rem; font-size: 0.875rem }
|
|
3869
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem }
|
|
3870
|
+
.card { background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; padding: 1.25rem }
|
|
3871
|
+
.card .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; margin-bottom: 0.5rem }
|
|
3872
|
+
.card .value { font-size: 1.75rem; font-weight: 700; color: #38bdf8 }
|
|
3873
|
+
.card .value.green { color: #4ade80 }
|
|
3874
|
+
.card .value.red { color: #f87171 }
|
|
3875
|
+
.card .value.yellow { color: #fbbf24 }
|
|
3876
|
+
.section-title { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; margin-top: 1.5rem }
|
|
3877
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.875rem }
|
|
3878
|
+
th { text-align: left; padding: 0.5rem 0.75rem; color: #64748b; font-weight: 500; border-bottom: 1px solid #334155 }
|
|
3879
|
+
td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #1e293b }
|
|
3880
|
+
.status-dot { display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; margin-right: 0.375rem }
|
|
3881
|
+
.status-dot.running { background: #4ade80 }
|
|
3882
|
+
.status-dot.stopped { background: #f87171 }
|
|
3883
|
+
</style>
|
|
3884
|
+
</head>
|
|
3885
|
+
<body>
|
|
3886
|
+
<h1>Kora Sync Server</h1>
|
|
3887
|
+
<p class="subtitle">v${version} · <span class="status-dot running"></span>Running</p>
|
|
3888
|
+
<div class="grid">
|
|
3889
|
+
<div class="card"><div class="label">Uptime</div><div class="value">${uptime}</div></div>
|
|
3890
|
+
<div class="card"><div class="label">Connected Clients</div><div class="value" id="connectedClients">${status.connectedClients}</div></div>
|
|
3891
|
+
<div class="card"><div class="label">Total Operations</div><div class="value" id="totalOperations">${status.totalOperations}</div></div>
|
|
3892
|
+
<div class="card"><div class="label">Peak Connections</div><div class="value green" id="peakConnections">${status.peakConnections}</div></div>
|
|
3893
|
+
<div class="card"><div class="label">Ops Received</div><div class="value" id="opsReceived">${status.operationsReceived}</div></div>
|
|
3894
|
+
<div class="card"><div class="label">Ops Sent</div><div class="value" id="opsSent">${status.operationsSent}</div></div>
|
|
3895
|
+
<div class="card"><div class="label">Errors</div><div class="value ${status.errorCount > 0 ? "red" : "green"}" id="errors">${status.errorCount}</div></div>
|
|
3896
|
+
<div class="card"><div class="label">Schema Version</div><div class="value">${status.schemaVersion}</div></div>
|
|
3897
|
+
</div>
|
|
3898
|
+
<script>
|
|
3899
|
+
(function() {
|
|
3900
|
+
const es = new EventSource('/__kora/events');
|
|
3901
|
+
es.addEventListener('status', (e) => {
|
|
3902
|
+
const s = JSON.parse(e.data);
|
|
3903
|
+
for (const [id, val] of Object.entries({
|
|
3904
|
+
connectedClients: s.connectedClients,
|
|
3905
|
+
totalOperations: s.totalOperations,
|
|
3906
|
+
peakConnections: s.peakConnections,
|
|
3907
|
+
opsReceived: s.operationsReceived,
|
|
3908
|
+
opsSent: s.operationsSent,
|
|
3909
|
+
errors: s.errorCount,
|
|
3910
|
+
})) {
|
|
3911
|
+
const el = document.getElementById(id);
|
|
3912
|
+
if (el) { el.textContent = String(val); el.className = 'value' + (id === 'errors' && val > 0 ? ' red' : id === 'errors' ? ' green' : ''); }
|
|
3913
|
+
}
|
|
3914
|
+
});
|
|
3915
|
+
es.onerror = () => { setTimeout(() => document.location.reload(), 5000); };
|
|
3916
|
+
})();
|
|
3917
|
+
</script>
|
|
3918
|
+
</body>
|
|
3919
|
+
</html>`;
|
|
3920
|
+
}
|
|
3921
|
+
function formatUptime(ms) {
|
|
3922
|
+
const seconds = Math.floor(ms / 1e3);
|
|
3923
|
+
const minutes = Math.floor(seconds / 60);
|
|
3924
|
+
const hours = Math.floor(minutes / 60);
|
|
3925
|
+
const parts = [];
|
|
3926
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
3927
|
+
if (minutes % 60 > 0) parts.push(`${minutes % 60}m`);
|
|
3928
|
+
parts.push(`${seconds % 60}s`);
|
|
3929
|
+
return parts.join(" ");
|
|
3930
|
+
}
|
|
2359
3931
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2360
3932
|
0 && (module.exports = {
|
|
3933
|
+
AwarenessRelay,
|
|
2361
3934
|
ClientSession,
|
|
2362
3935
|
HttpServerTransport,
|
|
2363
3936
|
KoraAuthProvider,
|
|
2364
3937
|
KoraSyncServer,
|
|
2365
3938
|
MemoryServerStore,
|
|
3939
|
+
MixedAuthProvider,
|
|
2366
3940
|
NoAuthProvider,
|
|
2367
3941
|
PostgresServerStore,
|
|
2368
3942
|
SqliteServerStore,
|
|
2369
3943
|
TokenAuthProvider,
|
|
2370
3944
|
WsServerTransport,
|
|
3945
|
+
createDefaultLogger,
|
|
3946
|
+
createJsonLogger,
|
|
2371
3947
|
createKoraServer,
|
|
2372
3948
|
createPostgresServerStore,
|
|
3949
|
+
createPrettyLogger,
|
|
2373
3950
|
createProductionServer,
|
|
3951
|
+
createSilentLogger,
|
|
2374
3952
|
createSqliteServerStore
|
|
2375
3953
|
});
|
|
2376
3954
|
//# sourceMappingURL=index.cjs.map
|