@korajs/server 1.0.0-beta.6 → 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 +268 -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 +238 -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
|
}
|
|
@@ -2740,6 +2816,20 @@ function warnIfMultiTenantWithoutScopes(auth, resolvedScopes, schema) {
|
|
|
2740
2816
|
"[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.)"
|
|
2741
2817
|
);
|
|
2742
2818
|
}
|
|
2819
|
+
function stableStringify(value) {
|
|
2820
|
+
if (Array.isArray(value)) {
|
|
2821
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
2822
|
+
}
|
|
2823
|
+
if (value && typeof value === "object") {
|
|
2824
|
+
const record = value;
|
|
2825
|
+
const entries = Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
|
|
2826
|
+
return `{${entries.join(",")}}`;
|
|
2827
|
+
}
|
|
2828
|
+
return JSON.stringify(value);
|
|
2829
|
+
}
|
|
2830
|
+
function sameScopeMap(a, b) {
|
|
2831
|
+
return stableStringify(a ?? null) === stableStringify(b ?? null);
|
|
2832
|
+
}
|
|
2743
2833
|
var ClientSession = class _ClientSession {
|
|
2744
2834
|
state = "connected";
|
|
2745
2835
|
clientNodeId = null;
|
|
@@ -2763,6 +2853,13 @@ var ClientSession = class _ClientSession {
|
|
|
2763
2853
|
* client's reported watermark at handshake.
|
|
2764
2854
|
*/
|
|
2765
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;
|
|
2766
2863
|
/** Serializes incremental delivery pushes so their batches never interleave. */
|
|
2767
2864
|
deliveryPushChain = Promise.resolve();
|
|
2768
2865
|
/**
|
|
@@ -2785,6 +2882,7 @@ var ClientSession = class _ClientSession {
|
|
|
2785
2882
|
batchSize;
|
|
2786
2883
|
schemaVersion;
|
|
2787
2884
|
supportedSchemaVersions;
|
|
2885
|
+
operationTransforms;
|
|
2788
2886
|
onRelay;
|
|
2789
2887
|
onAwarenessUpdate;
|
|
2790
2888
|
onYjsDocUpdate;
|
|
@@ -2814,6 +2912,7 @@ var ClientSession = class _ClientSession {
|
|
|
2814
2912
|
min: this.schemaVersion,
|
|
2815
2913
|
max: this.schemaVersion
|
|
2816
2914
|
};
|
|
2915
|
+
this.operationTransforms = options.operationTransforms ?? [];
|
|
2817
2916
|
this.onRelay = options.onRelay ?? null;
|
|
2818
2917
|
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
2819
2918
|
this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
|
|
@@ -2863,6 +2962,7 @@ var ClientSession = class _ClientSession {
|
|
|
2863
2962
|
this.deliveryPushChain = this.deliveryPushChain.then(async () => {
|
|
2864
2963
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
2865
2964
|
try {
|
|
2965
|
+
this.lastDeliveryPushAttemptAtMs = Date.now();
|
|
2866
2966
|
await this.sendDeliveryStream(
|
|
2867
2967
|
this.lastAckedDeliverySeq,
|
|
2868
2968
|
false,
|
|
@@ -2884,7 +2984,7 @@ var ClientSession = class _ClientSession {
|
|
|
2884
2984
|
const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
|
|
2885
2985
|
const msg = {
|
|
2886
2986
|
type: "operation-batch",
|
|
2887
|
-
messageId: (0,
|
|
2987
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
2888
2988
|
operations: serializedOps,
|
|
2889
2989
|
isFinal: true,
|
|
2890
2990
|
batchIndex: 0
|
|
@@ -2910,6 +3010,18 @@ var ClientSession = class _ClientSession {
|
|
|
2910
3010
|
retransmitPendingRelays(staleMs = 0) {
|
|
2911
3011
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
2912
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
|
+
}
|
|
2913
3025
|
this.pushDeliveryStream();
|
|
2914
3026
|
return;
|
|
2915
3027
|
}
|
|
@@ -3002,6 +3114,7 @@ var ClientSession = class _ClientSession {
|
|
|
3002
3114
|
this.pendingRelays.delete(message.acknowledgedMessageId);
|
|
3003
3115
|
if (message.deliverySequence !== void 0) {
|
|
3004
3116
|
this.lastAckedDeliverySeq = Math.max(this.lastAckedDeliverySeq, message.deliverySequence);
|
|
3117
|
+
this.deliveryStallRepeatCount = 0;
|
|
3005
3118
|
}
|
|
3006
3119
|
break;
|
|
3007
3120
|
case "error":
|
|
@@ -3033,7 +3146,7 @@ var ClientSession = class _ClientSession {
|
|
|
3033
3146
|
return;
|
|
3034
3147
|
}
|
|
3035
3148
|
const bytes = (0, import_sync2.decodeBlobChunkBytes)(message.bytes);
|
|
3036
|
-
const actual = await (0,
|
|
3149
|
+
const actual = await (0, import_core11.hashBlob)(bytes);
|
|
3037
3150
|
if (actual !== message.hash) {
|
|
3038
3151
|
return;
|
|
3039
3152
|
}
|
|
@@ -3092,6 +3205,9 @@ var ClientSession = class _ClientSession {
|
|
|
3092
3205
|
}
|
|
3093
3206
|
this.resumeDeltaCursor = msg.deltaCursor ? (0, import_sync3.decodeDeltaCursor)(msg.deltaCursor) : null;
|
|
3094
3207
|
this.clientDeliveryWatermark = msg.lastDeliverySequence ?? null;
|
|
3208
|
+
if (this.clientDeliveryWatermark !== null && !sameScopeMap(msg.syncScope, this.authContext?.scopes)) {
|
|
3209
|
+
this.clientDeliveryWatermark = 0;
|
|
3210
|
+
}
|
|
3095
3211
|
const clientUsesWatermark = this.clientDeliveryWatermark !== null;
|
|
3096
3212
|
let serverMaxDelivery = 0;
|
|
3097
3213
|
if (clientUsesWatermark) {
|
|
@@ -3105,9 +3221,17 @@ var ClientSession = class _ClientSession {
|
|
|
3105
3221
|
this.setSerializerWireFormat(selectedWireFormat);
|
|
3106
3222
|
if (!(0, import_sync3.isClientSchemaVersionSupported)(msg.schemaVersion, this.supportedSchemaVersions)) {
|
|
3107
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
|
+
});
|
|
3108
3232
|
const response2 = {
|
|
3109
3233
|
type: "handshake-response",
|
|
3110
|
-
messageId: (0,
|
|
3234
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3111
3235
|
nodeId: this.store.getNodeId(),
|
|
3112
3236
|
versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
|
|
3113
3237
|
schemaVersion: this.schemaVersion,
|
|
@@ -3123,7 +3247,7 @@ var ClientSession = class _ClientSession {
|
|
|
3123
3247
|
}
|
|
3124
3248
|
const response = {
|
|
3125
3249
|
type: "handshake-response",
|
|
3126
|
-
messageId: (0,
|
|
3250
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3127
3251
|
nodeId: this.store.getNodeId(),
|
|
3128
3252
|
versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
|
|
3129
3253
|
schemaVersion: this.schemaVersion,
|
|
@@ -3147,6 +3271,7 @@ var ClientSession = class _ClientSession {
|
|
|
3147
3271
|
if (this.clientDeliveryWatermark !== null) {
|
|
3148
3272
|
this.lastAckedDeliverySeq = this.clientDeliveryWatermark;
|
|
3149
3273
|
const excludeOwn = this.clientDeliveryWatermark > 0 ? this.clientNodeId ?? void 0 : void 0;
|
|
3274
|
+
this.lastDeliveryPushAttemptAtMs = Date.now();
|
|
3150
3275
|
await this.sendDeliveryStream(this.clientDeliveryWatermark, true, excludeOwn);
|
|
3151
3276
|
} else {
|
|
3152
3277
|
const clientVector = (0, import_sync3.wireToVersionVector)(msg.versionVector);
|
|
@@ -3163,10 +3288,20 @@ var ClientSession = class _ClientSession {
|
|
|
3163
3288
|
async handleOperationBatch(msg) {
|
|
3164
3289
|
const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
3165
3290
|
const applied = [];
|
|
3166
|
-
|
|
3291
|
+
let acknowledgedThrough = 0;
|
|
3292
|
+
let canAdvanceAck = true;
|
|
3167
3293
|
for (const op of operations2) {
|
|
3294
|
+
if (!canAdvanceAck) {
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
3168
3297
|
if (!await this.operationVisibleToClient(op)) {
|
|
3169
|
-
|
|
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;
|
|
3170
3305
|
continue;
|
|
3171
3306
|
}
|
|
3172
3307
|
if (!isOperationTimestampValid(op)) {
|
|
@@ -3175,6 +3310,7 @@ var ClientSession = class _ClientSession {
|
|
|
3175
3310
|
`Operation "${op.id}" timestamp is too far in the future`,
|
|
3176
3311
|
false
|
|
3177
3312
|
);
|
|
3313
|
+
canAdvanceAck = false;
|
|
3178
3314
|
continue;
|
|
3179
3315
|
}
|
|
3180
3316
|
if (!this.rateLimiter.allow(1)) {
|
|
@@ -3183,6 +3319,7 @@ var ClientSession = class _ClientSession {
|
|
|
3183
3319
|
`Session exceeded operation rate limit (${String(this.maxOpsPerMinute)} ops/min)`,
|
|
3184
3320
|
true
|
|
3185
3321
|
);
|
|
3322
|
+
canAdvanceAck = false;
|
|
3186
3323
|
continue;
|
|
3187
3324
|
}
|
|
3188
3325
|
const sizeCheck = validateOperationSize(op, this.maxOperationBytes);
|
|
@@ -3192,53 +3329,73 @@ var ClientSession = class _ClientSession {
|
|
|
3192
3329
|
sizeCheck.message ?? `Operation "${op.id}" is too large`,
|
|
3193
3330
|
false
|
|
3194
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;
|
|
3195
3344
|
continue;
|
|
3196
3345
|
}
|
|
3197
3346
|
if (this.validateOperation && this.koraContext) {
|
|
3198
3347
|
let decision;
|
|
3199
3348
|
try {
|
|
3200
|
-
decision = await this.validateOperation(
|
|
3349
|
+
decision = await this.validateOperation(serverOp, {
|
|
3201
3350
|
auth: this.authContext,
|
|
3202
3351
|
kora: this.koraContext
|
|
3203
3352
|
});
|
|
3204
3353
|
} catch (error) {
|
|
3205
3354
|
const message = error instanceof Error ? error.message : "validator error";
|
|
3206
|
-
this.sendOperationRejected(
|
|
3355
|
+
this.sendOperationRejected(
|
|
3356
|
+
serverOp,
|
|
3357
|
+
"VALIDATION_ERROR",
|
|
3358
|
+
`Validator threw: ${message}`,
|
|
3359
|
+
true
|
|
3360
|
+
);
|
|
3361
|
+
canAdvanceAck = false;
|
|
3207
3362
|
continue;
|
|
3208
3363
|
}
|
|
3209
3364
|
if (decision.action === "reject") {
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
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
|
+
}
|
|
3216
3372
|
continue;
|
|
3217
3373
|
}
|
|
3218
3374
|
if (decision.action === "ignore") {
|
|
3375
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3219
3376
|
continue;
|
|
3220
3377
|
}
|
|
3221
3378
|
}
|
|
3222
|
-
const applyResult = await applyServerOperation(this.store,
|
|
3379
|
+
const applyResult = await applyServerOperation(this.store, serverOp);
|
|
3223
3380
|
if (applyResult.rejection) {
|
|
3224
|
-
this.
|
|
3381
|
+
this.sendOperationRejected(
|
|
3382
|
+
serverOp,
|
|
3225
3383
|
applyResult.rejection.code,
|
|
3226
3384
|
applyResult.rejection.message,
|
|
3227
3385
|
applyResult.rejection.retriable
|
|
3228
3386
|
);
|
|
3387
|
+
if (applyResult.rejection.retriable) {
|
|
3388
|
+
canAdvanceAck = false;
|
|
3389
|
+
} else {
|
|
3390
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3391
|
+
}
|
|
3229
3392
|
continue;
|
|
3230
3393
|
}
|
|
3231
3394
|
if (applyResult.result === "applied") {
|
|
3232
3395
|
applied.push(...applyResult.appliedOperations);
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
for (const op of rejected) {
|
|
3237
|
-
this.sendError(
|
|
3238
|
-
"SCOPE_VIOLATION",
|
|
3239
|
-
`Operation "${op.id}" in collection "${op.collection}" is outside the client's sync scope`,
|
|
3240
|
-
false
|
|
3241
|
-
);
|
|
3396
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3397
|
+
} else {
|
|
3398
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3242
3399
|
}
|
|
3243
3400
|
}
|
|
3244
3401
|
if (operations2.length > 0) {
|
|
@@ -3251,15 +3408,21 @@ var ClientSession = class _ClientSession {
|
|
|
3251
3408
|
const lastOp = operations2[operations2.length - 1];
|
|
3252
3409
|
const ack = {
|
|
3253
3410
|
type: "acknowledgment",
|
|
3254
|
-
messageId: (0,
|
|
3411
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3255
3412
|
acknowledgedMessageId: msg.messageId,
|
|
3256
|
-
lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
|
|
3413
|
+
lastSequenceNumber: lastOp ? Math.min(acknowledgedThrough, lastOp.sequenceNumber) : 0
|
|
3257
3414
|
};
|
|
3258
3415
|
this.sendToClient(ack);
|
|
3259
3416
|
if (applied.length > 0) {
|
|
3260
3417
|
this.onRelay?.(this.sessionId, applied);
|
|
3261
3418
|
}
|
|
3262
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
|
+
}
|
|
3263
3426
|
async sendDelta(clientVector) {
|
|
3264
3427
|
const serverVector = this.store.getVersionVector();
|
|
3265
3428
|
const missing = [];
|
|
@@ -3277,7 +3440,7 @@ var ClientSession = class _ClientSession {
|
|
|
3277
3440
|
if (missing.length === 0) {
|
|
3278
3441
|
const emptyBatch = {
|
|
3279
3442
|
type: "operation-batch",
|
|
3280
|
-
messageId: (0,
|
|
3443
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3281
3444
|
operations: [],
|
|
3282
3445
|
isFinal: true,
|
|
3283
3446
|
batchIndex: 0,
|
|
@@ -3292,7 +3455,7 @@ var ClientSession = class _ClientSession {
|
|
|
3292
3455
|
if (afterCursor.length === 0) {
|
|
3293
3456
|
const emptyBatch = {
|
|
3294
3457
|
type: "operation-batch",
|
|
3295
|
-
messageId: (0,
|
|
3458
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3296
3459
|
operations: [],
|
|
3297
3460
|
isFinal: true,
|
|
3298
3461
|
batchIndex: this.resumeDeltaCursor?.batchIndex ?? 0,
|
|
@@ -3308,7 +3471,7 @@ var ClientSession = class _ClientSession {
|
|
|
3308
3471
|
const batchCursor = (0, import_sync3.createDeltaCursorFromBatch)(batchOps, i);
|
|
3309
3472
|
const batchMsg = {
|
|
3310
3473
|
type: "operation-batch",
|
|
3311
|
-
messageId: (0,
|
|
3474
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3312
3475
|
operations: serializedOps,
|
|
3313
3476
|
isFinal: i === totalBatches - 1,
|
|
3314
3477
|
batchIndex: i,
|
|
@@ -3385,7 +3548,7 @@ var ClientSession = class _ClientSession {
|
|
|
3385
3548
|
sendDeliveryBatch(slice, base, max, batchIndex, isFinal) {
|
|
3386
3549
|
const batchMsg = {
|
|
3387
3550
|
type: "operation-batch",
|
|
3388
|
-
messageId: (0,
|
|
3551
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3389
3552
|
operations: slice.map((delivered) => this.serializer.encodeOperation(delivered.operation)),
|
|
3390
3553
|
isFinal,
|
|
3391
3554
|
batchIndex,
|
|
@@ -3438,7 +3601,7 @@ var ClientSession = class _ClientSession {
|
|
|
3438
3601
|
sendError(code, message, retriable) {
|
|
3439
3602
|
const errorMsg = {
|
|
3440
3603
|
type: "error",
|
|
3441
|
-
messageId: (0,
|
|
3604
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3442
3605
|
code,
|
|
3443
3606
|
message,
|
|
3444
3607
|
retriable
|
|
@@ -3454,7 +3617,7 @@ var ClientSession = class _ClientSession {
|
|
|
3454
3617
|
sendOperationRejected(op, code, message, retriable) {
|
|
3455
3618
|
const rejectedMsg = {
|
|
3456
3619
|
type: "operation-rejected",
|
|
3457
|
-
messageId: (0,
|
|
3620
|
+
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3458
3621
|
operationId: op.id,
|
|
3459
3622
|
collection: op.collection,
|
|
3460
3623
|
recordId: op.recordId,
|
|
@@ -3486,13 +3649,13 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
3486
3649
|
|
|
3487
3650
|
// src/server/kora-sync-server.ts
|
|
3488
3651
|
init_cjs_shims();
|
|
3489
|
-
var
|
|
3652
|
+
var import_core14 = require("@korajs/core");
|
|
3490
3653
|
var import_internal2 = require("@korajs/core/internal");
|
|
3491
3654
|
var import_sync5 = require("@korajs/sync");
|
|
3492
3655
|
|
|
3493
3656
|
// src/awareness/awareness-relay.ts
|
|
3494
3657
|
init_cjs_shims();
|
|
3495
|
-
var
|
|
3658
|
+
var import_core12 = require("@korajs/core");
|
|
3496
3659
|
var AwarenessRelay = class {
|
|
3497
3660
|
clients = /* @__PURE__ */ new Map();
|
|
3498
3661
|
/**
|
|
@@ -3521,7 +3684,7 @@ var AwarenessRelay = class {
|
|
|
3521
3684
|
if (hasStates) {
|
|
3522
3685
|
const catchUpMsg = {
|
|
3523
3686
|
type: "awareness-update",
|
|
3524
|
-
messageId: (0,
|
|
3687
|
+
messageId: (0, import_core12.generateUUIDv7)(),
|
|
3525
3688
|
clientId: 0,
|
|
3526
3689
|
// Server-sourced
|
|
3527
3690
|
states: existingStates
|
|
@@ -3544,7 +3707,7 @@ var AwarenessRelay = class {
|
|
|
3544
3707
|
};
|
|
3545
3708
|
const msg = {
|
|
3546
3709
|
type: "awareness-update",
|
|
3547
|
-
messageId: (0,
|
|
3710
|
+
messageId: (0, import_core12.generateUUIDv7)(),
|
|
3548
3711
|
clientId: client.clientId,
|
|
3549
3712
|
states: removalStates
|
|
3550
3713
|
};
|
|
@@ -3850,7 +4013,7 @@ var YjsDocRelay = class {
|
|
|
3850
4013
|
|
|
3851
4014
|
// src/server/route-context.ts
|
|
3852
4015
|
init_cjs_shims();
|
|
3853
|
-
var
|
|
4016
|
+
var import_core13 = require("@korajs/core");
|
|
3854
4017
|
|
|
3855
4018
|
// src/apply/route-predicate.ts
|
|
3856
4019
|
init_cjs_shims();
|
|
@@ -3925,16 +4088,16 @@ function pickFields(record, keys) {
|
|
|
3925
4088
|
}
|
|
3926
4089
|
async function buildRouteOperation(store, mutation, currentOverride, clockOverride) {
|
|
3927
4090
|
const nodeId = store.getNodeId();
|
|
3928
|
-
const clock = clockOverride ?? new
|
|
4091
|
+
const clock = clockOverride ?? new import_core13.HybridLogicalClock(nodeId);
|
|
3929
4092
|
const schemaVersion = store.getSchema()?.version ?? 1;
|
|
3930
4093
|
const sequenceNumber = nextServerSequenceNumber(store);
|
|
3931
4094
|
if (mutation.type === "insert") {
|
|
3932
|
-
return (0,
|
|
4095
|
+
return (0, import_core13.createOperation)(
|
|
3933
4096
|
{
|
|
3934
4097
|
nodeId,
|
|
3935
4098
|
type: "insert",
|
|
3936
4099
|
collection: mutation.collection,
|
|
3937
|
-
recordId: mutation.recordId ?? (0,
|
|
4100
|
+
recordId: mutation.recordId ?? (0, import_core13.generateUUIDv7)(),
|
|
3938
4101
|
data: mutation.data ?? {},
|
|
3939
4102
|
previousData: null,
|
|
3940
4103
|
sequenceNumber,
|
|
@@ -3957,14 +4120,14 @@ async function buildRouteOperation(store, mutation, currentOverride, clockOverri
|
|
|
3957
4120
|
const atomicOps = {};
|
|
3958
4121
|
for (const key of Object.keys(rawData)) {
|
|
3959
4122
|
const value = rawData[key];
|
|
3960
|
-
if ((0,
|
|
3961
|
-
data[key] = (0,
|
|
3962
|
-
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);
|
|
3963
4126
|
} else {
|
|
3964
4127
|
data[key] = value;
|
|
3965
4128
|
}
|
|
3966
4129
|
}
|
|
3967
|
-
return (0,
|
|
4130
|
+
return (0, import_core13.createOperation)(
|
|
3968
4131
|
{
|
|
3969
4132
|
nodeId,
|
|
3970
4133
|
type: "update",
|
|
@@ -3980,7 +4143,7 @@ async function buildRouteOperation(store, mutation, currentOverride, clockOverri
|
|
|
3980
4143
|
clock
|
|
3981
4144
|
);
|
|
3982
4145
|
}
|
|
3983
|
-
return (0,
|
|
4146
|
+
return (0, import_core13.createOperation)(
|
|
3984
4147
|
{
|
|
3985
4148
|
nodeId,
|
|
3986
4149
|
type: "delete",
|
|
@@ -4246,6 +4409,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4246
4409
|
batchSize;
|
|
4247
4410
|
schemaVersion;
|
|
4248
4411
|
supportedSchemaVersions;
|
|
4412
|
+
operationTransforms;
|
|
4249
4413
|
port;
|
|
4250
4414
|
host;
|
|
4251
4415
|
path;
|
|
@@ -4283,16 +4447,24 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4283
4447
|
static ORPHANED_RELAY_TTL_MS = 5 * 6e4;
|
|
4284
4448
|
constructor(config) {
|
|
4285
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
|
+
}
|
|
4286
4457
|
this.auth = config.auth ?? null;
|
|
4287
4458
|
this.serializer = config.serializer ?? new import_sync5.JsonMessageSerializer();
|
|
4288
4459
|
this.emitter = config.emitter ?? null;
|
|
4289
4460
|
this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
|
|
4290
4461
|
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
|
|
4291
|
-
this.schemaVersion = config.schemaVersion ?? DEFAULT_SCHEMA_VERSION2;
|
|
4462
|
+
this.schemaVersion = config.schemaVersion ?? storeSchemaVersion ?? DEFAULT_SCHEMA_VERSION2;
|
|
4292
4463
|
this.supportedSchemaVersions = config.supportedSchemaVersions ?? {
|
|
4293
4464
|
min: this.schemaVersion,
|
|
4294
4465
|
max: this.schemaVersion
|
|
4295
4466
|
};
|
|
4467
|
+
this.operationTransforms = config.operationTransforms ?? [];
|
|
4296
4468
|
this.port = config.port;
|
|
4297
4469
|
this.host = config.host ?? DEFAULT_HOST;
|
|
4298
4470
|
this.path = config.path ?? DEFAULT_PATH;
|
|
@@ -4416,6 +4588,21 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4416
4588
|
details: { reason: event.reason }
|
|
4417
4589
|
});
|
|
4418
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
|
+
});
|
|
4419
4606
|
}
|
|
4420
4607
|
/**
|
|
4421
4608
|
* Get the metrics collector for external access (e.g., HTTP endpoints).
|
|
@@ -4436,10 +4623,10 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4436
4623
|
*/
|
|
4437
4624
|
async start(wsServerImpl) {
|
|
4438
4625
|
if (this.running) {
|
|
4439
|
-
throw new
|
|
4626
|
+
throw new import_core14.SyncError("Server is already running", { port: this.port });
|
|
4440
4627
|
}
|
|
4441
4628
|
if (!wsServerImpl && this.port === void 0) {
|
|
4442
|
-
throw new
|
|
4629
|
+
throw new import_core14.SyncError(
|
|
4443
4630
|
"Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
|
|
4444
4631
|
{}
|
|
4445
4632
|
);
|
|
@@ -4554,7 +4741,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4554
4741
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
4555
4742
|
transport.send({
|
|
4556
4743
|
type: "error",
|
|
4557
|
-
messageId: (0,
|
|
4744
|
+
messageId: (0, import_core14.generateUUIDv7)(),
|
|
4558
4745
|
code: "MAX_CONNECTIONS",
|
|
4559
4746
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
4560
4747
|
retriable: true
|
|
@@ -4567,12 +4754,12 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4567
4754
|
event: "connection.rejected",
|
|
4568
4755
|
details: { reason: "max_connections", max: this.maxConnections }
|
|
4569
4756
|
});
|
|
4570
|
-
throw new
|
|
4757
|
+
throw new import_core14.SyncError("Maximum connections reached", {
|
|
4571
4758
|
current: this.sessions.size,
|
|
4572
4759
|
max: this.maxConnections
|
|
4573
4760
|
});
|
|
4574
4761
|
}
|
|
4575
|
-
const sessionId = (0,
|
|
4762
|
+
const sessionId = (0, import_core14.generateUUIDv7)();
|
|
4576
4763
|
this.metrics.recordConnection(sessionId);
|
|
4577
4764
|
const sessionEmitter = new import_internal2.SimpleEventEmitter();
|
|
4578
4765
|
sessionEmitter.on("sync:connected", (event) => {
|
|
@@ -4618,6 +4805,33 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4618
4805
|
sessionId
|
|
4619
4806
|
});
|
|
4620
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
|
+
});
|
|
4621
4835
|
const session = new ClientSession({
|
|
4622
4836
|
sessionId,
|
|
4623
4837
|
transport,
|
|
@@ -4629,6 +4843,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4629
4843
|
batchSize: this.batchSize,
|
|
4630
4844
|
schemaVersion: this.schemaVersion,
|
|
4631
4845
|
supportedSchemaVersions: this.supportedSchemaVersions,
|
|
4846
|
+
operationTransforms: this.operationTransforms,
|
|
4632
4847
|
onRelay: (sourceSessionId, operations2) => {
|
|
4633
4848
|
this.handleRelay(sourceSessionId, operations2);
|
|
4634
4849
|
},
|
|
@@ -4756,7 +4971,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4756
4971
|
const records = await this.store.materializeCollection(name);
|
|
4757
4972
|
for (const record of records) {
|
|
4758
4973
|
for (const value of Object.values(record)) {
|
|
4759
|
-
if ((0,
|
|
4974
|
+
if ((0, import_core14.isBlobRef)(value)) {
|
|
4760
4975
|
refs.push(value);
|
|
4761
4976
|
}
|
|
4762
4977
|
}
|