@korajs/server 1.0.0-beta.7 → 1.0.0-beta.9
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/dist/index.cjs +251 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -2
- package/dist/index.d.ts +24 -2
- package/dist/index.js +221 -23
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -2397,6 +2397,7 @@ var WsServerTransport = class {
|
|
|
2397
2397
|
// src/session/client-session.ts
|
|
2398
2398
|
init_cjs_shims();
|
|
2399
2399
|
var import_core10 = require("@korajs/core");
|
|
2400
|
+
var import_core11 = require("@korajs/core");
|
|
2400
2401
|
var import_internal = require("@korajs/core/internal");
|
|
2401
2402
|
var import_sync2 = require("@korajs/sync");
|
|
2402
2403
|
var import_sync3 = require("@korajs/sync");
|
|
@@ -2522,6 +2523,18 @@ function nextServerSequenceNumber(store) {
|
|
|
2522
2523
|
async function applyServerOperation(store, op, relationLookup) {
|
|
2523
2524
|
const schema = store.getSchema();
|
|
2524
2525
|
const lookup = relationLookup ?? (schema ? (0, import_merge2.buildMergeRelationLookup)(schema) : /* @__PURE__ */ new Map());
|
|
2526
|
+
const shapeCheck = validateOperationShape(op, schema);
|
|
2527
|
+
if (!shapeCheck.valid) {
|
|
2528
|
+
return {
|
|
2529
|
+
result: "skipped",
|
|
2530
|
+
appliedOperations: [],
|
|
2531
|
+
rejection: {
|
|
2532
|
+
code: shapeCheck.code ?? "SCHEMA_VALIDATION_ERROR",
|
|
2533
|
+
message: shapeCheck.message ?? `Operation "${op.id}" does not match the server schema`,
|
|
2534
|
+
retriable: false
|
|
2535
|
+
}
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2525
2538
|
const constraintCheck = await validateIncomingOperationConstraints(store, op, schema);
|
|
2526
2539
|
if (!constraintCheck.valid) {
|
|
2527
2540
|
const code = constraintCheck.code ?? "CONSTRAINT_VIOLATION";
|
|
@@ -2575,6 +2588,67 @@ async function applyServerOperation(store, op, relationLookup) {
|
|
|
2575
2588
|
appliedOperations: result === "applied" ? [op] : []
|
|
2576
2589
|
};
|
|
2577
2590
|
}
|
|
2591
|
+
function validateOperationShape(op, schema) {
|
|
2592
|
+
if (!schema) {
|
|
2593
|
+
return { valid: true };
|
|
2594
|
+
}
|
|
2595
|
+
const collection = schema.collections[op.collection];
|
|
2596
|
+
if (!collection) {
|
|
2597
|
+
return {
|
|
2598
|
+
valid: false,
|
|
2599
|
+
code: "UNKNOWN_COLLECTION",
|
|
2600
|
+
message: `Operation "${op.id}" targets unknown collection "${op.collection}".`
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
const declaredFields = new Set(Object.keys(collection.fields));
|
|
2604
|
+
const systemPreviousFields = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_deleted"]);
|
|
2605
|
+
const invalidDataField = firstInvalidField(op.data, declaredFields);
|
|
2606
|
+
if (invalidDataField) {
|
|
2607
|
+
return {
|
|
2608
|
+
valid: false,
|
|
2609
|
+
code: "SCHEMA_VALIDATION_ERROR",
|
|
2610
|
+
message: `Operation "${op.id}" contains undeclared field "${invalidDataField}" in data for collection "${op.collection}".`
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
const invalidPreviousField = firstInvalidField(
|
|
2614
|
+
op.previousData,
|
|
2615
|
+
declaredFields,
|
|
2616
|
+
systemPreviousFields
|
|
2617
|
+
);
|
|
2618
|
+
if (invalidPreviousField) {
|
|
2619
|
+
return {
|
|
2620
|
+
valid: false,
|
|
2621
|
+
code: "SCHEMA_VALIDATION_ERROR",
|
|
2622
|
+
message: `Operation "${op.id}" contains undeclared field "${invalidPreviousField}" in previousData for collection "${op.collection}".`
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
if (op.atomicOps) {
|
|
2626
|
+
for (const field of Object.keys(op.atomicOps)) {
|
|
2627
|
+
if (!declaredFields.has(field)) {
|
|
2628
|
+
return {
|
|
2629
|
+
valid: false,
|
|
2630
|
+
code: "SCHEMA_VALIDATION_ERROR",
|
|
2631
|
+
message: `Operation "${op.id}" contains undeclared atomic field "${field}" for collection "${op.collection}".`
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
return { valid: true };
|
|
2637
|
+
}
|
|
2638
|
+
function firstInvalidField(value, declaredFields, extraAllowedFields) {
|
|
2639
|
+
if (value === null || value === void 0) {
|
|
2640
|
+
return null;
|
|
2641
|
+
}
|
|
2642
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
2643
|
+
return null;
|
|
2644
|
+
}
|
|
2645
|
+
for (const field of Object.keys(value)) {
|
|
2646
|
+
if (!declaredFields.has(field) && !extraAllowedFields?.has(field)) {
|
|
2647
|
+
return field;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return null;
|
|
2651
|
+
}
|
|
2578
2652
|
|
|
2579
2653
|
// src/auth/no-auth.ts
|
|
2580
2654
|
init_cjs_shims();
|
|
@@ -2626,6 +2700,7 @@ function operationMatchesScopes(op, scopes, fullRecord) {
|
|
|
2626
2700
|
if (!scopes) return true;
|
|
2627
2701
|
const collectionScope = scopes[op.collection];
|
|
2628
2702
|
if (!collectionScope) return false;
|
|
2703
|
+
if (Object.keys(collectionScope).length === 0) return true;
|
|
2629
2704
|
const snapshot = buildSnapshot(op, fullRecord);
|
|
2630
2705
|
if (!snapshot) return false;
|
|
2631
2706
|
for (const [field, expected] of Object.entries(collectionScope)) {
|
|
@@ -2639,6 +2714,7 @@ function missingScopeFields(op, scopes) {
|
|
|
2639
2714
|
if (!scopes) return [];
|
|
2640
2715
|
const collectionScope = scopes[op.collection];
|
|
2641
2716
|
if (!collectionScope) return [];
|
|
2717
|
+
if (Object.keys(collectionScope).length === 0) return [];
|
|
2642
2718
|
const snapshot = buildSnapshot(op);
|
|
2643
2719
|
return Object.keys(collectionScope).filter((field) => !snapshot || !(field in snapshot));
|
|
2644
2720
|
}
|
|
@@ -2777,6 +2853,13 @@ var ClientSession = class _ClientSession {
|
|
|
2777
2853
|
* client's reported watermark at handshake.
|
|
2778
2854
|
*/
|
|
2779
2855
|
lastAckedDeliverySeq = 0;
|
|
2856
|
+
/**
|
|
2857
|
+
* Timestamp of the last delivery-stream send attempt. Periodic retransmission uses
|
|
2858
|
+
* this as its stale window so a slow client does not receive duplicate full-stream
|
|
2859
|
+
* batches every server tick while the first batch is still being applied.
|
|
2860
|
+
*/
|
|
2861
|
+
lastDeliveryPushAttemptAtMs = 0;
|
|
2862
|
+
deliveryStallRepeatCount = 0;
|
|
2780
2863
|
/** Serializes incremental delivery pushes so their batches never interleave. */
|
|
2781
2864
|
deliveryPushChain = Promise.resolve();
|
|
2782
2865
|
/**
|
|
@@ -2799,6 +2882,7 @@ var ClientSession = class _ClientSession {
|
|
|
2799
2882
|
batchSize;
|
|
2800
2883
|
schemaVersion;
|
|
2801
2884
|
supportedSchemaVersions;
|
|
2885
|
+
operationTransforms;
|
|
2802
2886
|
onRelay;
|
|
2803
2887
|
onAwarenessUpdate;
|
|
2804
2888
|
onYjsDocUpdate;
|
|
@@ -2828,6 +2912,7 @@ var ClientSession = class _ClientSession {
|
|
|
2828
2912
|
min: this.schemaVersion,
|
|
2829
2913
|
max: this.schemaVersion
|
|
2830
2914
|
};
|
|
2915
|
+
this.operationTransforms = options.operationTransforms ?? [];
|
|
2831
2916
|
this.onRelay = options.onRelay ?? null;
|
|
2832
2917
|
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
2833
2918
|
this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
|
|
@@ -2877,6 +2962,7 @@ var ClientSession = class _ClientSession {
|
|
|
2877
2962
|
this.deliveryPushChain = this.deliveryPushChain.then(async () => {
|
|
2878
2963
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
2879
2964
|
try {
|
|
2965
|
+
this.lastDeliveryPushAttemptAtMs = Date.now();
|
|
2880
2966
|
await this.sendDeliveryStream(
|
|
2881
2967
|
this.lastAckedDeliverySeq,
|
|
2882
2968
|
false,
|
|
@@ -2898,7 +2984,7 @@ var ClientSession = class _ClientSession {
|
|
|
2898
2984
|
const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
|
|
2899
2985
|
const msg = {
|
|
2900
2986
|
type: "operation-batch",
|
|
2901
|
-
messageId: (0,
|
|
2987
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
2902
2988
|
operations: serializedOps,
|
|
2903
2989
|
isFinal: true,
|
|
2904
2990
|
batchIndex: 0
|
|
@@ -2924,6 +3010,18 @@ var ClientSession = class _ClientSession {
|
|
|
2924
3010
|
retransmitPendingRelays(staleMs = 0) {
|
|
2925
3011
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
2926
3012
|
if (this.clientDeliveryWatermark !== null) {
|
|
3013
|
+
if (staleMs > 0 && Date.now() - this.lastDeliveryPushAttemptAtMs < staleMs) {
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
this.deliveryStallRepeatCount += 1;
|
|
3017
|
+
if (this.deliveryStallRepeatCount >= 3) {
|
|
3018
|
+
this.emitter?.emit({
|
|
3019
|
+
type: "sync:delivery-stalled",
|
|
3020
|
+
sessionId: this.sessionId,
|
|
3021
|
+
watermark: this.lastAckedDeliverySeq,
|
|
3022
|
+
repeatCount: this.deliveryStallRepeatCount
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
2927
3025
|
this.pushDeliveryStream();
|
|
2928
3026
|
return;
|
|
2929
3027
|
}
|
|
@@ -3016,6 +3114,7 @@ var ClientSession = class _ClientSession {
|
|
|
3016
3114
|
this.pendingRelays.delete(message.acknowledgedMessageId);
|
|
3017
3115
|
if (message.deliverySequence !== void 0) {
|
|
3018
3116
|
this.lastAckedDeliverySeq = Math.max(this.lastAckedDeliverySeq, message.deliverySequence);
|
|
3117
|
+
this.deliveryStallRepeatCount = 0;
|
|
3019
3118
|
}
|
|
3020
3119
|
break;
|
|
3021
3120
|
case "error":
|
|
@@ -3047,7 +3146,7 @@ var ClientSession = class _ClientSession {
|
|
|
3047
3146
|
return;
|
|
3048
3147
|
}
|
|
3049
3148
|
const bytes = (0, import_sync2.decodeBlobChunkBytes)(message.bytes);
|
|
3050
|
-
const actual = await (0,
|
|
3149
|
+
const actual = await (0, import_core11.hashBlob)(bytes);
|
|
3051
3150
|
if (actual !== message.hash) {
|
|
3052
3151
|
return;
|
|
3053
3152
|
}
|
|
@@ -3122,9 +3221,17 @@ var ClientSession = class _ClientSession {
|
|
|
3122
3221
|
this.setSerializerWireFormat(selectedWireFormat);
|
|
3123
3222
|
if (!(0, import_sync3.isClientSchemaVersionSupported)(msg.schemaVersion, this.supportedSchemaVersions)) {
|
|
3124
3223
|
const { min, max } = this.supportedSchemaVersions;
|
|
3224
|
+
this.emitter?.emit({
|
|
3225
|
+
type: "sync:schema-mismatch",
|
|
3226
|
+
clientSchemaVersion: msg.schemaVersion,
|
|
3227
|
+
serverSchemaVersion: this.schemaVersion,
|
|
3228
|
+
supportedMin: min,
|
|
3229
|
+
supportedMax: max,
|
|
3230
|
+
reason: `${import_sync3.SCHEMA_MISMATCH_PREFIX}: client schema version ${msg.schemaVersion} not in supported range [${min}, ${max}]`
|
|
3231
|
+
});
|
|
3125
3232
|
const response2 = {
|
|
3126
3233
|
type: "handshake-response",
|
|
3127
|
-
messageId: (0,
|
|
3234
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3128
3235
|
nodeId: this.store.getNodeId(),
|
|
3129
3236
|
versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
|
|
3130
3237
|
schemaVersion: this.schemaVersion,
|
|
@@ -3140,7 +3247,7 @@ var ClientSession = class _ClientSession {
|
|
|
3140
3247
|
}
|
|
3141
3248
|
const response = {
|
|
3142
3249
|
type: "handshake-response",
|
|
3143
|
-
messageId: (0,
|
|
3250
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3144
3251
|
nodeId: this.store.getNodeId(),
|
|
3145
3252
|
versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
|
|
3146
3253
|
schemaVersion: this.schemaVersion,
|
|
@@ -3164,6 +3271,7 @@ var ClientSession = class _ClientSession {
|
|
|
3164
3271
|
if (this.clientDeliveryWatermark !== null) {
|
|
3165
3272
|
this.lastAckedDeliverySeq = this.clientDeliveryWatermark;
|
|
3166
3273
|
const excludeOwn = this.clientDeliveryWatermark > 0 ? this.clientNodeId ?? void 0 : void 0;
|
|
3274
|
+
this.lastDeliveryPushAttemptAtMs = Date.now();
|
|
3167
3275
|
await this.sendDeliveryStream(this.clientDeliveryWatermark, true, excludeOwn);
|
|
3168
3276
|
} else {
|
|
3169
3277
|
const clientVector = (0, import_sync3.wireToVersionVector)(msg.versionVector);
|
|
@@ -3180,10 +3288,20 @@ var ClientSession = class _ClientSession {
|
|
|
3180
3288
|
async handleOperationBatch(msg) {
|
|
3181
3289
|
const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
3182
3290
|
const applied = [];
|
|
3183
|
-
|
|
3291
|
+
let acknowledgedThrough = 0;
|
|
3292
|
+
let canAdvanceAck = true;
|
|
3184
3293
|
for (const op of operations2) {
|
|
3294
|
+
if (!canAdvanceAck) {
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
3185
3297
|
if (!await this.operationVisibleToClient(op)) {
|
|
3186
|
-
|
|
3298
|
+
this.sendOperationRejected(
|
|
3299
|
+
op,
|
|
3300
|
+
"SCOPE_VIOLATION",
|
|
3301
|
+
`Operation "${op.id}" in collection "${op.collection}" is outside the client's sync scope. Refresh sync scopes and retry.`,
|
|
3302
|
+
true
|
|
3303
|
+
);
|
|
3304
|
+
canAdvanceAck = false;
|
|
3187
3305
|
continue;
|
|
3188
3306
|
}
|
|
3189
3307
|
if (!isOperationTimestampValid(op)) {
|
|
@@ -3192,6 +3310,7 @@ var ClientSession = class _ClientSession {
|
|
|
3192
3310
|
`Operation "${op.id}" timestamp is too far in the future`,
|
|
3193
3311
|
false
|
|
3194
3312
|
);
|
|
3313
|
+
canAdvanceAck = false;
|
|
3195
3314
|
continue;
|
|
3196
3315
|
}
|
|
3197
3316
|
if (!this.rateLimiter.allow(1)) {
|
|
@@ -3200,6 +3319,7 @@ var ClientSession = class _ClientSession {
|
|
|
3200
3319
|
`Session exceeded operation rate limit (${String(this.maxOpsPerMinute)} ops/min)`,
|
|
3201
3320
|
true
|
|
3202
3321
|
);
|
|
3322
|
+
canAdvanceAck = false;
|
|
3203
3323
|
continue;
|
|
3204
3324
|
}
|
|
3205
3325
|
const sizeCheck = validateOperationSize(op, this.maxOperationBytes);
|
|
@@ -3209,53 +3329,73 @@ var ClientSession = class _ClientSession {
|
|
|
3209
3329
|
sizeCheck.message ?? `Operation "${op.id}" is too large`,
|
|
3210
3330
|
false
|
|
3211
3331
|
);
|
|
3332
|
+
canAdvanceAck = false;
|
|
3333
|
+
continue;
|
|
3334
|
+
}
|
|
3335
|
+
const serverOp = this.transformForServerSchema(op);
|
|
3336
|
+
if (serverOp === null) {
|
|
3337
|
+
this.sendOperationRejected(
|
|
3338
|
+
op,
|
|
3339
|
+
"SCHEMA_TRANSFORM_UNAVAILABLE",
|
|
3340
|
+
`Operation "${op.id}" cannot be transformed from schema v${op.schemaVersion} to server schema v${this.schemaVersion}.`,
|
|
3341
|
+
false
|
|
3342
|
+
);
|
|
3343
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3212
3344
|
continue;
|
|
3213
3345
|
}
|
|
3214
3346
|
if (this.validateOperation && this.koraContext) {
|
|
3215
3347
|
let decision;
|
|
3216
3348
|
try {
|
|
3217
|
-
decision = await this.validateOperation(
|
|
3349
|
+
decision = await this.validateOperation(serverOp, {
|
|
3218
3350
|
auth: this.authContext,
|
|
3219
3351
|
kora: this.koraContext
|
|
3220
3352
|
});
|
|
3221
3353
|
} catch (error) {
|
|
3222
3354
|
const message = error instanceof Error ? error.message : "validator error";
|
|
3223
|
-
this.sendOperationRejected(
|
|
3355
|
+
this.sendOperationRejected(
|
|
3356
|
+
serverOp,
|
|
3357
|
+
"VALIDATION_ERROR",
|
|
3358
|
+
`Validator threw: ${message}`,
|
|
3359
|
+
true
|
|
3360
|
+
);
|
|
3361
|
+
canAdvanceAck = false;
|
|
3224
3362
|
continue;
|
|
3225
3363
|
}
|
|
3226
3364
|
if (decision.action === "reject") {
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3365
|
+
const retriable = decision.retriable ?? isRetriableRejection(decision.code);
|
|
3366
|
+
this.sendOperationRejected(serverOp, decision.code, decision.message, retriable);
|
|
3367
|
+
if (retriable) {
|
|
3368
|
+
canAdvanceAck = false;
|
|
3369
|
+
} else {
|
|
3370
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3371
|
+
}
|
|
3233
3372
|
continue;
|
|
3234
3373
|
}
|
|
3235
3374
|
if (decision.action === "ignore") {
|
|
3375
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3236
3376
|
continue;
|
|
3237
3377
|
}
|
|
3238
3378
|
}
|
|
3239
|
-
const applyResult = await applyServerOperation(this.store,
|
|
3379
|
+
const applyResult = await applyServerOperation(this.store, serverOp);
|
|
3240
3380
|
if (applyResult.rejection) {
|
|
3241
|
-
this.
|
|
3381
|
+
this.sendOperationRejected(
|
|
3382
|
+
serverOp,
|
|
3242
3383
|
applyResult.rejection.code,
|
|
3243
3384
|
applyResult.rejection.message,
|
|
3244
3385
|
applyResult.rejection.retriable
|
|
3245
3386
|
);
|
|
3387
|
+
if (applyResult.rejection.retriable) {
|
|
3388
|
+
canAdvanceAck = false;
|
|
3389
|
+
} else {
|
|
3390
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3391
|
+
}
|
|
3246
3392
|
continue;
|
|
3247
3393
|
}
|
|
3248
3394
|
if (applyResult.result === "applied") {
|
|
3249
3395
|
applied.push(...applyResult.appliedOperations);
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
for (const op of rejected) {
|
|
3254
|
-
this.sendError(
|
|
3255
|
-
"SCOPE_VIOLATION",
|
|
3256
|
-
`Operation "${op.id}" in collection "${op.collection}" is outside the client's sync scope`,
|
|
3257
|
-
false
|
|
3258
|
-
);
|
|
3396
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3397
|
+
} else {
|
|
3398
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3259
3399
|
}
|
|
3260
3400
|
}
|
|
3261
3401
|
if (operations2.length > 0) {
|
|
@@ -3268,15 +3408,21 @@ var ClientSession = class _ClientSession {
|
|
|
3268
3408
|
const lastOp = operations2[operations2.length - 1];
|
|
3269
3409
|
const ack = {
|
|
3270
3410
|
type: "acknowledgment",
|
|
3271
|
-
messageId: (0,
|
|
3411
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3272
3412
|
acknowledgedMessageId: msg.messageId,
|
|
3273
|
-
lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
|
|
3413
|
+
lastSequenceNumber: lastOp ? Math.min(acknowledgedThrough, lastOp.sequenceNumber) : 0
|
|
3274
3414
|
};
|
|
3275
3415
|
this.sendToClient(ack);
|
|
3276
3416
|
if (applied.length > 0) {
|
|
3277
3417
|
this.onRelay?.(this.sessionId, applied);
|
|
3278
3418
|
}
|
|
3279
3419
|
}
|
|
3420
|
+
transformForServerSchema(op) {
|
|
3421
|
+
if (op.schemaVersion === this.schemaVersion) {
|
|
3422
|
+
return op;
|
|
3423
|
+
}
|
|
3424
|
+
return (0, import_core10.applyOperationTransforms)(op, this.schemaVersion, this.operationTransforms);
|
|
3425
|
+
}
|
|
3280
3426
|
async sendDelta(clientVector) {
|
|
3281
3427
|
const serverVector = this.store.getVersionVector();
|
|
3282
3428
|
const missing = [];
|
|
@@ -3294,7 +3440,7 @@ var ClientSession = class _ClientSession {
|
|
|
3294
3440
|
if (missing.length === 0) {
|
|
3295
3441
|
const emptyBatch = {
|
|
3296
3442
|
type: "operation-batch",
|
|
3297
|
-
messageId: (0,
|
|
3443
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3298
3444
|
operations: [],
|
|
3299
3445
|
isFinal: true,
|
|
3300
3446
|
batchIndex: 0,
|
|
@@ -3309,7 +3455,7 @@ var ClientSession = class _ClientSession {
|
|
|
3309
3455
|
if (afterCursor.length === 0) {
|
|
3310
3456
|
const emptyBatch = {
|
|
3311
3457
|
type: "operation-batch",
|
|
3312
|
-
messageId: (0,
|
|
3458
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3313
3459
|
operations: [],
|
|
3314
3460
|
isFinal: true,
|
|
3315
3461
|
batchIndex: this.resumeDeltaCursor?.batchIndex ?? 0,
|
|
@@ -3325,7 +3471,7 @@ var ClientSession = class _ClientSession {
|
|
|
3325
3471
|
const batchCursor = (0, import_sync3.createDeltaCursorFromBatch)(batchOps, i);
|
|
3326
3472
|
const batchMsg = {
|
|
3327
3473
|
type: "operation-batch",
|
|
3328
|
-
messageId: (0,
|
|
3474
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3329
3475
|
operations: serializedOps,
|
|
3330
3476
|
isFinal: i === totalBatches - 1,
|
|
3331
3477
|
batchIndex: i,
|
|
@@ -3402,7 +3548,7 @@ var ClientSession = class _ClientSession {
|
|
|
3402
3548
|
sendDeliveryBatch(slice, base, max, batchIndex, isFinal) {
|
|
3403
3549
|
const batchMsg = {
|
|
3404
3550
|
type: "operation-batch",
|
|
3405
|
-
messageId: (0,
|
|
3551
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3406
3552
|
operations: slice.map((delivered) => this.serializer.encodeOperation(delivered.operation)),
|
|
3407
3553
|
isFinal,
|
|
3408
3554
|
batchIndex,
|
|
@@ -3455,7 +3601,7 @@ var ClientSession = class _ClientSession {
|
|
|
3455
3601
|
sendError(code, message, retriable) {
|
|
3456
3602
|
const errorMsg = {
|
|
3457
3603
|
type: "error",
|
|
3458
|
-
messageId: (0,
|
|
3604
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3459
3605
|
code,
|
|
3460
3606
|
message,
|
|
3461
3607
|
retriable
|
|
@@ -3471,7 +3617,7 @@ var ClientSession = class _ClientSession {
|
|
|
3471
3617
|
sendOperationRejected(op, code, message, retriable) {
|
|
3472
3618
|
const rejectedMsg = {
|
|
3473
3619
|
type: "operation-rejected",
|
|
3474
|
-
messageId: (0,
|
|
3620
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3475
3621
|
operationId: op.id,
|
|
3476
3622
|
collection: op.collection,
|
|
3477
3623
|
recordId: op.recordId,
|
|
@@ -3503,13 +3649,13 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
3503
3649
|
|
|
3504
3650
|
// src/server/kora-sync-server.ts
|
|
3505
3651
|
init_cjs_shims();
|
|
3506
|
-
var
|
|
3652
|
+
var import_core14 = require("@korajs/core");
|
|
3507
3653
|
var import_internal2 = require("@korajs/core/internal");
|
|
3508
3654
|
var import_sync5 = require("@korajs/sync");
|
|
3509
3655
|
|
|
3510
3656
|
// src/awareness/awareness-relay.ts
|
|
3511
3657
|
init_cjs_shims();
|
|
3512
|
-
var
|
|
3658
|
+
var import_core12 = require("@korajs/core");
|
|
3513
3659
|
var AwarenessRelay = class {
|
|
3514
3660
|
clients = /* @__PURE__ */ new Map();
|
|
3515
3661
|
/**
|
|
@@ -3538,7 +3684,7 @@ var AwarenessRelay = class {
|
|
|
3538
3684
|
if (hasStates) {
|
|
3539
3685
|
const catchUpMsg = {
|
|
3540
3686
|
type: "awareness-update",
|
|
3541
|
-
messageId: (0,
|
|
3687
|
+
messageId: (0, import_core12.generateUUIDv7)(),
|
|
3542
3688
|
clientId: 0,
|
|
3543
3689
|
// Server-sourced
|
|
3544
3690
|
states: existingStates
|
|
@@ -3561,7 +3707,7 @@ var AwarenessRelay = class {
|
|
|
3561
3707
|
};
|
|
3562
3708
|
const msg = {
|
|
3563
3709
|
type: "awareness-update",
|
|
3564
|
-
messageId: (0,
|
|
3710
|
+
messageId: (0, import_core12.generateUUIDv7)(),
|
|
3565
3711
|
clientId: client.clientId,
|
|
3566
3712
|
states: removalStates
|
|
3567
3713
|
};
|
|
@@ -3867,7 +4013,7 @@ var YjsDocRelay = class {
|
|
|
3867
4013
|
|
|
3868
4014
|
// src/server/route-context.ts
|
|
3869
4015
|
init_cjs_shims();
|
|
3870
|
-
var
|
|
4016
|
+
var import_core13 = require("@korajs/core");
|
|
3871
4017
|
|
|
3872
4018
|
// src/apply/route-predicate.ts
|
|
3873
4019
|
init_cjs_shims();
|
|
@@ -3942,16 +4088,16 @@ function pickFields(record, keys) {
|
|
|
3942
4088
|
}
|
|
3943
4089
|
async function buildRouteOperation(store, mutation, currentOverride, clockOverride) {
|
|
3944
4090
|
const nodeId = store.getNodeId();
|
|
3945
|
-
const clock = clockOverride ?? new
|
|
4091
|
+
const clock = clockOverride ?? new import_core13.HybridLogicalClock(nodeId);
|
|
3946
4092
|
const schemaVersion = store.getSchema()?.version ?? 1;
|
|
3947
4093
|
const sequenceNumber = nextServerSequenceNumber(store);
|
|
3948
4094
|
if (mutation.type === "insert") {
|
|
3949
|
-
return (0,
|
|
4095
|
+
return (0, import_core13.createOperation)(
|
|
3950
4096
|
{
|
|
3951
4097
|
nodeId,
|
|
3952
4098
|
type: "insert",
|
|
3953
4099
|
collection: mutation.collection,
|
|
3954
|
-
recordId: mutation.recordId ?? (0,
|
|
4100
|
+
recordId: mutation.recordId ?? (0, import_core13.generateUUIDv7)(),
|
|
3955
4101
|
data: mutation.data ?? {},
|
|
3956
4102
|
previousData: null,
|
|
3957
4103
|
sequenceNumber,
|
|
@@ -3974,14 +4120,14 @@ async function buildRouteOperation(store, mutation, currentOverride, clockOverri
|
|
|
3974
4120
|
const atomicOps = {};
|
|
3975
4121
|
for (const key of Object.keys(rawData)) {
|
|
3976
4122
|
const value = rawData[key];
|
|
3977
|
-
if ((0,
|
|
3978
|
-
data[key] = (0,
|
|
3979
|
-
atomicOps[key] = (0,
|
|
4123
|
+
if ((0, import_core13.isAtomicOp)(value)) {
|
|
4124
|
+
data[key] = (0, import_core13.resolveAtomicOp)(current ? current[key] : void 0, value);
|
|
4125
|
+
atomicOps[key] = (0, import_core13.toAtomicOp)(value);
|
|
3980
4126
|
} else {
|
|
3981
4127
|
data[key] = value;
|
|
3982
4128
|
}
|
|
3983
4129
|
}
|
|
3984
|
-
return (0,
|
|
4130
|
+
return (0, import_core13.createOperation)(
|
|
3985
4131
|
{
|
|
3986
4132
|
nodeId,
|
|
3987
4133
|
type: "update",
|
|
@@ -3997,7 +4143,7 @@ async function buildRouteOperation(store, mutation, currentOverride, clockOverri
|
|
|
3997
4143
|
clock
|
|
3998
4144
|
);
|
|
3999
4145
|
}
|
|
4000
|
-
return (0,
|
|
4146
|
+
return (0, import_core13.createOperation)(
|
|
4001
4147
|
{
|
|
4002
4148
|
nodeId,
|
|
4003
4149
|
type: "delete",
|
|
@@ -4263,6 +4409,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4263
4409
|
batchSize;
|
|
4264
4410
|
schemaVersion;
|
|
4265
4411
|
supportedSchemaVersions;
|
|
4412
|
+
operationTransforms;
|
|
4266
4413
|
port;
|
|
4267
4414
|
host;
|
|
4268
4415
|
path;
|
|
@@ -4300,16 +4447,24 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4300
4447
|
static ORPHANED_RELAY_TTL_MS = 5 * 6e4;
|
|
4301
4448
|
constructor(config) {
|
|
4302
4449
|
this.store = config.store;
|
|
4450
|
+
const storeSchemaVersion = this.store.getSchema()?.version;
|
|
4451
|
+
if (config.schemaVersion !== void 0 && storeSchemaVersion !== void 0 && config.schemaVersion !== storeSchemaVersion) {
|
|
4452
|
+
throw new import_core14.SyncError("Sync schema version does not match the configured store schema.", {
|
|
4453
|
+
syncSchemaVersion: config.schemaVersion,
|
|
4454
|
+
storeSchemaVersion
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4303
4457
|
this.auth = config.auth ?? null;
|
|
4304
4458
|
this.serializer = config.serializer ?? new import_sync5.JsonMessageSerializer();
|
|
4305
4459
|
this.emitter = config.emitter ?? null;
|
|
4306
4460
|
this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
|
|
4307
4461
|
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
|
|
4308
|
-
this.schemaVersion = config.schemaVersion ?? DEFAULT_SCHEMA_VERSION2;
|
|
4462
|
+
this.schemaVersion = config.schemaVersion ?? storeSchemaVersion ?? DEFAULT_SCHEMA_VERSION2;
|
|
4309
4463
|
this.supportedSchemaVersions = config.supportedSchemaVersions ?? {
|
|
4310
4464
|
min: this.schemaVersion,
|
|
4311
4465
|
max: this.schemaVersion
|
|
4312
4466
|
};
|
|
4467
|
+
this.operationTransforms = config.operationTransforms ?? [];
|
|
4313
4468
|
this.port = config.port;
|
|
4314
4469
|
this.host = config.host ?? DEFAULT_HOST;
|
|
4315
4470
|
this.path = config.path ?? DEFAULT_PATH;
|
|
@@ -4433,6 +4588,21 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4433
4588
|
details: { reason: event.reason }
|
|
4434
4589
|
});
|
|
4435
4590
|
});
|
|
4591
|
+
sessionEmitter.on("sync:schema-mismatch", (event) => {
|
|
4592
|
+
this.logger.log({
|
|
4593
|
+
timestamp: Date.now(),
|
|
4594
|
+
level: "warn",
|
|
4595
|
+
event: "session.schema_mismatch",
|
|
4596
|
+
sessionId,
|
|
4597
|
+
details: {
|
|
4598
|
+
clientSchemaVersion: event.clientSchemaVersion,
|
|
4599
|
+
serverSchemaVersion: event.serverSchemaVersion,
|
|
4600
|
+
supportedMin: event.supportedMin,
|
|
4601
|
+
supportedMax: event.supportedMax,
|
|
4602
|
+
reason: event.reason
|
|
4603
|
+
}
|
|
4604
|
+
});
|
|
4605
|
+
});
|
|
4436
4606
|
}
|
|
4437
4607
|
/**
|
|
4438
4608
|
* Get the metrics collector for external access (e.g., HTTP endpoints).
|
|
@@ -4453,10 +4623,10 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4453
4623
|
*/
|
|
4454
4624
|
async start(wsServerImpl) {
|
|
4455
4625
|
if (this.running) {
|
|
4456
|
-
throw new
|
|
4626
|
+
throw new import_core14.SyncError("Server is already running", { port: this.port });
|
|
4457
4627
|
}
|
|
4458
4628
|
if (!wsServerImpl && this.port === void 0) {
|
|
4459
|
-
throw new
|
|
4629
|
+
throw new import_core14.SyncError(
|
|
4460
4630
|
"Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
|
|
4461
4631
|
{}
|
|
4462
4632
|
);
|
|
@@ -4571,7 +4741,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4571
4741
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
4572
4742
|
transport.send({
|
|
4573
4743
|
type: "error",
|
|
4574
|
-
messageId: (0,
|
|
4744
|
+
messageId: (0, import_core14.generateUUIDv7)(),
|
|
4575
4745
|
code: "MAX_CONNECTIONS",
|
|
4576
4746
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
4577
4747
|
retriable: true
|
|
@@ -4584,12 +4754,12 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4584
4754
|
event: "connection.rejected",
|
|
4585
4755
|
details: { reason: "max_connections", max: this.maxConnections }
|
|
4586
4756
|
});
|
|
4587
|
-
throw new
|
|
4757
|
+
throw new import_core14.SyncError("Maximum connections reached", {
|
|
4588
4758
|
current: this.sessions.size,
|
|
4589
4759
|
max: this.maxConnections
|
|
4590
4760
|
});
|
|
4591
4761
|
}
|
|
4592
|
-
const sessionId = (0,
|
|
4762
|
+
const sessionId = (0, import_core14.generateUUIDv7)();
|
|
4593
4763
|
this.metrics.recordConnection(sessionId);
|
|
4594
4764
|
const sessionEmitter = new import_internal2.SimpleEventEmitter();
|
|
4595
4765
|
sessionEmitter.on("sync:connected", (event) => {
|
|
@@ -4635,6 +4805,33 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4635
4805
|
sessionId
|
|
4636
4806
|
});
|
|
4637
4807
|
});
|
|
4808
|
+
sessionEmitter.on("sync:schema-mismatch", (event) => {
|
|
4809
|
+
this.logger.log({
|
|
4810
|
+
timestamp: Date.now(),
|
|
4811
|
+
level: "warn",
|
|
4812
|
+
event: "session.schema_mismatch",
|
|
4813
|
+
sessionId,
|
|
4814
|
+
details: {
|
|
4815
|
+
clientSchemaVersion: event.clientSchemaVersion,
|
|
4816
|
+
serverSchemaVersion: event.serverSchemaVersion,
|
|
4817
|
+
supportedMin: event.supportedMin,
|
|
4818
|
+
supportedMax: event.supportedMax,
|
|
4819
|
+
reason: event.reason
|
|
4820
|
+
}
|
|
4821
|
+
});
|
|
4822
|
+
});
|
|
4823
|
+
sessionEmitter.on("sync:delivery-stalled", (event) => {
|
|
4824
|
+
this.logger.log({
|
|
4825
|
+
timestamp: Date.now(),
|
|
4826
|
+
level: "warn",
|
|
4827
|
+
event: "session.delivery_stalled",
|
|
4828
|
+
sessionId,
|
|
4829
|
+
details: {
|
|
4830
|
+
watermark: event.watermark,
|
|
4831
|
+
repeatCount: event.repeatCount
|
|
4832
|
+
}
|
|
4833
|
+
});
|
|
4834
|
+
});
|
|
4638
4835
|
const session = new ClientSession({
|
|
4639
4836
|
sessionId,
|
|
4640
4837
|
transport,
|
|
@@ -4646,6 +4843,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4646
4843
|
batchSize: this.batchSize,
|
|
4647
4844
|
schemaVersion: this.schemaVersion,
|
|
4648
4845
|
supportedSchemaVersions: this.supportedSchemaVersions,
|
|
4846
|
+
operationTransforms: this.operationTransforms,
|
|
4649
4847
|
onRelay: (sourceSessionId, operations2) => {
|
|
4650
4848
|
this.handleRelay(sourceSessionId, operations2);
|
|
4651
4849
|
},
|
|
@@ -4773,7 +4971,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4773
4971
|
const records = await this.store.materializeCollection(name);
|
|
4774
4972
|
for (const record of records) {
|
|
4775
4973
|
for (const value of Object.values(record)) {
|
|
4776
|
-
if ((0,
|
|
4974
|
+
if ((0, import_core14.isBlobRef)(value)) {
|
|
4777
4975
|
refs.push(value);
|
|
4778
4976
|
}
|
|
4779
4977
|
}
|