@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 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, import_core10.generateUUIDv7)(),
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, import_core10.hashBlob)(bytes);
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, import_core10.generateUUIDv7)(),
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, import_core10.generateUUIDv7)(),
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
- const rejected = [];
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
- rejected.push(op);
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(op, {
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(op, "VALIDATION_ERROR", `Validator threw: ${message}`, true);
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
- this.sendOperationRejected(
3211
- op,
3212
- decision.code,
3213
- decision.message,
3214
- decision.retriable ?? isRetriableRejection(decision.code)
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, op);
3379
+ const applyResult = await applyServerOperation(this.store, serverOp);
3223
3380
  if (applyResult.rejection) {
3224
- this.sendError(
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
- if (rejected.length > 0) {
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, import_core10.generateUUIDv7)(),
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, import_core10.generateUUIDv7)(),
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, import_core10.generateUUIDv7)(),
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, import_core10.generateUUIDv7)(),
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, import_core10.generateUUIDv7)(),
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, import_core10.generateUUIDv7)(),
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, import_core10.generateUUIDv7)(),
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 import_core13 = require("@korajs/core");
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 import_core11 = require("@korajs/core");
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, import_core11.generateUUIDv7)(),
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, import_core11.generateUUIDv7)(),
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 import_core12 = require("@korajs/core");
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 import_core12.HybridLogicalClock(nodeId);
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, import_core12.createOperation)(
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, import_core12.generateUUIDv7)(),
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, import_core12.isAtomicOp)(value)) {
3961
- data[key] = (0, import_core12.resolveAtomicOp)(current ? current[key] : void 0, value);
3962
- atomicOps[key] = (0, import_core12.toAtomicOp)(value);
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, import_core12.createOperation)(
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, import_core12.createOperation)(
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 import_core13.SyncError("Server is already running", { port: this.port });
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 import_core13.SyncError(
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, import_core13.generateUUIDv7)(),
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 import_core13.SyncError("Maximum connections reached", {
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, import_core13.generateUUIDv7)();
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, import_core13.isBlobRef)(value)) {
4974
+ if ((0, import_core14.isBlobRef)(value)) {
4760
4975
  refs.push(value);
4761
4976
  }
4762
4977
  }