@korajs/server 0.6.1 → 1.0.0-beta.1
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/LICENSE +21 -0
- package/dist/index.cjs +527 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +155 -3
- package/dist/index.d.ts +155 -3
- package/dist/index.js +477 -13
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -250,10 +250,11 @@ function createDefaultLogger() {
|
|
|
250
250
|
|
|
251
251
|
// src/store/memory-server-store.ts
|
|
252
252
|
init_cjs_shims();
|
|
253
|
-
var
|
|
253
|
+
var import_core2 = require("@korajs/core");
|
|
254
254
|
|
|
255
255
|
// src/store/materialization.ts
|
|
256
256
|
init_cjs_shims();
|
|
257
|
+
var import_core = require("@korajs/core");
|
|
257
258
|
function fieldTypeToSql(descriptor, dialect) {
|
|
258
259
|
switch (descriptor.kind) {
|
|
259
260
|
case "string":
|
|
@@ -349,6 +350,52 @@ function replayOperationsForRecord(ops) {
|
|
|
349
350
|
}
|
|
350
351
|
return deleted ? null : record;
|
|
351
352
|
}
|
|
353
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
354
|
+
var BASE64_REVERSE = new Map(
|
|
355
|
+
Array.from(BASE64_ALPHABET, (char, index3) => [char, index3])
|
|
356
|
+
);
|
|
357
|
+
function base64ToBytes(base64) {
|
|
358
|
+
const cleaned = base64.replace(/=+$/, "");
|
|
359
|
+
const out = new Uint8Array(Math.floor(cleaned.length * 6 / 8));
|
|
360
|
+
let buffer = 0;
|
|
361
|
+
let bits = 0;
|
|
362
|
+
let index3 = 0;
|
|
363
|
+
for (const char of cleaned) {
|
|
364
|
+
const value = BASE64_REVERSE.get(char);
|
|
365
|
+
if (value === void 0) {
|
|
366
|
+
throw new import_core.StorageError(`Invalid base64 character "${char}" in tagged richtext value`, {
|
|
367
|
+
char
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
buffer = buffer << 6 | value;
|
|
371
|
+
bits += 6;
|
|
372
|
+
if (bits >= 8) {
|
|
373
|
+
bits -= 8;
|
|
374
|
+
out[index3] = buffer >> bits & 255;
|
|
375
|
+
index3 += 1;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
function decodeRichtextColumnValue(value) {
|
|
381
|
+
if (typeof value !== "object" || value === null || ArrayBuffer.isView(value)) {
|
|
382
|
+
return value;
|
|
383
|
+
}
|
|
384
|
+
const record = value;
|
|
385
|
+
if (Object.keys(record).length === 1 && typeof record.$koraBytes === "string") {
|
|
386
|
+
return base64ToBytes(record.$koraBytes);
|
|
387
|
+
}
|
|
388
|
+
const keys = Object.keys(record);
|
|
389
|
+
if (keys.length > 0 && keys.every((k, i) => k === String(i) && typeof record[k] === "number")) {
|
|
390
|
+
const bytes = new Uint8Array(keys.length);
|
|
391
|
+
for (let i = 0; i < keys.length; i++) {
|
|
392
|
+
const byte = record[String(i)];
|
|
393
|
+
bytes[i] = typeof byte === "number" ? byte & 255 : 0;
|
|
394
|
+
}
|
|
395
|
+
return bytes;
|
|
396
|
+
}
|
|
397
|
+
return value;
|
|
398
|
+
}
|
|
352
399
|
function serializeFieldValue(value, descriptor) {
|
|
353
400
|
if (value === null || value === void 0) return null;
|
|
354
401
|
switch (descriptor.kind) {
|
|
@@ -356,6 +403,8 @@ function serializeFieldValue(value, descriptor) {
|
|
|
356
403
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
357
404
|
case "boolean":
|
|
358
405
|
return value ? 1 : 0;
|
|
406
|
+
case "richtext":
|
|
407
|
+
return decodeRichtextColumnValue(value);
|
|
359
408
|
default:
|
|
360
409
|
return value;
|
|
361
410
|
}
|
|
@@ -401,7 +450,7 @@ var MemoryServerStore = class {
|
|
|
401
450
|
materializedRecords = /* @__PURE__ */ new Map();
|
|
402
451
|
closed = false;
|
|
403
452
|
constructor(nodeId) {
|
|
404
|
-
this.nodeId = nodeId ?? (0,
|
|
453
|
+
this.nodeId = nodeId ?? (0, import_core2.generateUUIDv7)();
|
|
405
454
|
}
|
|
406
455
|
getVersionVector() {
|
|
407
456
|
return new Map(this.versionVector);
|
|
@@ -738,7 +787,7 @@ var MemoryServerStore = class {
|
|
|
738
787
|
|
|
739
788
|
// src/store/postgres-server-store.ts
|
|
740
789
|
init_cjs_shims();
|
|
741
|
-
var
|
|
790
|
+
var import_core3 = require("@korajs/core");
|
|
742
791
|
var import_drizzle_orm = require("drizzle-orm");
|
|
743
792
|
|
|
744
793
|
// src/store/drizzle-pg-schema.ts
|
|
@@ -787,7 +836,7 @@ var PostgresServerStore = class {
|
|
|
787
836
|
closed = false;
|
|
788
837
|
constructor(db, nodeId) {
|
|
789
838
|
this.db = db;
|
|
790
|
-
this.nodeId = nodeId ?? (0,
|
|
839
|
+
this.nodeId = nodeId ?? (0, import_core3.generateUUIDv7)();
|
|
791
840
|
this.ready = this.initialize();
|
|
792
841
|
}
|
|
793
842
|
getVersionVector() {
|
|
@@ -1323,7 +1372,7 @@ async function loadPostgresDeps() {
|
|
|
1323
1372
|
// src/store/sqlite-server-store.ts
|
|
1324
1373
|
init_cjs_shims();
|
|
1325
1374
|
var import_node_module = require("module");
|
|
1326
|
-
var
|
|
1375
|
+
var import_core4 = require("@korajs/core");
|
|
1327
1376
|
var import_drizzle_orm2 = require("drizzle-orm");
|
|
1328
1377
|
|
|
1329
1378
|
// src/store/drizzle-schema.ts
|
|
@@ -1371,7 +1420,7 @@ var SqliteServerStore = class {
|
|
|
1371
1420
|
closed = false;
|
|
1372
1421
|
constructor(db, nodeId) {
|
|
1373
1422
|
this.db = db;
|
|
1374
|
-
this.nodeId = nodeId ?? (0,
|
|
1423
|
+
this.nodeId = nodeId ?? (0, import_core4.generateUUIDv7)();
|
|
1375
1424
|
this.ensureTables();
|
|
1376
1425
|
}
|
|
1377
1426
|
getVersionVector() {
|
|
@@ -1869,7 +1918,7 @@ function createSqliteServerStore(options) {
|
|
|
1869
1918
|
|
|
1870
1919
|
// src/transport/http-server-transport.ts
|
|
1871
1920
|
init_cjs_shims();
|
|
1872
|
-
var
|
|
1921
|
+
var import_core5 = require("@korajs/core");
|
|
1873
1922
|
var HttpServerTransport = class {
|
|
1874
1923
|
serializer;
|
|
1875
1924
|
messageHandler = null;
|
|
@@ -1911,7 +1960,7 @@ var HttpServerTransport = class {
|
|
|
1911
1960
|
}
|
|
1912
1961
|
receive(payload) {
|
|
1913
1962
|
if (!this.connected) {
|
|
1914
|
-
throw new
|
|
1963
|
+
throw new import_core5.SyncError("HTTP server transport is closed");
|
|
1915
1964
|
}
|
|
1916
1965
|
try {
|
|
1917
1966
|
const message = this.serializer.decode(payload);
|
|
@@ -1951,7 +2000,7 @@ var HttpServerTransport = class {
|
|
|
1951
2000
|
|
|
1952
2001
|
// src/transport/ws-server-transport.ts
|
|
1953
2002
|
init_cjs_shims();
|
|
1954
|
-
var
|
|
2003
|
+
var import_core6 = require("@korajs/core");
|
|
1955
2004
|
var import_sync = require("@korajs/sync");
|
|
1956
2005
|
var WS_OPEN = 1;
|
|
1957
2006
|
var WsServerTransport = class {
|
|
@@ -1967,7 +2016,7 @@ var WsServerTransport = class {
|
|
|
1967
2016
|
}
|
|
1968
2017
|
send(message) {
|
|
1969
2018
|
if (this.ws.readyState !== WS_OPEN) {
|
|
1970
|
-
throw new
|
|
2019
|
+
throw new import_core6.SyncError("Cannot send message: WebSocket is not open", {
|
|
1971
2020
|
readyState: this.ws.readyState,
|
|
1972
2021
|
messageType: message.type
|
|
1973
2022
|
});
|
|
@@ -1994,7 +2043,7 @@ var WsServerTransport = class {
|
|
|
1994
2043
|
this.ws.on("message", (data) => {
|
|
1995
2044
|
try {
|
|
1996
2045
|
if (typeof data !== "string" && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer)) {
|
|
1997
|
-
throw new
|
|
2046
|
+
throw new import_core6.SyncError("Unsupported WebSocket payload type", {
|
|
1998
2047
|
payloadType: typeof data
|
|
1999
2048
|
});
|
|
2000
2049
|
}
|
|
@@ -2015,9 +2064,10 @@ var WsServerTransport = class {
|
|
|
2015
2064
|
|
|
2016
2065
|
// src/session/client-session.ts
|
|
2017
2066
|
init_cjs_shims();
|
|
2018
|
-
var
|
|
2067
|
+
var import_core10 = require("@korajs/core");
|
|
2019
2068
|
var import_internal = require("@korajs/core/internal");
|
|
2020
2069
|
var import_sync2 = require("@korajs/sync");
|
|
2070
|
+
var import_sync3 = require("@korajs/sync");
|
|
2021
2071
|
|
|
2022
2072
|
// src/apply/apply-server-operation.ts
|
|
2023
2073
|
init_cjs_shims();
|
|
@@ -2107,12 +2157,12 @@ function createServerReferentialContext(store) {
|
|
|
2107
2157
|
|
|
2108
2158
|
// src/apply/server-side-effect-operation.ts
|
|
2109
2159
|
init_cjs_shims();
|
|
2110
|
-
var
|
|
2160
|
+
var import_core7 = require("@korajs/core");
|
|
2111
2161
|
async function createServerSideEffectOperation(store, parentOp, effect, schemaVersion, sequenceNumber) {
|
|
2112
2162
|
const nodeId = store.getNodeId();
|
|
2113
|
-
const clock = new
|
|
2163
|
+
const clock = new import_core7.HybridLogicalClock(nodeId);
|
|
2114
2164
|
clock.receive(parentOp.timestamp);
|
|
2115
|
-
return (0,
|
|
2165
|
+
return (0, import_core7.createOperation)(
|
|
2116
2166
|
{
|
|
2117
2167
|
nodeId,
|
|
2118
2168
|
type: effect.type === "delete" ? "delete" : "update",
|
|
@@ -2190,21 +2240,29 @@ async function applyServerOperation(store, op, relationLookup) {
|
|
|
2190
2240
|
};
|
|
2191
2241
|
}
|
|
2192
2242
|
|
|
2243
|
+
// src/auth/no-auth.ts
|
|
2244
|
+
init_cjs_shims();
|
|
2245
|
+
var NoAuthProvider = class {
|
|
2246
|
+
async authenticate(_token) {
|
|
2247
|
+
return { userId: "anonymous" };
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
|
|
2193
2251
|
// src/scopes/resolve-session-scopes.ts
|
|
2194
2252
|
init_cjs_shims();
|
|
2195
|
-
var
|
|
2253
|
+
var import_core8 = require("@korajs/core");
|
|
2196
2254
|
function resolveSessionScopes(schema, options) {
|
|
2197
2255
|
const { handshakeScope, authScopes, scopeValues } = options;
|
|
2198
2256
|
let resolved;
|
|
2199
2257
|
if (schema && scopeValues && Object.keys(scopeValues).length > 0) {
|
|
2200
|
-
resolved = (0,
|
|
2258
|
+
resolved = (0, import_core8.buildScopeMap)(schema, scopeValues);
|
|
2201
2259
|
} else if (handshakeScope) {
|
|
2202
2260
|
resolved = { ...handshakeScope };
|
|
2203
2261
|
} else if (authScopes) {
|
|
2204
2262
|
resolved = { ...authScopes };
|
|
2205
2263
|
}
|
|
2206
2264
|
if (schema && !resolved && scopeValues) {
|
|
2207
|
-
resolved = (0,
|
|
2265
|
+
resolved = (0, import_core8.buildScopeMap)(schema, scopeValues);
|
|
2208
2266
|
}
|
|
2209
2267
|
if (authScopes) {
|
|
2210
2268
|
resolved = mergeScopeMaps(resolved, authScopes, "auth");
|
|
@@ -2259,9 +2317,17 @@ function asRecord(value) {
|
|
|
2259
2317
|
|
|
2260
2318
|
// src/session/operation-validation.ts
|
|
2261
2319
|
init_cjs_shims();
|
|
2320
|
+
var import_core9 = require("@korajs/core");
|
|
2262
2321
|
var SERVER_MAX_TIMESTAMP_FUTURE_MS = 6e4;
|
|
2263
2322
|
function isOperationTimestampValid(op, now = Date.now()) {
|
|
2264
|
-
|
|
2323
|
+
const { wallTime, logical } = op.timestamp;
|
|
2324
|
+
if (!Number.isInteger(wallTime) || wallTime < 0) {
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
if (!Number.isInteger(logical) || logical < 0 || logical > import_core9.MAX_LOGICAL) {
|
|
2328
|
+
return false;
|
|
2329
|
+
}
|
|
2330
|
+
return wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS;
|
|
2265
2331
|
}
|
|
2266
2332
|
|
|
2267
2333
|
// src/session/session-operation-limits.ts
|
|
@@ -2311,6 +2377,25 @@ var SessionRateLimiter = class {
|
|
|
2311
2377
|
// src/session/client-session.ts
|
|
2312
2378
|
var DEFAULT_BATCH_SIZE = 100;
|
|
2313
2379
|
var DEFAULT_SCHEMA_VERSION = 1;
|
|
2380
|
+
var warnedUnscopedProviders = /* @__PURE__ */ new WeakSet();
|
|
2381
|
+
function warnIfMultiTenantWithoutScopes(auth, resolvedScopes, schema) {
|
|
2382
|
+
if (!auth || auth instanceof NoAuthProvider) {
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
if (resolvedScopes) {
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
if (!schema || Object.keys(schema.collections).length === 0) {
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
if (warnedUnscopedProviders.has(auth)) {
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
warnedUnscopedProviders.add(auth);
|
|
2395
|
+
console.warn(
|
|
2396
|
+
"[kora] An authenticated session resolved to no sync scopes, so every user will sync every other user's data. Return per-user sync scopes from your auth provider (for example KoraAuthProvider's resolveScopes) to isolate tenants. Note: declaring sync rules in your schema is not enough on its own \u2014 the per-user values come from the auth provider. (This warning is expected for single-tenant apps where all authenticated users are meant to share the same data.)"
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2314
2399
|
var ClientSession = class {
|
|
2315
2400
|
state = "connected";
|
|
2316
2401
|
clientNodeId = null;
|
|
@@ -2329,6 +2414,9 @@ var ClientSession = class {
|
|
|
2329
2414
|
onRelay;
|
|
2330
2415
|
onAwarenessUpdate;
|
|
2331
2416
|
onYjsDocUpdate;
|
|
2417
|
+
onBlobChunkRequest;
|
|
2418
|
+
onBlobChunkResponse;
|
|
2419
|
+
persistBlobChunk;
|
|
2332
2420
|
onClose;
|
|
2333
2421
|
maxOperationBytes;
|
|
2334
2422
|
maxOpsPerMinute;
|
|
@@ -2338,7 +2426,7 @@ var ClientSession = class {
|
|
|
2338
2426
|
this.transport = options.transport;
|
|
2339
2427
|
this.store = options.store;
|
|
2340
2428
|
this.auth = options.auth ?? null;
|
|
2341
|
-
this.serializer = options.serializer ?? new
|
|
2429
|
+
this.serializer = options.serializer ?? new import_sync3.NegotiatedMessageSerializer("json");
|
|
2342
2430
|
this.emitter = options.emitter ?? null;
|
|
2343
2431
|
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2344
2432
|
this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
@@ -2350,6 +2438,9 @@ var ClientSession = class {
|
|
|
2350
2438
|
this.onRelay = options.onRelay ?? null;
|
|
2351
2439
|
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
2352
2440
|
this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
|
|
2441
|
+
this.onBlobChunkRequest = options.onBlobChunkRequest ?? null;
|
|
2442
|
+
this.onBlobChunkResponse = options.onBlobChunkResponse ?? null;
|
|
2443
|
+
this.persistBlobChunk = options.persistBlobChunk ?? null;
|
|
2353
2444
|
this.onClose = options.onClose ?? null;
|
|
2354
2445
|
this.maxOperationBytes = options.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
|
|
2355
2446
|
this.maxOpsPerMinute = options.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE;
|
|
@@ -2379,7 +2470,7 @@ var ClientSession = class {
|
|
|
2379
2470
|
const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
|
|
2380
2471
|
const msg = {
|
|
2381
2472
|
type: "operation-batch",
|
|
2382
|
-
messageId: (0,
|
|
2473
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2383
2474
|
operations: serializedOps,
|
|
2384
2475
|
isFinal: true,
|
|
2385
2476
|
batchIndex: 0
|
|
@@ -2455,7 +2546,32 @@ var ClientSession = class {
|
|
|
2455
2546
|
case "yjs-doc-update":
|
|
2456
2547
|
this.handleYjsDocUpdate(message);
|
|
2457
2548
|
break;
|
|
2549
|
+
case "blob-chunk-request":
|
|
2550
|
+
this.onBlobChunkRequest?.(this.sessionId, message);
|
|
2551
|
+
break;
|
|
2552
|
+
case "blob-chunk-response":
|
|
2553
|
+
this.onBlobChunkResponse?.(this.sessionId, message);
|
|
2554
|
+
break;
|
|
2555
|
+
case "blob-chunk-push":
|
|
2556
|
+
await this.handleBlobChunkPush(message);
|
|
2557
|
+
break;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Persist a client-uploaded blob chunk (or manifest) centrally. The bytes are
|
|
2562
|
+
* verified to hash to the declared hash before storing, so a corrupt or
|
|
2563
|
+
* mislabeled upload is rejected rather than served later as trusted content.
|
|
2564
|
+
*/
|
|
2565
|
+
async handleBlobChunkPush(message) {
|
|
2566
|
+
if (!this.persistBlobChunk) {
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
const bytes = (0, import_sync2.decodeBlobChunkBytes)(message.bytes);
|
|
2570
|
+
const actual = await (0, import_core10.hashBlob)(bytes);
|
|
2571
|
+
if (actual !== message.hash) {
|
|
2572
|
+
return;
|
|
2458
2573
|
}
|
|
2574
|
+
await this.persistBlobChunk(message.hash, bytes);
|
|
2459
2575
|
}
|
|
2460
2576
|
handleMessageFailure(error) {
|
|
2461
2577
|
const reason = error instanceof Error ? error.message : "Message handling failed";
|
|
@@ -2490,27 +2606,29 @@ var ClientSession = class {
|
|
|
2490
2606
|
this.authContext = { userId: msg.nodeId, scopes: resolvedScopes };
|
|
2491
2607
|
}
|
|
2492
2608
|
}
|
|
2609
|
+
warnIfMultiTenantWithoutScopes(this.auth, resolvedScopes, this.store.getSchema());
|
|
2493
2610
|
if (msg.syncQueries && msg.syncQueries.length > 0) {
|
|
2494
|
-
this.syncQuerySubsets = (0,
|
|
2611
|
+
this.syncQuerySubsets = (0, import_sync3.dedupeQuerySubsets)(msg.syncQueries);
|
|
2495
2612
|
} else {
|
|
2496
2613
|
this.syncQuerySubsets = [];
|
|
2497
2614
|
}
|
|
2498
|
-
this.resumeDeltaCursor = msg.deltaCursor ? (0,
|
|
2615
|
+
this.resumeDeltaCursor = msg.deltaCursor ? (0, import_sync3.decodeDeltaCursor)(msg.deltaCursor) : null;
|
|
2499
2616
|
const serverVector = this.store.getVersionVector();
|
|
2500
2617
|
const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
|
|
2501
2618
|
this.setSerializerWireFormat(selectedWireFormat);
|
|
2502
|
-
if (!(0,
|
|
2619
|
+
if (!(0, import_sync3.isClientSchemaVersionSupported)(msg.schemaVersion, this.supportedSchemaVersions)) {
|
|
2503
2620
|
const { min, max } = this.supportedSchemaVersions;
|
|
2504
2621
|
const response2 = {
|
|
2505
2622
|
type: "handshake-response",
|
|
2506
|
-
messageId: (0,
|
|
2623
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2507
2624
|
nodeId: this.store.getNodeId(),
|
|
2508
|
-
versionVector: (0,
|
|
2625
|
+
versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
|
|
2509
2626
|
schemaVersion: this.schemaVersion,
|
|
2510
2627
|
accepted: false,
|
|
2511
|
-
rejectReason: `${
|
|
2628
|
+
rejectReason: `${import_sync3.SCHEMA_MISMATCH_PREFIX}: client schema version ${msg.schemaVersion} not in supported range [${min}, ${max}]`,
|
|
2512
2629
|
supportedSchemaMin: min,
|
|
2513
|
-
supportedSchemaMax: max
|
|
2630
|
+
supportedSchemaMax: max,
|
|
2631
|
+
serverTime: Date.now()
|
|
2514
2632
|
};
|
|
2515
2633
|
this.sendToClient(response2);
|
|
2516
2634
|
this.close("schema version mismatch");
|
|
@@ -2518,12 +2636,16 @@ var ClientSession = class {
|
|
|
2518
2636
|
}
|
|
2519
2637
|
const response = {
|
|
2520
2638
|
type: "handshake-response",
|
|
2521
|
-
messageId: (0,
|
|
2639
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2522
2640
|
nodeId: this.store.getNodeId(),
|
|
2523
|
-
versionVector: (0,
|
|
2641
|
+
versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
|
|
2524
2642
|
schemaVersion: this.schemaVersion,
|
|
2525
2643
|
accepted: true,
|
|
2526
2644
|
selectedWireFormat,
|
|
2645
|
+
serverTime: Date.now(),
|
|
2646
|
+
// Advertise central blob storage so the client uploads the bytes behind
|
|
2647
|
+
// its blob fields, keeping them available after the author goes offline.
|
|
2648
|
+
...this.persistBlobChunk ? { blobStorageEnabled: true } : {},
|
|
2527
2649
|
// Confirm the accepted scope so the client knows what data will be synced.
|
|
2528
2650
|
// This may differ from what the client requested if auth scopes are narrower.
|
|
2529
2651
|
...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
|
|
@@ -2531,7 +2653,7 @@ var ClientSession = class {
|
|
|
2531
2653
|
this.sendToClient(response);
|
|
2532
2654
|
this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
|
|
2533
2655
|
this.state = "syncing";
|
|
2534
|
-
const clientVector = (0,
|
|
2656
|
+
const clientVector = (0, import_sync3.wireToVersionVector)(msg.versionVector);
|
|
2535
2657
|
await this.sendDelta(clientVector);
|
|
2536
2658
|
this.state = "streaming";
|
|
2537
2659
|
}
|
|
@@ -2600,7 +2722,7 @@ var ClientSession = class {
|
|
|
2600
2722
|
const lastOp = operations2[operations2.length - 1];
|
|
2601
2723
|
const ack = {
|
|
2602
2724
|
type: "acknowledgment",
|
|
2603
|
-
messageId: (0,
|
|
2725
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2604
2726
|
acknowledgedMessageId: msg.messageId,
|
|
2605
2727
|
lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
|
|
2606
2728
|
};
|
|
@@ -2623,7 +2745,7 @@ var ClientSession = class {
|
|
|
2623
2745
|
if (missing.length === 0) {
|
|
2624
2746
|
const emptyBatch = {
|
|
2625
2747
|
type: "operation-batch",
|
|
2626
|
-
messageId: (0,
|
|
2748
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2627
2749
|
operations: [],
|
|
2628
2750
|
isFinal: true,
|
|
2629
2751
|
batchIndex: 0,
|
|
@@ -2633,12 +2755,12 @@ var ClientSession = class {
|
|
|
2633
2755
|
return;
|
|
2634
2756
|
}
|
|
2635
2757
|
const sorted = (0, import_internal.topologicalSort)(missing);
|
|
2636
|
-
const afterCursor = (0,
|
|
2758
|
+
const afterCursor = (0, import_sync3.sliceOperationsAfterCursor)(sorted, this.resumeDeltaCursor);
|
|
2637
2759
|
const totalBatches = Math.ceil(afterCursor.length / this.batchSize);
|
|
2638
2760
|
if (afterCursor.length === 0) {
|
|
2639
2761
|
const emptyBatch = {
|
|
2640
2762
|
type: "operation-batch",
|
|
2641
|
-
messageId: (0,
|
|
2763
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2642
2764
|
operations: [],
|
|
2643
2765
|
isFinal: true,
|
|
2644
2766
|
batchIndex: this.resumeDeltaCursor?.batchIndex ?? 0,
|
|
@@ -2651,15 +2773,15 @@ var ClientSession = class {
|
|
|
2651
2773
|
const start = i * this.batchSize;
|
|
2652
2774
|
const batchOps = afterCursor.slice(start, start + this.batchSize);
|
|
2653
2775
|
const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
|
|
2654
|
-
const batchCursor = (0,
|
|
2776
|
+
const batchCursor = (0, import_sync3.createDeltaCursorFromBatch)(batchOps, i);
|
|
2655
2777
|
const batchMsg = {
|
|
2656
2778
|
type: "operation-batch",
|
|
2657
|
-
messageId: (0,
|
|
2779
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2658
2780
|
operations: serializedOps,
|
|
2659
2781
|
isFinal: i === totalBatches - 1,
|
|
2660
2782
|
batchIndex: i,
|
|
2661
2783
|
totalBatches,
|
|
2662
|
-
...batchCursor ? { cursor: (0,
|
|
2784
|
+
...batchCursor ? { cursor: (0, import_sync3.encodeDeltaCursor)(batchCursor) } : {}
|
|
2663
2785
|
};
|
|
2664
2786
|
this.sendToClient(batchMsg);
|
|
2665
2787
|
this.emitter?.emit({
|
|
@@ -2673,7 +2795,7 @@ var ClientSession = class {
|
|
|
2673
2795
|
if (!operationMatchesScopes(op, this.authContext?.scopes)) {
|
|
2674
2796
|
return false;
|
|
2675
2797
|
}
|
|
2676
|
-
return (0,
|
|
2798
|
+
return (0, import_sync3.operationMatchesQuerySubsets)(op, this.syncQuerySubsets);
|
|
2677
2799
|
}
|
|
2678
2800
|
handleAwarenessUpdate(msg) {
|
|
2679
2801
|
this.onAwarenessUpdate?.(this.sessionId, msg);
|
|
@@ -2684,7 +2806,7 @@ var ClientSession = class {
|
|
|
2684
2806
|
sendError(code, message, retriable) {
|
|
2685
2807
|
const errorMsg = {
|
|
2686
2808
|
type: "error",
|
|
2687
|
-
messageId: (0,
|
|
2809
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2688
2810
|
code,
|
|
2689
2811
|
message,
|
|
2690
2812
|
retriable
|
|
@@ -2712,13 +2834,13 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
2712
2834
|
|
|
2713
2835
|
// src/server/kora-sync-server.ts
|
|
2714
2836
|
init_cjs_shims();
|
|
2715
|
-
var
|
|
2837
|
+
var import_core12 = require("@korajs/core");
|
|
2716
2838
|
var import_internal2 = require("@korajs/core/internal");
|
|
2717
|
-
var
|
|
2839
|
+
var import_sync5 = require("@korajs/sync");
|
|
2718
2840
|
|
|
2719
2841
|
// src/awareness/awareness-relay.ts
|
|
2720
2842
|
init_cjs_shims();
|
|
2721
|
-
var
|
|
2843
|
+
var import_core11 = require("@korajs/core");
|
|
2722
2844
|
var AwarenessRelay = class {
|
|
2723
2845
|
clients = /* @__PURE__ */ new Map();
|
|
2724
2846
|
/**
|
|
@@ -2747,7 +2869,7 @@ var AwarenessRelay = class {
|
|
|
2747
2869
|
if (hasStates) {
|
|
2748
2870
|
const catchUpMsg = {
|
|
2749
2871
|
type: "awareness-update",
|
|
2750
|
-
messageId: (0,
|
|
2872
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
2751
2873
|
clientId: 0,
|
|
2752
2874
|
// Server-sourced
|
|
2753
2875
|
states: existingStates
|
|
@@ -2770,7 +2892,7 @@ var AwarenessRelay = class {
|
|
|
2770
2892
|
};
|
|
2771
2893
|
const msg = {
|
|
2772
2894
|
type: "awareness-update",
|
|
2773
|
-
messageId: (0,
|
|
2895
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
2774
2896
|
clientId: client.clientId,
|
|
2775
2897
|
states: removalStates
|
|
2776
2898
|
};
|
|
@@ -2931,6 +3053,114 @@ var ServerMetricsCollector = class {
|
|
|
2931
3053
|
}
|
|
2932
3054
|
};
|
|
2933
3055
|
|
|
3056
|
+
// src/richtext/blob-chunk-relay.ts
|
|
3057
|
+
init_cjs_shims();
|
|
3058
|
+
var import_sync4 = require("@korajs/sync");
|
|
3059
|
+
var BlobChunkRelay = class {
|
|
3060
|
+
clients = /* @__PURE__ */ new Map();
|
|
3061
|
+
/** requestId -> the session that originated the request (to route the answer back). */
|
|
3062
|
+
pending = /* @__PURE__ */ new Map();
|
|
3063
|
+
resolveBlobChunk;
|
|
3064
|
+
constructor(resolveBlobChunk) {
|
|
3065
|
+
this.resolveBlobChunk = resolveBlobChunk ?? null;
|
|
3066
|
+
}
|
|
3067
|
+
addClient(sessionId, transport) {
|
|
3068
|
+
this.clients.set(sessionId, { sessionId, transport });
|
|
3069
|
+
}
|
|
3070
|
+
removeClient(sessionId) {
|
|
3071
|
+
this.clients.delete(sessionId);
|
|
3072
|
+
for (const [requestId, originSessionId] of this.pending) {
|
|
3073
|
+
if (originSessionId === sessionId) {
|
|
3074
|
+
this.pending.delete(requestId);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
getClientCount() {
|
|
3079
|
+
return this.clients.size;
|
|
3080
|
+
}
|
|
3081
|
+
getPendingCount() {
|
|
3082
|
+
return this.pending.size;
|
|
3083
|
+
}
|
|
3084
|
+
clear() {
|
|
3085
|
+
this.clients.clear();
|
|
3086
|
+
this.pending.clear();
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Handle an inbound chunk request from a session. Tries the server's own
|
|
3090
|
+
* store first (if configured), otherwise forwards the request to peer
|
|
3091
|
+
* sessions and remembers who to route the answer back to.
|
|
3092
|
+
*/
|
|
3093
|
+
handleRequest(sourceSessionId, message) {
|
|
3094
|
+
if (!this.clients.has(sourceSessionId)) {
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
if (this.resolveBlobChunk) {
|
|
3098
|
+
void this.resolveBlobChunk(message.hash).then(
|
|
3099
|
+
(bytes) => {
|
|
3100
|
+
if (bytes !== null) {
|
|
3101
|
+
this.sendResponseTo(sourceSessionId, message.requestId, (0, import_sync4.encodeBlobChunkBytes)(bytes));
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
this.forwardRequestToPeers(sourceSessionId, message);
|
|
3105
|
+
},
|
|
3106
|
+
() => {
|
|
3107
|
+
this.forwardRequestToPeers(sourceSessionId, message);
|
|
3108
|
+
}
|
|
3109
|
+
);
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
this.forwardRequestToPeers(sourceSessionId, message);
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Handle an inbound chunk response from a peer. Routes it back to the session
|
|
3116
|
+
* that originally requested it. A "not held" answer (bytes === null) is
|
|
3117
|
+
* ignored so a peer without the chunk does not preempt one that has it; the
|
|
3118
|
+
* requester's own per-request timeout bounds the wait.
|
|
3119
|
+
*/
|
|
3120
|
+
handleResponse(sourceSessionId, message) {
|
|
3121
|
+
if (!this.clients.has(sourceSessionId)) {
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
if (message.bytes === null) {
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
const originSessionId = this.pending.get(message.requestId);
|
|
3128
|
+
if (!originSessionId) {
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
this.pending.delete(message.requestId);
|
|
3132
|
+
this.sendResponseTo(originSessionId, message.requestId, message.bytes);
|
|
3133
|
+
}
|
|
3134
|
+
forwardRequestToPeers(sourceSessionId, message) {
|
|
3135
|
+
this.pending.set(message.requestId, sourceSessionId);
|
|
3136
|
+
for (const [, client] of this.clients) {
|
|
3137
|
+
if (client.sessionId === sourceSessionId) {
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
if (!client.transport.isConnected()) {
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
client.transport.send(message);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
sendResponseTo(sessionId, requestId, bytes) {
|
|
3147
|
+
const client = this.clients.get(sessionId);
|
|
3148
|
+
if (!client || !client.transport.isConnected()) {
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
const response = {
|
|
3152
|
+
type: "blob-chunk-response",
|
|
3153
|
+
messageId: generateResponseId(requestId),
|
|
3154
|
+
requestId,
|
|
3155
|
+
bytes
|
|
3156
|
+
};
|
|
3157
|
+
client.transport.send(response);
|
|
3158
|
+
}
|
|
3159
|
+
};
|
|
3160
|
+
function generateResponseId(requestId) {
|
|
3161
|
+
return `blob-resp-${requestId}`;
|
|
3162
|
+
}
|
|
3163
|
+
|
|
2934
3164
|
// src/richtext/yjs-doc-relay.ts
|
|
2935
3165
|
init_cjs_shims();
|
|
2936
3166
|
var YjsDocRelay = class {
|
|
@@ -2988,6 +3218,8 @@ var KoraSyncServer = class {
|
|
|
2988
3218
|
metrics;
|
|
2989
3219
|
awarenessRelay = new AwarenessRelay();
|
|
2990
3220
|
yjsDocRelay = new YjsDocRelay();
|
|
3221
|
+
blobChunkRelay;
|
|
3222
|
+
persistBlobChunk;
|
|
2991
3223
|
sessions = /* @__PURE__ */ new Map();
|
|
2992
3224
|
httpClients = /* @__PURE__ */ new Map();
|
|
2993
3225
|
httpSessionToClient = /* @__PURE__ */ new Map();
|
|
@@ -2997,7 +3229,7 @@ var KoraSyncServer = class {
|
|
|
2997
3229
|
constructor(config) {
|
|
2998
3230
|
this.store = config.store;
|
|
2999
3231
|
this.auth = config.auth ?? null;
|
|
3000
|
-
this.serializer = config.serializer ?? new
|
|
3232
|
+
this.serializer = config.serializer ?? new import_sync5.JsonMessageSerializer();
|
|
3001
3233
|
this.emitter = config.emitter ?? null;
|
|
3002
3234
|
this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
|
|
3003
3235
|
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
|
|
@@ -3012,6 +3244,8 @@ var KoraSyncServer = class {
|
|
|
3012
3244
|
this.logger = config.logger ?? createDefaultLogger();
|
|
3013
3245
|
this.metrics = config.metricsCollector ?? new ServerMetricsCollector();
|
|
3014
3246
|
this.metrics.setSchemaVersion(this.schemaVersion);
|
|
3247
|
+
this.blobChunkRelay = new BlobChunkRelay(config.resolveBlobChunk);
|
|
3248
|
+
this.persistBlobChunk = config.persistBlobChunk ?? null;
|
|
3015
3249
|
if (!this.emitter) {
|
|
3016
3250
|
this.emitter = new import_internal2.SimpleEventEmitter();
|
|
3017
3251
|
}
|
|
@@ -3084,10 +3318,10 @@ var KoraSyncServer = class {
|
|
|
3084
3318
|
*/
|
|
3085
3319
|
async start(wsServerImpl) {
|
|
3086
3320
|
if (this.running) {
|
|
3087
|
-
throw new
|
|
3321
|
+
throw new import_core12.SyncError("Server is already running", { port: this.port });
|
|
3088
3322
|
}
|
|
3089
3323
|
if (!wsServerImpl && this.port === void 0) {
|
|
3090
|
-
throw new
|
|
3324
|
+
throw new import_core12.SyncError(
|
|
3091
3325
|
"Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
|
|
3092
3326
|
{}
|
|
3093
3327
|
);
|
|
@@ -3135,6 +3369,7 @@ var KoraSyncServer = class {
|
|
|
3135
3369
|
});
|
|
3136
3370
|
this.awarenessRelay.clear();
|
|
3137
3371
|
this.yjsDocRelay.clear();
|
|
3372
|
+
this.blobChunkRelay.clear();
|
|
3138
3373
|
for (const session of this.sessions.values()) {
|
|
3139
3374
|
session.close("server shutting down");
|
|
3140
3375
|
}
|
|
@@ -3196,7 +3431,7 @@ var KoraSyncServer = class {
|
|
|
3196
3431
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
3197
3432
|
transport.send({
|
|
3198
3433
|
type: "error",
|
|
3199
|
-
messageId: (0,
|
|
3434
|
+
messageId: (0, import_core12.generateUUIDv7)(),
|
|
3200
3435
|
code: "MAX_CONNECTIONS",
|
|
3201
3436
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
3202
3437
|
retriable: true
|
|
@@ -3209,12 +3444,12 @@ var KoraSyncServer = class {
|
|
|
3209
3444
|
event: "connection.rejected",
|
|
3210
3445
|
details: { reason: "max_connections", max: this.maxConnections }
|
|
3211
3446
|
});
|
|
3212
|
-
throw new
|
|
3447
|
+
throw new import_core12.SyncError("Maximum connections reached", {
|
|
3213
3448
|
current: this.sessions.size,
|
|
3214
3449
|
max: this.maxConnections
|
|
3215
3450
|
});
|
|
3216
3451
|
}
|
|
3217
|
-
const sessionId = (0,
|
|
3452
|
+
const sessionId = (0, import_core12.generateUUIDv7)();
|
|
3218
3453
|
this.metrics.recordConnection(sessionId);
|
|
3219
3454
|
const sessionEmitter = new import_internal2.SimpleEventEmitter();
|
|
3220
3455
|
sessionEmitter.on("sync:connected", (event) => {
|
|
@@ -3279,12 +3514,20 @@ var KoraSyncServer = class {
|
|
|
3279
3514
|
onYjsDocUpdate: (sourceSessionId, message) => {
|
|
3280
3515
|
this.handleYjsDocRelay(sourceSessionId, message);
|
|
3281
3516
|
},
|
|
3517
|
+
onBlobChunkRequest: (sourceSessionId, message) => {
|
|
3518
|
+
this.blobChunkRelay.handleRequest(sourceSessionId, message);
|
|
3519
|
+
},
|
|
3520
|
+
onBlobChunkResponse: (sourceSessionId, message) => {
|
|
3521
|
+
this.blobChunkRelay.handleResponse(sourceSessionId, message);
|
|
3522
|
+
},
|
|
3523
|
+
...this.persistBlobChunk ? { persistBlobChunk: this.persistBlobChunk } : {},
|
|
3282
3524
|
onClose: (sid) => {
|
|
3283
3525
|
this.handleSessionClose(sid);
|
|
3284
3526
|
}
|
|
3285
3527
|
});
|
|
3286
3528
|
this.sessions.set(sessionId, session);
|
|
3287
3529
|
this.yjsDocRelay.addClient(sessionId, transport);
|
|
3530
|
+
this.blobChunkRelay.addClient(sessionId, transport);
|
|
3288
3531
|
session.start();
|
|
3289
3532
|
this.logger.log({
|
|
3290
3533
|
timestamp: Date.now(),
|
|
@@ -3317,6 +3560,61 @@ var KoraSyncServer = class {
|
|
|
3317
3560
|
errorCount: snapshot.errorCount
|
|
3318
3561
|
};
|
|
3319
3562
|
}
|
|
3563
|
+
/**
|
|
3564
|
+
* Apply a server-originated operation (for example one created by a custom
|
|
3565
|
+
* HTTP route) through the same validated pipeline that incoming client
|
|
3566
|
+
* operations use — Tier 2 constraints, referential integrity, and cascade
|
|
3567
|
+
* side effects — then relay every applied operation to connected clients.
|
|
3568
|
+
*
|
|
3569
|
+
* Because the operation did not come from a client session, it is relayed to
|
|
3570
|
+
* ALL sessions (there is no source session to exclude). Each session still
|
|
3571
|
+
* applies its own per-scope visibility filter in `relayOperations`, so a
|
|
3572
|
+
* client only receives the operation if it falls within that client's scope.
|
|
3573
|
+
*
|
|
3574
|
+
* @param op - A fully-formed, server-originated operation to apply
|
|
3575
|
+
* @returns The apply result, including any server-generated side-effect ops
|
|
3576
|
+
*/
|
|
3577
|
+
async applyLocalOperation(op) {
|
|
3578
|
+
const result = await applyServerOperation(this.store, op);
|
|
3579
|
+
if (result.result === "applied" && result.appliedOperations.length > 0) {
|
|
3580
|
+
for (const session of this.sessions.values()) {
|
|
3581
|
+
session.relayOperations(result.appliedOperations);
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
return result;
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3587
|
+
* Collect every blob reference still reachable from live records on the server.
|
|
3588
|
+
*
|
|
3589
|
+
* This is the live set for garbage-collecting the server's central blob store:
|
|
3590
|
+
* pass the result to `collectBlobGarbage(blobStore, liveRefs)` from
|
|
3591
|
+
* `@korajs/store` to reclaim bytes no record references any more. Only
|
|
3592
|
+
* collections that declare a `blob` field are scanned.
|
|
3593
|
+
*
|
|
3594
|
+
* @returns Every live blob reference across all collections
|
|
3595
|
+
*/
|
|
3596
|
+
async getLiveBlobRefs() {
|
|
3597
|
+
const schema = this.store.getSchema();
|
|
3598
|
+
if (!schema) {
|
|
3599
|
+
return [];
|
|
3600
|
+
}
|
|
3601
|
+
const refs = [];
|
|
3602
|
+
for (const [name, collection] of Object.entries(schema.collections)) {
|
|
3603
|
+
const hasBlobField = Object.values(collection.fields).some((field) => field.kind === "blob");
|
|
3604
|
+
if (!hasBlobField) {
|
|
3605
|
+
continue;
|
|
3606
|
+
}
|
|
3607
|
+
const records = await this.store.materializeCollection(name);
|
|
3608
|
+
for (const record of records) {
|
|
3609
|
+
for (const value of Object.values(record)) {
|
|
3610
|
+
if ((0, import_core12.isBlobRef)(value)) {
|
|
3611
|
+
refs.push(value);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
return refs;
|
|
3617
|
+
}
|
|
3320
3618
|
/**
|
|
3321
3619
|
* Get the number of currently connected clients.
|
|
3322
3620
|
*/
|
|
@@ -3350,6 +3648,7 @@ var KoraSyncServer = class {
|
|
|
3350
3648
|
this.metrics.recordDisconnection(sessionId);
|
|
3351
3649
|
this.awarenessRelay.removeClient(sessionId);
|
|
3352
3650
|
this.yjsDocRelay.removeClient(sessionId);
|
|
3651
|
+
this.blobChunkRelay.removeClient(sessionId);
|
|
3353
3652
|
this.sessions.delete(sessionId);
|
|
3354
3653
|
const clientId = this.httpSessionToClient.get(sessionId);
|
|
3355
3654
|
if (clientId) {
|
|
@@ -3402,14 +3701,6 @@ function normalizeHttpBody(body, contentType) {
|
|
|
3402
3701
|
return body;
|
|
3403
3702
|
}
|
|
3404
3703
|
|
|
3405
|
-
// src/auth/no-auth.ts
|
|
3406
|
-
init_cjs_shims();
|
|
3407
|
-
var NoAuthProvider = class {
|
|
3408
|
-
async authenticate(_token) {
|
|
3409
|
-
return { userId: "anonymous" };
|
|
3410
|
-
}
|
|
3411
|
-
};
|
|
3412
|
-
|
|
3413
3704
|
// src/auth/token-auth.ts
|
|
3414
3705
|
init_cjs_shims();
|
|
3415
3706
|
var TokenAuthProvider = class {
|
|
@@ -3496,6 +3787,162 @@ function createKoraServer(config) {
|
|
|
3496
3787
|
|
|
3497
3788
|
// src/server/production-server.ts
|
|
3498
3789
|
init_cjs_shims();
|
|
3790
|
+
|
|
3791
|
+
// src/server/route-context.ts
|
|
3792
|
+
init_cjs_shims();
|
|
3793
|
+
var import_core13 = require("@korajs/core");
|
|
3794
|
+
function recordMatchesScope(collection, record, scope) {
|
|
3795
|
+
const collectionScope = scope[collection];
|
|
3796
|
+
if (!collectionScope) {
|
|
3797
|
+
return false;
|
|
3798
|
+
}
|
|
3799
|
+
for (const [field, expected] of Object.entries(collectionScope)) {
|
|
3800
|
+
if (record[field] !== expected) {
|
|
3801
|
+
return false;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
return true;
|
|
3805
|
+
}
|
|
3806
|
+
function pickFields(record, keys) {
|
|
3807
|
+
const picked = {};
|
|
3808
|
+
for (const key of keys) {
|
|
3809
|
+
if (key in record) {
|
|
3810
|
+
picked[key] = record[key];
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
return picked;
|
|
3814
|
+
}
|
|
3815
|
+
async function buildRouteOperation(store, mutation) {
|
|
3816
|
+
const nodeId = store.getNodeId();
|
|
3817
|
+
const clock = new import_core13.HybridLogicalClock(nodeId);
|
|
3818
|
+
const schemaVersion = store.getSchema()?.version ?? 1;
|
|
3819
|
+
const sequenceNumber = nextServerSequenceNumber(store);
|
|
3820
|
+
if (mutation.type === "insert") {
|
|
3821
|
+
return (0, import_core13.createOperation)(
|
|
3822
|
+
{
|
|
3823
|
+
nodeId,
|
|
3824
|
+
type: "insert",
|
|
3825
|
+
collection: mutation.collection,
|
|
3826
|
+
recordId: mutation.recordId ?? (0, import_core13.generateUUIDv7)(),
|
|
3827
|
+
data: mutation.data ?? {},
|
|
3828
|
+
previousData: null,
|
|
3829
|
+
sequenceNumber,
|
|
3830
|
+
causalDeps: [],
|
|
3831
|
+
schemaVersion
|
|
3832
|
+
},
|
|
3833
|
+
clock
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
if (!mutation.recordId) {
|
|
3837
|
+
throw new RouteMutationError(
|
|
3838
|
+
"MISSING_RECORD_ID",
|
|
3839
|
+
`A ${mutation.type} mutation on "${mutation.collection}" requires a recordId.`
|
|
3840
|
+
);
|
|
3841
|
+
}
|
|
3842
|
+
const current = await store.findRecord(mutation.collection, mutation.recordId);
|
|
3843
|
+
if (mutation.type === "update") {
|
|
3844
|
+
const data = mutation.data ?? {};
|
|
3845
|
+
return (0, import_core13.createOperation)(
|
|
3846
|
+
{
|
|
3847
|
+
nodeId,
|
|
3848
|
+
type: "update",
|
|
3849
|
+
collection: mutation.collection,
|
|
3850
|
+
recordId: mutation.recordId,
|
|
3851
|
+
data,
|
|
3852
|
+
previousData: current ? pickFields(current, Object.keys(data)) : {},
|
|
3853
|
+
sequenceNumber,
|
|
3854
|
+
causalDeps: [],
|
|
3855
|
+
schemaVersion
|
|
3856
|
+
},
|
|
3857
|
+
clock
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3860
|
+
return (0, import_core13.createOperation)(
|
|
3861
|
+
{
|
|
3862
|
+
nodeId,
|
|
3863
|
+
type: "delete",
|
|
3864
|
+
collection: mutation.collection,
|
|
3865
|
+
recordId: mutation.recordId,
|
|
3866
|
+
data: null,
|
|
3867
|
+
previousData: current ? { ...current } : null,
|
|
3868
|
+
sequenceNumber,
|
|
3869
|
+
causalDeps: [],
|
|
3870
|
+
schemaVersion
|
|
3871
|
+
},
|
|
3872
|
+
clock
|
|
3873
|
+
);
|
|
3874
|
+
}
|
|
3875
|
+
var RouteMutationError = class extends Error {
|
|
3876
|
+
constructor(code, message) {
|
|
3877
|
+
super(message);
|
|
3878
|
+
this.code = code;
|
|
3879
|
+
this.name = "RouteMutationError";
|
|
3880
|
+
}
|
|
3881
|
+
code;
|
|
3882
|
+
};
|
|
3883
|
+
function createRouteContext(server, store) {
|
|
3884
|
+
let mutationTail = Promise.resolve();
|
|
3885
|
+
function serialize(work) {
|
|
3886
|
+
const run = mutationTail.then(work, work);
|
|
3887
|
+
mutationTail = run.then(
|
|
3888
|
+
() => void 0,
|
|
3889
|
+
() => void 0
|
|
3890
|
+
);
|
|
3891
|
+
return run;
|
|
3892
|
+
}
|
|
3893
|
+
return {
|
|
3894
|
+
apply(mutation, options) {
|
|
3895
|
+
return serialize(async () => {
|
|
3896
|
+
let op;
|
|
3897
|
+
try {
|
|
3898
|
+
op = await buildRouteOperation(store, mutation);
|
|
3899
|
+
} catch (error) {
|
|
3900
|
+
if (error instanceof RouteMutationError) {
|
|
3901
|
+
return { ok: false, code: error.code, message: error.message };
|
|
3902
|
+
}
|
|
3903
|
+
throw error;
|
|
3904
|
+
}
|
|
3905
|
+
if (options?.scope && !operationMatchesScopes(op, options.scope)) {
|
|
3906
|
+
return {
|
|
3907
|
+
ok: false,
|
|
3908
|
+
code: "SCOPE_VIOLATION",
|
|
3909
|
+
message: `Mutation on "${mutation.collection}" is outside the provided scope.`
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
const result = await server.applyLocalOperation(op);
|
|
3913
|
+
if (result.result !== "applied") {
|
|
3914
|
+
return {
|
|
3915
|
+
ok: false,
|
|
3916
|
+
code: result.rejection?.code ?? "NOT_APPLIED",
|
|
3917
|
+
message: result.rejection?.message ?? `Operation on "${mutation.collection}" was not applied (${result.result}).`
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
const record = await store.findRecord(mutation.collection, op.recordId);
|
|
3921
|
+
return { ok: true, operation: op, record };
|
|
3922
|
+
});
|
|
3923
|
+
},
|
|
3924
|
+
async query(collection, options) {
|
|
3925
|
+
const { scope, ...queryOptions } = options ?? {};
|
|
3926
|
+
const records = await store.queryCollection(collection, queryOptions);
|
|
3927
|
+
if (!scope) {
|
|
3928
|
+
return records;
|
|
3929
|
+
}
|
|
3930
|
+
return records.filter((record) => recordMatchesScope(collection, record, scope));
|
|
3931
|
+
},
|
|
3932
|
+
async findById(collection, id, options) {
|
|
3933
|
+
const record = await store.findRecord(collection, id);
|
|
3934
|
+
if (!record) {
|
|
3935
|
+
return null;
|
|
3936
|
+
}
|
|
3937
|
+
if (options?.scope && !recordMatchesScope(collection, record, options.scope)) {
|
|
3938
|
+
return null;
|
|
3939
|
+
}
|
|
3940
|
+
return record;
|
|
3941
|
+
}
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
// src/server/production-server.ts
|
|
3499
3946
|
var MIME_TYPES = {
|
|
3500
3947
|
".html": "text/html",
|
|
3501
3948
|
".js": "text/javascript",
|
|
@@ -3519,6 +3966,7 @@ function createProductionServer(config) {
|
|
|
3519
3966
|
enableDashboard: true,
|
|
3520
3967
|
...config.syncOptions
|
|
3521
3968
|
});
|
|
3969
|
+
const routeContext = createRouteContext(syncServer, config.store);
|
|
3522
3970
|
let httpServer = null;
|
|
3523
3971
|
function getOperationalToken(kind) {
|
|
3524
3972
|
const auth = config.operationalAuth;
|
|
@@ -3605,6 +4053,10 @@ function createProductionServer(config) {
|
|
|
3605
4053
|
const chunks = [];
|
|
3606
4054
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
3607
4055
|
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
4056
|
+
if (!req.readableFlowing) {
|
|
4057
|
+
req.resume();
|
|
4058
|
+
}
|
|
4059
|
+
req.on("error", () => resolve(Buffer.concat(chunks)));
|
|
3608
4060
|
});
|
|
3609
4061
|
}
|
|
3610
4062
|
async function readJsonBody(req) {
|
|
@@ -3661,6 +4113,19 @@ function createProductionServer(config) {
|
|
|
3661
4113
|
const { WebSocketServer } = await import("ws");
|
|
3662
4114
|
const distDir = resolve(staticDir);
|
|
3663
4115
|
httpServer = createServer(async (req, res) => {
|
|
4116
|
+
try {
|
|
4117
|
+
await handleRequest(req, res);
|
|
4118
|
+
} catch (error) {
|
|
4119
|
+
console.error("[kora] Unhandled error in HTTP request handler:", error);
|
|
4120
|
+
if (!res.headersSent) {
|
|
4121
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4122
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
4123
|
+
} else if (!res.writableEnded) {
|
|
4124
|
+
res.end();
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
});
|
|
4128
|
+
async function handleRequest(req, res) {
|
|
3664
4129
|
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
3665
4130
|
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
3666
4131
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
@@ -3788,7 +4253,8 @@ data: ${JSON.stringify(s)}
|
|
|
3788
4253
|
body: await readJsonBody(req),
|
|
3789
4254
|
headers: req.headers,
|
|
3790
4255
|
query: getQuery(url),
|
|
3791
|
-
ip: getClientIp(req)
|
|
4256
|
+
ip: getClientIp(req),
|
|
4257
|
+
kora: routeContext
|
|
3792
4258
|
});
|
|
3793
4259
|
writeJsonResponse(res, result);
|
|
3794
4260
|
return;
|
|
@@ -3822,7 +4288,7 @@ data: ${JSON.stringify(s)}
|
|
|
3822
4288
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
3823
4289
|
res.writeHead(200, { "Content-Type": contentType });
|
|
3824
4290
|
createReadStream(filePath).pipe(res);
|
|
3825
|
-
}
|
|
4291
|
+
}
|
|
3826
4292
|
const wss = new WebSocketServer({ noServer: true });
|
|
3827
4293
|
httpServer.on("upgrade", (req, socket, head) => {
|
|
3828
4294
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|