@korajs/server 1.0.0-beta.11 → 1.0.0-beta.12
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 +216 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +216 -53
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -2775,6 +2775,20 @@ function operationMatchesScopes(op, scopes, fullRecord) {
|
|
|
2775
2775
|
}
|
|
2776
2776
|
return true;
|
|
2777
2777
|
}
|
|
2778
|
+
function operationExitsScopes(op, scopes, resultingRecord) {
|
|
2779
|
+
if (!scopes || op.type !== "update" || !op.previousData) return false;
|
|
2780
|
+
if (operationMatchesScopes(op, scopes, resultingRecord)) return false;
|
|
2781
|
+
const previousSnapshot = {
|
|
2782
|
+
...resultingRecord ?? {},
|
|
2783
|
+
...op.data ?? {},
|
|
2784
|
+
...op.previousData
|
|
2785
|
+
};
|
|
2786
|
+
return operationMatchesScopes(
|
|
2787
|
+
{ ...op, type: "insert", data: previousSnapshot, previousData: null },
|
|
2788
|
+
scopes,
|
|
2789
|
+
previousSnapshot
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2778
2792
|
function matchesPredicate(actual, expected) {
|
|
2779
2793
|
if (expected && typeof expected === "object" && !Array.isArray(expected) && "$in" in expected) {
|
|
2780
2794
|
const values = expected.$in;
|
|
@@ -2908,6 +2922,7 @@ var ClientSession = class _ClientSession {
|
|
|
2908
2922
|
clientNodeId = null;
|
|
2909
2923
|
authContext = null;
|
|
2910
2924
|
syncQuerySubsets = [];
|
|
2925
|
+
scopeExitPolicy = "retain";
|
|
2911
2926
|
resumeDeltaCursor = null;
|
|
2912
2927
|
/**
|
|
2913
2928
|
* The delivery watermark the client reported at handshake, or null when the client
|
|
@@ -2932,7 +2947,7 @@ var ClientSession = class _ClientSession {
|
|
|
2932
2947
|
* batches every server tick while the first batch is still being applied.
|
|
2933
2948
|
*/
|
|
2934
2949
|
lastDeliveryPushAttemptAtMs = 0;
|
|
2935
|
-
|
|
2950
|
+
outstandingDelivery = null;
|
|
2936
2951
|
/** Serializes incremental delivery pushes so their batches never interleave. */
|
|
2937
2952
|
deliveryPushChain = Promise.resolve();
|
|
2938
2953
|
/**
|
|
@@ -3031,34 +3046,66 @@ var ClientSession = class _ClientSession {
|
|
|
3031
3046
|
* from the highest delivery sequence already sent. Pushes are serialized so their
|
|
3032
3047
|
* batches never interleave (which would break the base/max chain).
|
|
3033
3048
|
*/
|
|
3034
|
-
pushDeliveryStream() {
|
|
3049
|
+
pushDeliveryStream(options = {}) {
|
|
3035
3050
|
this.deliveryPushChain = this.deliveryPushChain.then(async () => {
|
|
3036
3051
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
3037
3052
|
try {
|
|
3053
|
+
const previous = this.outstandingDelivery;
|
|
3054
|
+
if (options.trackStall && previous && previous.max > this.lastAckedDeliverySeq) {
|
|
3055
|
+
previous.repeatCount += 1;
|
|
3056
|
+
if (previous.repeatCount >= 3) {
|
|
3057
|
+
this.emitter?.emit({
|
|
3058
|
+
type: "sync:delivery-stalled",
|
|
3059
|
+
sessionId: this.sessionId,
|
|
3060
|
+
watermark: this.lastAckedDeliverySeq,
|
|
3061
|
+
outstandingMaxDeliverySequence: previous.max,
|
|
3062
|
+
repeatCount: previous.repeatCount,
|
|
3063
|
+
reason: "unacknowledged-delivery"
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3038
3067
|
this.lastDeliveryPushAttemptAtMs = Date.now();
|
|
3039
|
-
await this.sendDeliveryStream(
|
|
3068
|
+
const result = await this.sendDeliveryStream(
|
|
3040
3069
|
this.lastAckedDeliverySeq,
|
|
3041
3070
|
false,
|
|
3042
3071
|
this.clientNodeId ?? void 0
|
|
3043
3072
|
);
|
|
3073
|
+
if (result.sent && result.maxScanned > this.lastAckedDeliverySeq) {
|
|
3074
|
+
if (!this.outstandingDelivery || this.outstandingDelivery.base !== this.lastAckedDeliverySeq || this.outstandingDelivery.max !== result.maxScanned) {
|
|
3075
|
+
this.outstandingDelivery = {
|
|
3076
|
+
base: this.lastAckedDeliverySeq,
|
|
3077
|
+
max: result.maxScanned,
|
|
3078
|
+
sentAtMs: Date.now(),
|
|
3079
|
+
repeatCount: 0
|
|
3080
|
+
};
|
|
3081
|
+
} else {
|
|
3082
|
+
this.outstandingDelivery.sentAtMs = Date.now();
|
|
3083
|
+
}
|
|
3084
|
+
} else if (result.maxScanned <= this.lastAckedDeliverySeq) {
|
|
3085
|
+
this.outstandingDelivery = null;
|
|
3086
|
+
}
|
|
3044
3087
|
} catch {
|
|
3045
3088
|
}
|
|
3046
3089
|
});
|
|
3047
3090
|
}
|
|
3048
3091
|
async relayVisibleOperations(operations2) {
|
|
3049
3092
|
const visibleOperations = [];
|
|
3093
|
+
const retractions = [];
|
|
3050
3094
|
for (const op of operations2) {
|
|
3051
3095
|
if (await this.operationVisibleToClient(op)) {
|
|
3052
3096
|
visibleOperations.push(op);
|
|
3097
|
+
} else if (await this.scopeRetractionFor(op)) {
|
|
3098
|
+
retractions.push({ collection: op.collection, recordId: op.recordId });
|
|
3053
3099
|
}
|
|
3054
3100
|
}
|
|
3055
|
-
if (visibleOperations.length === 0) return;
|
|
3101
|
+
if (visibleOperations.length === 0 && retractions.length === 0) return;
|
|
3056
3102
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
3057
3103
|
const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
|
|
3058
3104
|
const msg = {
|
|
3059
3105
|
type: "operation-batch",
|
|
3060
3106
|
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3061
3107
|
operations: serializedOps,
|
|
3108
|
+
...retractions.length > 0 ? { retractions } : {},
|
|
3062
3109
|
isFinal: true,
|
|
3063
3110
|
batchIndex: 0
|
|
3064
3111
|
};
|
|
@@ -3102,21 +3149,14 @@ var ClientSession = class _ClientSession {
|
|
|
3102
3149
|
pushDeliveryStreamIfSupported(staleMs = 0, options = {}) {
|
|
3103
3150
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
3104
3151
|
if (this.clientDeliveryWatermark === null) return;
|
|
3105
|
-
if (
|
|
3152
|
+
if (options.serverFrontier !== void 0 && options.serverFrontier <= this.lastAckedDeliverySeq) {
|
|
3153
|
+
this.outstandingDelivery = null;
|
|
3106
3154
|
return;
|
|
3107
3155
|
}
|
|
3108
|
-
if (
|
|
3109
|
-
|
|
3110
|
-
if (this.deliveryStallRepeatCount >= 3) {
|
|
3111
|
-
this.emitter?.emit({
|
|
3112
|
-
type: "sync:delivery-stalled",
|
|
3113
|
-
sessionId: this.sessionId,
|
|
3114
|
-
watermark: this.lastAckedDeliverySeq,
|
|
3115
|
-
repeatCount: this.deliveryStallRepeatCount
|
|
3116
|
-
});
|
|
3117
|
-
}
|
|
3156
|
+
if (staleMs > 0 && Date.now() - this.lastDeliveryPushAttemptAtMs < staleMs) {
|
|
3157
|
+
return;
|
|
3118
3158
|
}
|
|
3119
|
-
this.pushDeliveryStream();
|
|
3159
|
+
this.pushDeliveryStream({ trackStall: options.trackStall });
|
|
3120
3160
|
}
|
|
3121
3161
|
/**
|
|
3122
3162
|
* Close this session.
|
|
@@ -3199,7 +3239,11 @@ var ClientSession = class _ClientSession {
|
|
|
3199
3239
|
this.pendingRelays.delete(message.acknowledgedMessageId);
|
|
3200
3240
|
if (message.deliverySequence !== void 0) {
|
|
3201
3241
|
this.lastAckedDeliverySeq = Math.max(this.lastAckedDeliverySeq, message.deliverySequence);
|
|
3202
|
-
this.
|
|
3242
|
+
if (this.outstandingDelivery && this.lastAckedDeliverySeq >= this.outstandingDelivery.max) {
|
|
3243
|
+
this.outstandingDelivery = null;
|
|
3244
|
+
} else if (this.outstandingDelivery) {
|
|
3245
|
+
this.outstandingDelivery.repeatCount = 0;
|
|
3246
|
+
}
|
|
3203
3247
|
}
|
|
3204
3248
|
break;
|
|
3205
3249
|
case "error":
|
|
@@ -3260,6 +3304,7 @@ var ClientSession = class _ClientSession {
|
|
|
3260
3304
|
return;
|
|
3261
3305
|
}
|
|
3262
3306
|
this.clientNodeId = msg.nodeId;
|
|
3307
|
+
this.scopeExitPolicy = msg.scopeExitPolicy ?? "retain";
|
|
3263
3308
|
if (this.auth) {
|
|
3264
3309
|
const token = msg.authToken ?? "";
|
|
3265
3310
|
const context = await this.auth.authenticate(token);
|
|
@@ -3271,13 +3316,19 @@ var ClientSession = class _ClientSession {
|
|
|
3271
3316
|
this.authContext = context;
|
|
3272
3317
|
this.state = "authenticated";
|
|
3273
3318
|
}
|
|
3274
|
-
const
|
|
3319
|
+
const directionalScopesConfigured = this.authContext?.downlinkScopes !== void 0 || this.authContext?.uplinkScopes !== void 0;
|
|
3320
|
+
const downlinkAuthScopes = directionalScopesConfigured ? this.authContext?.downlinkScopes ?? this.authContext?.scopes ?? {} : this.authContext?.scopes;
|
|
3321
|
+
const uplinkAuthScopes = directionalScopesConfigured ? this.authContext?.uplinkScopes ?? this.authContext?.scopes ?? {} : this.authContext?.scopes;
|
|
3322
|
+
const rawResolvedDownlinkScopes = resolveSessionScopes(this.store.getSchema(), {
|
|
3275
3323
|
handshakeScope: msg.syncScope,
|
|
3276
|
-
authScopes:
|
|
3324
|
+
authScopes: downlinkAuthScopes
|
|
3277
3325
|
});
|
|
3278
|
-
|
|
3326
|
+
const rawResolvedUplinkScopes = directionalScopesConfigured ? uplinkAuthScopes : rawResolvedDownlinkScopes;
|
|
3327
|
+
let resolvedDownlinkScopes;
|
|
3328
|
+
let resolvedUplinkScopes;
|
|
3279
3329
|
try {
|
|
3280
|
-
|
|
3330
|
+
resolvedDownlinkScopes = rawResolvedDownlinkScopes ? normalizeScopeMap(rawResolvedDownlinkScopes) : directionalScopesConfigured ? {} : void 0;
|
|
3331
|
+
resolvedUplinkScopes = rawResolvedUplinkScopes ? normalizeScopeMap(rawResolvedUplinkScopes) : directionalScopesConfigured ? {} : void 0;
|
|
3281
3332
|
} catch (error) {
|
|
3282
3333
|
this.sendToClient({
|
|
3283
3334
|
type: "error",
|
|
@@ -3289,14 +3340,24 @@ var ClientSession = class _ClientSession {
|
|
|
3289
3340
|
this.close("invalid scope predicate");
|
|
3290
3341
|
return;
|
|
3291
3342
|
}
|
|
3292
|
-
if (
|
|
3343
|
+
if (resolvedDownlinkScopes || resolvedUplinkScopes) {
|
|
3293
3344
|
if (this.authContext) {
|
|
3294
|
-
this.authContext = {
|
|
3345
|
+
this.authContext = {
|
|
3346
|
+
...this.authContext,
|
|
3347
|
+
scopes: resolvedDownlinkScopes,
|
|
3348
|
+
downlinkScopes: resolvedDownlinkScopes,
|
|
3349
|
+
uplinkScopes: resolvedUplinkScopes
|
|
3350
|
+
};
|
|
3295
3351
|
} else {
|
|
3296
|
-
this.authContext = {
|
|
3352
|
+
this.authContext = {
|
|
3353
|
+
userId: msg.nodeId,
|
|
3354
|
+
scopes: resolvedDownlinkScopes,
|
|
3355
|
+
downlinkScopes: resolvedDownlinkScopes,
|
|
3356
|
+
uplinkScopes: resolvedUplinkScopes
|
|
3357
|
+
};
|
|
3297
3358
|
}
|
|
3298
3359
|
}
|
|
3299
|
-
warnIfMultiTenantWithoutScopes(this.auth,
|
|
3360
|
+
warnIfMultiTenantWithoutScopes(this.auth, resolvedDownlinkScopes, this.store.getSchema());
|
|
3300
3361
|
if (msg.syncQueries && msg.syncQueries.length > 0) {
|
|
3301
3362
|
this.syncQuerySubsets = (0, import_sync3.dedupeQuerySubsets)(msg.syncQueries);
|
|
3302
3363
|
} else {
|
|
@@ -3304,7 +3365,7 @@ var ClientSession = class _ClientSession {
|
|
|
3304
3365
|
}
|
|
3305
3366
|
this.resumeDeltaCursor = msg.deltaCursor ? (0, import_sync3.decodeDeltaCursor)(msg.deltaCursor) : null;
|
|
3306
3367
|
this.clientDeliveryWatermark = msg.lastDeliverySequence ?? null;
|
|
3307
|
-
if (this.clientDeliveryWatermark !== null && !sameScopeMap(msg.syncScope, this.authContext?.
|
|
3368
|
+
if (this.clientDeliveryWatermark !== null && !sameScopeMap(msg.syncScope, this.authContext?.downlinkScopes)) {
|
|
3308
3369
|
this.clientDeliveryWatermark = 0;
|
|
3309
3370
|
}
|
|
3310
3371
|
const clientUsesWatermark = this.clientDeliveryWatermark !== null;
|
|
@@ -3362,7 +3423,11 @@ var ClientSession = class _ClientSession {
|
|
|
3362
3423
|
...this.persistBlobChunk ? { blobStorageEnabled: true } : {},
|
|
3363
3424
|
// Confirm the accepted scope so the client knows what data will be synced.
|
|
3364
3425
|
// This may differ from what the client requested if auth scopes are narrower.
|
|
3365
|
-
...this.authContext?.
|
|
3426
|
+
...this.authContext?.downlinkScopes ? {
|
|
3427
|
+
acceptedScope: this.authContext.downlinkScopes,
|
|
3428
|
+
acceptedDownlinkScopes: this.authContext.downlinkScopes
|
|
3429
|
+
} : {},
|
|
3430
|
+
...this.authContext?.uplinkScopes ? { acceptedUplinkScopes: this.authContext.uplinkScopes } : {}
|
|
3366
3431
|
};
|
|
3367
3432
|
this.sendToClient(response);
|
|
3368
3433
|
this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
|
|
@@ -3389,18 +3454,22 @@ var ClientSession = class _ClientSession {
|
|
|
3389
3454
|
const applied = [];
|
|
3390
3455
|
let acknowledgedThrough = 0;
|
|
3391
3456
|
let canAdvanceAck = true;
|
|
3457
|
+
let uniqueOperations = 0;
|
|
3458
|
+
let duplicateOperations = 0;
|
|
3459
|
+
let rejectedOperations = 0;
|
|
3392
3460
|
for (const op of operations2) {
|
|
3393
3461
|
if (!canAdvanceAck) {
|
|
3394
3462
|
continue;
|
|
3395
3463
|
}
|
|
3396
|
-
if (!await this.
|
|
3464
|
+
if (!await this.operationAllowedFromClient(op)) {
|
|
3397
3465
|
this.sendOperationRejected(
|
|
3398
3466
|
op,
|
|
3399
3467
|
"SCOPE_VIOLATION",
|
|
3400
|
-
`Operation "${op.id}" in collection "${op.collection}" is outside the
|
|
3401
|
-
|
|
3468
|
+
`Operation "${op.id}" in collection "${op.collection}" is outside the accepted uplink scope. Refresh scopes before creating or explicitly resubmitting an authorized operation.`,
|
|
3469
|
+
false
|
|
3402
3470
|
);
|
|
3403
|
-
|
|
3471
|
+
rejectedOperations += 1;
|
|
3472
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3404
3473
|
continue;
|
|
3405
3474
|
}
|
|
3406
3475
|
if (!isOperationTimestampValid(op)) {
|
|
@@ -3439,6 +3508,7 @@ var ClientSession = class _ClientSession {
|
|
|
3439
3508
|
`Operation "${op.id}" cannot be transformed from schema v${op.schemaVersion} to server schema v${this.schemaVersion}.`,
|
|
3440
3509
|
false
|
|
3441
3510
|
);
|
|
3511
|
+
rejectedOperations += 1;
|
|
3442
3512
|
acknowledgedThrough = op.sequenceNumber;
|
|
3443
3513
|
continue;
|
|
3444
3514
|
}
|
|
@@ -3463,6 +3533,7 @@ var ClientSession = class _ClientSession {
|
|
|
3463
3533
|
if (decision.action === "reject") {
|
|
3464
3534
|
const retriable = decision.retriable ?? isRetriableRejection(decision.code);
|
|
3465
3535
|
this.sendOperationRejected(serverOp, decision.code, decision.message, retriable);
|
|
3536
|
+
rejectedOperations += 1;
|
|
3466
3537
|
if (retriable) {
|
|
3467
3538
|
canAdvanceAck = false;
|
|
3468
3539
|
} else {
|
|
@@ -3483,6 +3554,7 @@ var ClientSession = class _ClientSession {
|
|
|
3483
3554
|
applyResult.rejection.message,
|
|
3484
3555
|
applyResult.rejection.retriable
|
|
3485
3556
|
);
|
|
3557
|
+
rejectedOperations += 1;
|
|
3486
3558
|
if (applyResult.rejection.retriable) {
|
|
3487
3559
|
canAdvanceAck = false;
|
|
3488
3560
|
} else {
|
|
@@ -3492,8 +3564,10 @@ var ClientSession = class _ClientSession {
|
|
|
3492
3564
|
}
|
|
3493
3565
|
if (applyResult.result === "applied") {
|
|
3494
3566
|
applied.push(...applyResult.appliedOperations);
|
|
3567
|
+
uniqueOperations += 1;
|
|
3495
3568
|
acknowledgedThrough = op.sequenceNumber;
|
|
3496
3569
|
} else {
|
|
3570
|
+
duplicateOperations += 1;
|
|
3497
3571
|
acknowledgedThrough = op.sequenceNumber;
|
|
3498
3572
|
}
|
|
3499
3573
|
}
|
|
@@ -3501,7 +3575,10 @@ var ClientSession = class _ClientSession {
|
|
|
3501
3575
|
this.emitter?.emit({
|
|
3502
3576
|
type: "sync:received",
|
|
3503
3577
|
operations: operations2,
|
|
3504
|
-
batchSize: operations2.length
|
|
3578
|
+
batchSize: operations2.length,
|
|
3579
|
+
uniqueOperations,
|
|
3580
|
+
duplicateOperations,
|
|
3581
|
+
rejectedOperations
|
|
3505
3582
|
});
|
|
3506
3583
|
}
|
|
3507
3584
|
const lastOp = operations2[operations2.length - 1];
|
|
@@ -3606,7 +3683,7 @@ var ClientSession = class _ClientSession {
|
|
|
3606
3683
|
const scanChunk = Math.max(this.batchSize, 1) * 5;
|
|
3607
3684
|
let scanCursor = fromDeliverySeq;
|
|
3608
3685
|
let maxScanned = fromDeliverySeq;
|
|
3609
|
-
const
|
|
3686
|
+
const deliverable = [];
|
|
3610
3687
|
while (true) {
|
|
3611
3688
|
const chunk = await this.store.getOperationsAfterDelivery(scanCursor, scanChunk);
|
|
3612
3689
|
const last = chunk[chunk.length - 1];
|
|
@@ -3618,21 +3695,24 @@ var ClientSession = class _ClientSession {
|
|
|
3618
3695
|
continue;
|
|
3619
3696
|
}
|
|
3620
3697
|
if (await this.operationVisibleToClient(delivered.operation)) {
|
|
3621
|
-
|
|
3698
|
+
deliverable.push(delivered);
|
|
3699
|
+
} else if (await this.scopeRetractionFor(delivered.operation)) {
|
|
3700
|
+
deliverable.push({ ...delivered, retraction: true });
|
|
3622
3701
|
}
|
|
3623
3702
|
}
|
|
3624
3703
|
if (chunk.length < scanChunk) break;
|
|
3625
3704
|
}
|
|
3626
|
-
if (
|
|
3627
|
-
|
|
3628
|
-
|
|
3705
|
+
if (deliverable.length === 0) {
|
|
3706
|
+
let sent = false;
|
|
3707
|
+
if (finalizeWhenEmpty || maxScanned > fromDeliverySeq) {
|
|
3708
|
+
sent = this.sendDeliveryBatch([], fromDeliverySeq, maxScanned, 0, true);
|
|
3629
3709
|
}
|
|
3630
|
-
return 0;
|
|
3710
|
+
return { sentOperations: 0, maxScanned, sent };
|
|
3631
3711
|
}
|
|
3632
|
-
const totalBatches = Math.ceil(
|
|
3712
|
+
const totalBatches = Math.ceil(deliverable.length / this.batchSize);
|
|
3633
3713
|
let base = fromDeliverySeq;
|
|
3634
3714
|
for (let i = 0; i < totalBatches; i++) {
|
|
3635
|
-
const slice =
|
|
3715
|
+
const slice = deliverable.slice(i * this.batchSize, (i + 1) * this.batchSize);
|
|
3636
3716
|
const lastInSlice = slice[slice.length - 1];
|
|
3637
3717
|
if (lastInSlice === void 0) continue;
|
|
3638
3718
|
const isFinal = i === totalBatches - 1;
|
|
@@ -3641,30 +3721,42 @@ var ClientSession = class _ClientSession {
|
|
|
3641
3721
|
this.sendDeliveryBatch(slice, base, max, i, isFinal);
|
|
3642
3722
|
base = max;
|
|
3643
3723
|
}
|
|
3644
|
-
return
|
|
3724
|
+
return {
|
|
3725
|
+
sentOperations: deliverable.filter((item) => !item.retraction).length,
|
|
3726
|
+
maxScanned,
|
|
3727
|
+
sent: true
|
|
3728
|
+
};
|
|
3645
3729
|
}
|
|
3646
3730
|
/** Build, track, and send one chained delivery-stream batch. */
|
|
3647
3731
|
sendDeliveryBatch(slice, base, max, batchIndex, isFinal) {
|
|
3648
3732
|
const batchMsg = {
|
|
3649
3733
|
type: "operation-batch",
|
|
3650
3734
|
messageId: (0, import_core11.generateUUIDv7)(),
|
|
3651
|
-
operations: slice.map((delivered) => this.serializer.encodeOperation(delivered.operation)),
|
|
3735
|
+
operations: slice.filter((delivered) => !delivered.retraction).map((delivered) => this.serializer.encodeOperation(delivered.operation)),
|
|
3736
|
+
...slice.some((delivered) => delivered.retraction) ? {
|
|
3737
|
+
retractions: slice.filter((delivered) => delivered.retraction).map((delivered) => ({
|
|
3738
|
+
collection: delivered.operation.collection,
|
|
3739
|
+
recordId: delivered.operation.recordId
|
|
3740
|
+
}))
|
|
3741
|
+
} : {},
|
|
3652
3742
|
isFinal,
|
|
3653
3743
|
batchIndex,
|
|
3654
3744
|
baseDeliverySequence: base,
|
|
3655
3745
|
maxDeliverySequence: max
|
|
3656
3746
|
};
|
|
3657
|
-
this.sendToClient(batchMsg);
|
|
3658
|
-
|
|
3747
|
+
const sent = this.sendToClient(batchMsg);
|
|
3748
|
+
const sentOperations = slice.filter((delivered) => !delivered.retraction);
|
|
3749
|
+
if (sentOperations.length > 0) {
|
|
3659
3750
|
this.emitter?.emit({
|
|
3660
3751
|
type: "sync:sent",
|
|
3661
|
-
operations:
|
|
3662
|
-
batchSize:
|
|
3752
|
+
operations: sentOperations.map((delivered) => delivered.operation),
|
|
3753
|
+
batchSize: sentOperations.length
|
|
3663
3754
|
});
|
|
3664
3755
|
}
|
|
3756
|
+
return sent;
|
|
3665
3757
|
}
|
|
3666
3758
|
async operationVisibleToClient(op) {
|
|
3667
|
-
const scopes = this.authContext?.scopes;
|
|
3759
|
+
const scopes = this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
3668
3760
|
const subsets = this.syncQuerySubsets;
|
|
3669
3761
|
const needsBackfill = missingScopeFields(op, scopes).length > 0 || subsets !== void 0 && subsets.length > 0;
|
|
3670
3762
|
const fullRecord = needsBackfill ? await this.lookupRecordFields(op.collection, op.recordId) : void 0;
|
|
@@ -3673,6 +3765,20 @@ var ClientSession = class _ClientSession {
|
|
|
3673
3765
|
}
|
|
3674
3766
|
return (0, import_sync3.operationMatchesQuerySubsets)(op, subsets, fullRecord);
|
|
3675
3767
|
}
|
|
3768
|
+
/** Upload authorization is independent from the client's downloaded/query view. */
|
|
3769
|
+
async operationAllowedFromClient(op) {
|
|
3770
|
+
const scopes = this.authContext?.uplinkScopes ?? this.authContext?.scopes;
|
|
3771
|
+
const needsBackfill = missingScopeFields(op, scopes).length > 0;
|
|
3772
|
+
const fullRecord = needsBackfill ? await this.lookupRecordFields(op.collection, op.recordId) : void 0;
|
|
3773
|
+
return operationMatchesScopes(op, scopes, fullRecord);
|
|
3774
|
+
}
|
|
3775
|
+
async scopeRetractionFor(op) {
|
|
3776
|
+
if (this.scopeExitPolicy !== "retract") return false;
|
|
3777
|
+
const scopes = this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
3778
|
+
if (!scopes) return false;
|
|
3779
|
+
const resultingRecord = await this.lookupRecordFields(op.collection, op.recordId);
|
|
3780
|
+
return operationExitsScopes(op, scopes, resultingRecord);
|
|
3781
|
+
}
|
|
3676
3782
|
/**
|
|
3677
3783
|
* Read a record's current field values from the materialized store for scope /
|
|
3678
3784
|
* query-subset backfill. Includes soft-deleted rows so a relayed delete (whose op
|
|
@@ -3864,6 +3970,10 @@ var ServerMetricsCollector = class {
|
|
|
3864
3970
|
peakConnections = 0;
|
|
3865
3971
|
connectionsTotal = 0;
|
|
3866
3972
|
operationsReceived = 0;
|
|
3973
|
+
batchesReceived = 0;
|
|
3974
|
+
uniqueOperationsReceived = 0;
|
|
3975
|
+
duplicateOperationsReceived = 0;
|
|
3976
|
+
rejectedOperations = 0;
|
|
3867
3977
|
operationsSent = 0;
|
|
3868
3978
|
bytesReceived = 0;
|
|
3869
3979
|
bytesSent = 0;
|
|
@@ -3879,6 +3989,10 @@ var ServerMetricsCollector = class {
|
|
|
3879
3989
|
state: "connected",
|
|
3880
3990
|
connectedAt: Date.now(),
|
|
3881
3991
|
operationsReceived: 0,
|
|
3992
|
+
batchesReceived: 0,
|
|
3993
|
+
uniqueOperationsReceived: 0,
|
|
3994
|
+
duplicateOperationsReceived: 0,
|
|
3995
|
+
rejectedOperations: 0,
|
|
3882
3996
|
operationsSent: 0,
|
|
3883
3997
|
authContext: null
|
|
3884
3998
|
});
|
|
@@ -3910,12 +4024,24 @@ var ServerMetricsCollector = class {
|
|
|
3910
4024
|
}
|
|
3911
4025
|
}
|
|
3912
4026
|
/** Record operations received from a client. */
|
|
3913
|
-
recordReceived(sessionId, count3, byteSize
|
|
4027
|
+
recordReceived(sessionId, count3, byteSize, outcomes = {
|
|
4028
|
+
unique: count3,
|
|
4029
|
+
duplicates: 0,
|
|
4030
|
+
rejected: 0
|
|
4031
|
+
}) {
|
|
3914
4032
|
this.operationsReceived += count3;
|
|
4033
|
+
this.batchesReceived += 1;
|
|
4034
|
+
this.uniqueOperationsReceived += outcomes.unique;
|
|
4035
|
+
this.duplicateOperationsReceived += outcomes.duplicates;
|
|
4036
|
+
this.rejectedOperations += outcomes.rejected;
|
|
3915
4037
|
this.bytesReceived += byteSize;
|
|
3916
4038
|
const client = this.clientMetrics.get(sessionId);
|
|
3917
4039
|
if (client) {
|
|
3918
4040
|
client.operationsReceived += count3;
|
|
4041
|
+
client.batchesReceived += 1;
|
|
4042
|
+
client.uniqueOperationsReceived += outcomes.unique;
|
|
4043
|
+
client.duplicateOperationsReceived += outcomes.duplicates;
|
|
4044
|
+
client.rejectedOperations += outcomes.rejected;
|
|
3919
4045
|
}
|
|
3920
4046
|
}
|
|
3921
4047
|
/** Record operations sent to a client. */
|
|
@@ -3943,6 +4069,10 @@ var ServerMetricsCollector = class {
|
|
|
3943
4069
|
peakConnections: this.peakConnections,
|
|
3944
4070
|
connectionsTotal: this.connectionsTotal,
|
|
3945
4071
|
operationsReceived: this.operationsReceived,
|
|
4072
|
+
batchesReceived: this.batchesReceived,
|
|
4073
|
+
uniqueOperationsReceived: this.uniqueOperationsReceived,
|
|
4074
|
+
duplicateOperationsReceived: this.duplicateOperationsReceived,
|
|
4075
|
+
rejectedOperations: this.rejectedOperations,
|
|
3946
4076
|
operationsSent: this.operationsSent,
|
|
3947
4077
|
bytesReceived: this.bytesReceived,
|
|
3948
4078
|
bytesSent: this.bytesSent,
|
|
@@ -3959,6 +4089,10 @@ var ServerMetricsCollector = class {
|
|
|
3959
4089
|
this.peakConnections = 0;
|
|
3960
4090
|
this.connectionsTotal = 0;
|
|
3961
4091
|
this.operationsReceived = 0;
|
|
4092
|
+
this.batchesReceived = 0;
|
|
4093
|
+
this.uniqueOperationsReceived = 0;
|
|
4094
|
+
this.duplicateOperationsReceived = 0;
|
|
4095
|
+
this.rejectedOperations = 0;
|
|
3962
4096
|
this.operationsSent = 0;
|
|
3963
4097
|
this.bytesReceived = 0;
|
|
3964
4098
|
this.bytesSent = 0;
|
|
@@ -4640,14 +4774,17 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4640
4774
|
if (maxDeliverySequence <= this.lastObservedDeliverySequence) {
|
|
4641
4775
|
for (const session of this.sessions.values()) {
|
|
4642
4776
|
session.pushDeliveryStreamIfSupported(this.deliveryPollIntervalMs, {
|
|
4643
|
-
trackStall: true
|
|
4777
|
+
trackStall: true,
|
|
4778
|
+
serverFrontier: maxDeliverySequence
|
|
4644
4779
|
});
|
|
4645
4780
|
}
|
|
4646
4781
|
return;
|
|
4647
4782
|
}
|
|
4648
4783
|
this.lastObservedDeliverySequence = maxDeliverySequence;
|
|
4649
4784
|
for (const session of this.sessions.values()) {
|
|
4650
|
-
session.pushDeliveryStreamIfSupported(0
|
|
4785
|
+
session.pushDeliveryStreamIfSupported(0, {
|
|
4786
|
+
serverFrontier: maxDeliverySequence
|
|
4787
|
+
});
|
|
4651
4788
|
}
|
|
4652
4789
|
} catch (error) {
|
|
4653
4790
|
this.logger.log({
|
|
@@ -4721,7 +4858,11 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4721
4858
|
});
|
|
4722
4859
|
sessionEmitter.on("sync:received", (event) => {
|
|
4723
4860
|
const byteSize = estimateByteSize(event.operations);
|
|
4724
|
-
this.metrics.recordReceived(sessionId, event.batchSize, byteSize
|
|
4861
|
+
this.metrics.recordReceived(sessionId, event.batchSize, byteSize, {
|
|
4862
|
+
unique: event.uniqueOperations ?? event.batchSize,
|
|
4863
|
+
duplicates: event.duplicateOperations ?? 0,
|
|
4864
|
+
rejected: event.rejectedOperations ?? 0
|
|
4865
|
+
});
|
|
4725
4866
|
this.logger.log({
|
|
4726
4867
|
timestamp: Date.now(),
|
|
4727
4868
|
level: "info",
|
|
@@ -4944,7 +5085,11 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4944
5085
|
});
|
|
4945
5086
|
sessionEmitter.on("sync:received", (event) => {
|
|
4946
5087
|
const byteSize = estimateOperationByteSize(event.operations);
|
|
4947
|
-
this.metrics.recordReceived(sessionId, event.batchSize, byteSize
|
|
5088
|
+
this.metrics.recordReceived(sessionId, event.batchSize, byteSize, {
|
|
5089
|
+
unique: event.uniqueOperations ?? event.batchSize,
|
|
5090
|
+
duplicates: event.duplicateOperations ?? 0,
|
|
5091
|
+
rejected: event.rejectedOperations ?? 0
|
|
5092
|
+
});
|
|
4948
5093
|
this.logger.log({
|
|
4949
5094
|
timestamp: Date.now(),
|
|
4950
5095
|
level: "info",
|
|
@@ -4997,7 +5142,9 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4997
5142
|
sessionId,
|
|
4998
5143
|
details: {
|
|
4999
5144
|
watermark: event.watermark,
|
|
5000
|
-
|
|
5145
|
+
outstandingMaxDeliverySequence: event.outstandingMaxDeliverySequence,
|
|
5146
|
+
repeatCount: event.repeatCount,
|
|
5147
|
+
reason: event.reason
|
|
5001
5148
|
}
|
|
5002
5149
|
});
|
|
5003
5150
|
});
|
|
@@ -5399,6 +5546,22 @@ function createProductionServer(config) {
|
|
|
5399
5546
|
"# TYPE kora_operations_received_total counter",
|
|
5400
5547
|
`kora_operations_received_total ${status.operationsReceived}`,
|
|
5401
5548
|
"",
|
|
5549
|
+
"# HELP kora_operation_batches_received_total Total operation batches received from clients",
|
|
5550
|
+
"# TYPE kora_operation_batches_received_total counter",
|
|
5551
|
+
`kora_operation_batches_received_total ${status.batchesReceived}`,
|
|
5552
|
+
"",
|
|
5553
|
+
"# HELP kora_unique_operations_received_total Newly materialized operations received from clients",
|
|
5554
|
+
"# TYPE kora_unique_operations_received_total counter",
|
|
5555
|
+
`kora_unique_operations_received_total ${status.uniqueOperationsReceived}`,
|
|
5556
|
+
"",
|
|
5557
|
+
"# HELP kora_duplicate_operations_received_total Duplicate operations received from clients",
|
|
5558
|
+
"# TYPE kora_duplicate_operations_received_total counter",
|
|
5559
|
+
`kora_duplicate_operations_received_total ${status.duplicateOperationsReceived}`,
|
|
5560
|
+
"",
|
|
5561
|
+
"# HELP kora_rejected_operations_total Operations rejected before authoritative materialization",
|
|
5562
|
+
"# TYPE kora_rejected_operations_total counter",
|
|
5563
|
+
`kora_rejected_operations_total ${status.rejectedOperations}`,
|
|
5564
|
+
"",
|
|
5402
5565
|
"# HELP kora_operations_sent_total Total operations sent to clients",
|
|
5403
5566
|
"# TYPE kora_operations_sent_total counter",
|
|
5404
5567
|
`kora_operations_sent_total ${status.operationsSent}`,
|