@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.js
CHANGED
|
@@ -2570,6 +2570,20 @@ function operationMatchesScopes(op, scopes, fullRecord) {
|
|
|
2570
2570
|
}
|
|
2571
2571
|
return true;
|
|
2572
2572
|
}
|
|
2573
|
+
function operationExitsScopes(op, scopes, resultingRecord) {
|
|
2574
|
+
if (!scopes || op.type !== "update" || !op.previousData) return false;
|
|
2575
|
+
if (operationMatchesScopes(op, scopes, resultingRecord)) return false;
|
|
2576
|
+
const previousSnapshot = {
|
|
2577
|
+
...resultingRecord ?? {},
|
|
2578
|
+
...op.data ?? {},
|
|
2579
|
+
...op.previousData
|
|
2580
|
+
};
|
|
2581
|
+
return operationMatchesScopes(
|
|
2582
|
+
{ ...op, type: "insert", data: previousSnapshot, previousData: null },
|
|
2583
|
+
scopes,
|
|
2584
|
+
previousSnapshot
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2573
2587
|
function matchesPredicate(actual, expected) {
|
|
2574
2588
|
if (expected && typeof expected === "object" && !Array.isArray(expected) && "$in" in expected) {
|
|
2575
2589
|
const values = expected.$in;
|
|
@@ -2701,6 +2715,7 @@ var ClientSession = class _ClientSession {
|
|
|
2701
2715
|
clientNodeId = null;
|
|
2702
2716
|
authContext = null;
|
|
2703
2717
|
syncQuerySubsets = [];
|
|
2718
|
+
scopeExitPolicy = "retain";
|
|
2704
2719
|
resumeDeltaCursor = null;
|
|
2705
2720
|
/**
|
|
2706
2721
|
* The delivery watermark the client reported at handshake, or null when the client
|
|
@@ -2725,7 +2740,7 @@ var ClientSession = class _ClientSession {
|
|
|
2725
2740
|
* batches every server tick while the first batch is still being applied.
|
|
2726
2741
|
*/
|
|
2727
2742
|
lastDeliveryPushAttemptAtMs = 0;
|
|
2728
|
-
|
|
2743
|
+
outstandingDelivery = null;
|
|
2729
2744
|
/** Serializes incremental delivery pushes so their batches never interleave. */
|
|
2730
2745
|
deliveryPushChain = Promise.resolve();
|
|
2731
2746
|
/**
|
|
@@ -2824,34 +2839,66 @@ var ClientSession = class _ClientSession {
|
|
|
2824
2839
|
* from the highest delivery sequence already sent. Pushes are serialized so their
|
|
2825
2840
|
* batches never interleave (which would break the base/max chain).
|
|
2826
2841
|
*/
|
|
2827
|
-
pushDeliveryStream() {
|
|
2842
|
+
pushDeliveryStream(options = {}) {
|
|
2828
2843
|
this.deliveryPushChain = this.deliveryPushChain.then(async () => {
|
|
2829
2844
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
2830
2845
|
try {
|
|
2846
|
+
const previous = this.outstandingDelivery;
|
|
2847
|
+
if (options.trackStall && previous && previous.max > this.lastAckedDeliverySeq) {
|
|
2848
|
+
previous.repeatCount += 1;
|
|
2849
|
+
if (previous.repeatCount >= 3) {
|
|
2850
|
+
this.emitter?.emit({
|
|
2851
|
+
type: "sync:delivery-stalled",
|
|
2852
|
+
sessionId: this.sessionId,
|
|
2853
|
+
watermark: this.lastAckedDeliverySeq,
|
|
2854
|
+
outstandingMaxDeliverySequence: previous.max,
|
|
2855
|
+
repeatCount: previous.repeatCount,
|
|
2856
|
+
reason: "unacknowledged-delivery"
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2831
2860
|
this.lastDeliveryPushAttemptAtMs = Date.now();
|
|
2832
|
-
await this.sendDeliveryStream(
|
|
2861
|
+
const result = await this.sendDeliveryStream(
|
|
2833
2862
|
this.lastAckedDeliverySeq,
|
|
2834
2863
|
false,
|
|
2835
2864
|
this.clientNodeId ?? void 0
|
|
2836
2865
|
);
|
|
2866
|
+
if (result.sent && result.maxScanned > this.lastAckedDeliverySeq) {
|
|
2867
|
+
if (!this.outstandingDelivery || this.outstandingDelivery.base !== this.lastAckedDeliverySeq || this.outstandingDelivery.max !== result.maxScanned) {
|
|
2868
|
+
this.outstandingDelivery = {
|
|
2869
|
+
base: this.lastAckedDeliverySeq,
|
|
2870
|
+
max: result.maxScanned,
|
|
2871
|
+
sentAtMs: Date.now(),
|
|
2872
|
+
repeatCount: 0
|
|
2873
|
+
};
|
|
2874
|
+
} else {
|
|
2875
|
+
this.outstandingDelivery.sentAtMs = Date.now();
|
|
2876
|
+
}
|
|
2877
|
+
} else if (result.maxScanned <= this.lastAckedDeliverySeq) {
|
|
2878
|
+
this.outstandingDelivery = null;
|
|
2879
|
+
}
|
|
2837
2880
|
} catch {
|
|
2838
2881
|
}
|
|
2839
2882
|
});
|
|
2840
2883
|
}
|
|
2841
2884
|
async relayVisibleOperations(operations2) {
|
|
2842
2885
|
const visibleOperations = [];
|
|
2886
|
+
const retractions = [];
|
|
2843
2887
|
for (const op of operations2) {
|
|
2844
2888
|
if (await this.operationVisibleToClient(op)) {
|
|
2845
2889
|
visibleOperations.push(op);
|
|
2890
|
+
} else if (await this.scopeRetractionFor(op)) {
|
|
2891
|
+
retractions.push({ collection: op.collection, recordId: op.recordId });
|
|
2846
2892
|
}
|
|
2847
2893
|
}
|
|
2848
|
-
if (visibleOperations.length === 0) return;
|
|
2894
|
+
if (visibleOperations.length === 0 && retractions.length === 0) return;
|
|
2849
2895
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
2850
2896
|
const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
|
|
2851
2897
|
const msg = {
|
|
2852
2898
|
type: "operation-batch",
|
|
2853
2899
|
messageId: generateUUIDv74(),
|
|
2854
2900
|
operations: serializedOps,
|
|
2901
|
+
...retractions.length > 0 ? { retractions } : {},
|
|
2855
2902
|
isFinal: true,
|
|
2856
2903
|
batchIndex: 0
|
|
2857
2904
|
};
|
|
@@ -2895,21 +2942,14 @@ var ClientSession = class _ClientSession {
|
|
|
2895
2942
|
pushDeliveryStreamIfSupported(staleMs = 0, options = {}) {
|
|
2896
2943
|
if (this.state !== "streaming" || !this.transport.isConnected()) return;
|
|
2897
2944
|
if (this.clientDeliveryWatermark === null) return;
|
|
2898
|
-
if (
|
|
2945
|
+
if (options.serverFrontier !== void 0 && options.serverFrontier <= this.lastAckedDeliverySeq) {
|
|
2946
|
+
this.outstandingDelivery = null;
|
|
2899
2947
|
return;
|
|
2900
2948
|
}
|
|
2901
|
-
if (
|
|
2902
|
-
|
|
2903
|
-
if (this.deliveryStallRepeatCount >= 3) {
|
|
2904
|
-
this.emitter?.emit({
|
|
2905
|
-
type: "sync:delivery-stalled",
|
|
2906
|
-
sessionId: this.sessionId,
|
|
2907
|
-
watermark: this.lastAckedDeliverySeq,
|
|
2908
|
-
repeatCount: this.deliveryStallRepeatCount
|
|
2909
|
-
});
|
|
2910
|
-
}
|
|
2949
|
+
if (staleMs > 0 && Date.now() - this.lastDeliveryPushAttemptAtMs < staleMs) {
|
|
2950
|
+
return;
|
|
2911
2951
|
}
|
|
2912
|
-
this.pushDeliveryStream();
|
|
2952
|
+
this.pushDeliveryStream({ trackStall: options.trackStall });
|
|
2913
2953
|
}
|
|
2914
2954
|
/**
|
|
2915
2955
|
* Close this session.
|
|
@@ -2992,7 +3032,11 @@ var ClientSession = class _ClientSession {
|
|
|
2992
3032
|
this.pendingRelays.delete(message.acknowledgedMessageId);
|
|
2993
3033
|
if (message.deliverySequence !== void 0) {
|
|
2994
3034
|
this.lastAckedDeliverySeq = Math.max(this.lastAckedDeliverySeq, message.deliverySequence);
|
|
2995
|
-
this.
|
|
3035
|
+
if (this.outstandingDelivery && this.lastAckedDeliverySeq >= this.outstandingDelivery.max) {
|
|
3036
|
+
this.outstandingDelivery = null;
|
|
3037
|
+
} else if (this.outstandingDelivery) {
|
|
3038
|
+
this.outstandingDelivery.repeatCount = 0;
|
|
3039
|
+
}
|
|
2996
3040
|
}
|
|
2997
3041
|
break;
|
|
2998
3042
|
case "error":
|
|
@@ -3053,6 +3097,7 @@ var ClientSession = class _ClientSession {
|
|
|
3053
3097
|
return;
|
|
3054
3098
|
}
|
|
3055
3099
|
this.clientNodeId = msg.nodeId;
|
|
3100
|
+
this.scopeExitPolicy = msg.scopeExitPolicy ?? "retain";
|
|
3056
3101
|
if (this.auth) {
|
|
3057
3102
|
const token = msg.authToken ?? "";
|
|
3058
3103
|
const context = await this.auth.authenticate(token);
|
|
@@ -3064,13 +3109,19 @@ var ClientSession = class _ClientSession {
|
|
|
3064
3109
|
this.authContext = context;
|
|
3065
3110
|
this.state = "authenticated";
|
|
3066
3111
|
}
|
|
3067
|
-
const
|
|
3112
|
+
const directionalScopesConfigured = this.authContext?.downlinkScopes !== void 0 || this.authContext?.uplinkScopes !== void 0;
|
|
3113
|
+
const downlinkAuthScopes = directionalScopesConfigured ? this.authContext?.downlinkScopes ?? this.authContext?.scopes ?? {} : this.authContext?.scopes;
|
|
3114
|
+
const uplinkAuthScopes = directionalScopesConfigured ? this.authContext?.uplinkScopes ?? this.authContext?.scopes ?? {} : this.authContext?.scopes;
|
|
3115
|
+
const rawResolvedDownlinkScopes = resolveSessionScopes(this.store.getSchema(), {
|
|
3068
3116
|
handshakeScope: msg.syncScope,
|
|
3069
|
-
authScopes:
|
|
3117
|
+
authScopes: downlinkAuthScopes
|
|
3070
3118
|
});
|
|
3071
|
-
|
|
3119
|
+
const rawResolvedUplinkScopes = directionalScopesConfigured ? uplinkAuthScopes : rawResolvedDownlinkScopes;
|
|
3120
|
+
let resolvedDownlinkScopes;
|
|
3121
|
+
let resolvedUplinkScopes;
|
|
3072
3122
|
try {
|
|
3073
|
-
|
|
3123
|
+
resolvedDownlinkScopes = rawResolvedDownlinkScopes ? normalizeScopeMap(rawResolvedDownlinkScopes) : directionalScopesConfigured ? {} : void 0;
|
|
3124
|
+
resolvedUplinkScopes = rawResolvedUplinkScopes ? normalizeScopeMap(rawResolvedUplinkScopes) : directionalScopesConfigured ? {} : void 0;
|
|
3074
3125
|
} catch (error) {
|
|
3075
3126
|
this.sendToClient({
|
|
3076
3127
|
type: "error",
|
|
@@ -3082,14 +3133,24 @@ var ClientSession = class _ClientSession {
|
|
|
3082
3133
|
this.close("invalid scope predicate");
|
|
3083
3134
|
return;
|
|
3084
3135
|
}
|
|
3085
|
-
if (
|
|
3136
|
+
if (resolvedDownlinkScopes || resolvedUplinkScopes) {
|
|
3086
3137
|
if (this.authContext) {
|
|
3087
|
-
this.authContext = {
|
|
3138
|
+
this.authContext = {
|
|
3139
|
+
...this.authContext,
|
|
3140
|
+
scopes: resolvedDownlinkScopes,
|
|
3141
|
+
downlinkScopes: resolvedDownlinkScopes,
|
|
3142
|
+
uplinkScopes: resolvedUplinkScopes
|
|
3143
|
+
};
|
|
3088
3144
|
} else {
|
|
3089
|
-
this.authContext = {
|
|
3145
|
+
this.authContext = {
|
|
3146
|
+
userId: msg.nodeId,
|
|
3147
|
+
scopes: resolvedDownlinkScopes,
|
|
3148
|
+
downlinkScopes: resolvedDownlinkScopes,
|
|
3149
|
+
uplinkScopes: resolvedUplinkScopes
|
|
3150
|
+
};
|
|
3090
3151
|
}
|
|
3091
3152
|
}
|
|
3092
|
-
warnIfMultiTenantWithoutScopes(this.auth,
|
|
3153
|
+
warnIfMultiTenantWithoutScopes(this.auth, resolvedDownlinkScopes, this.store.getSchema());
|
|
3093
3154
|
if (msg.syncQueries && msg.syncQueries.length > 0) {
|
|
3094
3155
|
this.syncQuerySubsets = dedupeQuerySubsets(msg.syncQueries);
|
|
3095
3156
|
} else {
|
|
@@ -3097,7 +3158,7 @@ var ClientSession = class _ClientSession {
|
|
|
3097
3158
|
}
|
|
3098
3159
|
this.resumeDeltaCursor = msg.deltaCursor ? decodeDeltaCursor(msg.deltaCursor) : null;
|
|
3099
3160
|
this.clientDeliveryWatermark = msg.lastDeliverySequence ?? null;
|
|
3100
|
-
if (this.clientDeliveryWatermark !== null && !sameScopeMap(msg.syncScope, this.authContext?.
|
|
3161
|
+
if (this.clientDeliveryWatermark !== null && !sameScopeMap(msg.syncScope, this.authContext?.downlinkScopes)) {
|
|
3101
3162
|
this.clientDeliveryWatermark = 0;
|
|
3102
3163
|
}
|
|
3103
3164
|
const clientUsesWatermark = this.clientDeliveryWatermark !== null;
|
|
@@ -3155,7 +3216,11 @@ var ClientSession = class _ClientSession {
|
|
|
3155
3216
|
...this.persistBlobChunk ? { blobStorageEnabled: true } : {},
|
|
3156
3217
|
// Confirm the accepted scope so the client knows what data will be synced.
|
|
3157
3218
|
// This may differ from what the client requested if auth scopes are narrower.
|
|
3158
|
-
...this.authContext?.
|
|
3219
|
+
...this.authContext?.downlinkScopes ? {
|
|
3220
|
+
acceptedScope: this.authContext.downlinkScopes,
|
|
3221
|
+
acceptedDownlinkScopes: this.authContext.downlinkScopes
|
|
3222
|
+
} : {},
|
|
3223
|
+
...this.authContext?.uplinkScopes ? { acceptedUplinkScopes: this.authContext.uplinkScopes } : {}
|
|
3159
3224
|
};
|
|
3160
3225
|
this.sendToClient(response);
|
|
3161
3226
|
this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
|
|
@@ -3182,18 +3247,22 @@ var ClientSession = class _ClientSession {
|
|
|
3182
3247
|
const applied = [];
|
|
3183
3248
|
let acknowledgedThrough = 0;
|
|
3184
3249
|
let canAdvanceAck = true;
|
|
3250
|
+
let uniqueOperations = 0;
|
|
3251
|
+
let duplicateOperations = 0;
|
|
3252
|
+
let rejectedOperations = 0;
|
|
3185
3253
|
for (const op of operations2) {
|
|
3186
3254
|
if (!canAdvanceAck) {
|
|
3187
3255
|
continue;
|
|
3188
3256
|
}
|
|
3189
|
-
if (!await this.
|
|
3257
|
+
if (!await this.operationAllowedFromClient(op)) {
|
|
3190
3258
|
this.sendOperationRejected(
|
|
3191
3259
|
op,
|
|
3192
3260
|
"SCOPE_VIOLATION",
|
|
3193
|
-
`Operation "${op.id}" in collection "${op.collection}" is outside the
|
|
3194
|
-
|
|
3261
|
+
`Operation "${op.id}" in collection "${op.collection}" is outside the accepted uplink scope. Refresh scopes before creating or explicitly resubmitting an authorized operation.`,
|
|
3262
|
+
false
|
|
3195
3263
|
);
|
|
3196
|
-
|
|
3264
|
+
rejectedOperations += 1;
|
|
3265
|
+
acknowledgedThrough = op.sequenceNumber;
|
|
3197
3266
|
continue;
|
|
3198
3267
|
}
|
|
3199
3268
|
if (!isOperationTimestampValid(op)) {
|
|
@@ -3232,6 +3301,7 @@ var ClientSession = class _ClientSession {
|
|
|
3232
3301
|
`Operation "${op.id}" cannot be transformed from schema v${op.schemaVersion} to server schema v${this.schemaVersion}.`,
|
|
3233
3302
|
false
|
|
3234
3303
|
);
|
|
3304
|
+
rejectedOperations += 1;
|
|
3235
3305
|
acknowledgedThrough = op.sequenceNumber;
|
|
3236
3306
|
continue;
|
|
3237
3307
|
}
|
|
@@ -3256,6 +3326,7 @@ var ClientSession = class _ClientSession {
|
|
|
3256
3326
|
if (decision.action === "reject") {
|
|
3257
3327
|
const retriable = decision.retriable ?? isRetriableRejection(decision.code);
|
|
3258
3328
|
this.sendOperationRejected(serverOp, decision.code, decision.message, retriable);
|
|
3329
|
+
rejectedOperations += 1;
|
|
3259
3330
|
if (retriable) {
|
|
3260
3331
|
canAdvanceAck = false;
|
|
3261
3332
|
} else {
|
|
@@ -3276,6 +3347,7 @@ var ClientSession = class _ClientSession {
|
|
|
3276
3347
|
applyResult.rejection.message,
|
|
3277
3348
|
applyResult.rejection.retriable
|
|
3278
3349
|
);
|
|
3350
|
+
rejectedOperations += 1;
|
|
3279
3351
|
if (applyResult.rejection.retriable) {
|
|
3280
3352
|
canAdvanceAck = false;
|
|
3281
3353
|
} else {
|
|
@@ -3285,8 +3357,10 @@ var ClientSession = class _ClientSession {
|
|
|
3285
3357
|
}
|
|
3286
3358
|
if (applyResult.result === "applied") {
|
|
3287
3359
|
applied.push(...applyResult.appliedOperations);
|
|
3360
|
+
uniqueOperations += 1;
|
|
3288
3361
|
acknowledgedThrough = op.sequenceNumber;
|
|
3289
3362
|
} else {
|
|
3363
|
+
duplicateOperations += 1;
|
|
3290
3364
|
acknowledgedThrough = op.sequenceNumber;
|
|
3291
3365
|
}
|
|
3292
3366
|
}
|
|
@@ -3294,7 +3368,10 @@ var ClientSession = class _ClientSession {
|
|
|
3294
3368
|
this.emitter?.emit({
|
|
3295
3369
|
type: "sync:received",
|
|
3296
3370
|
operations: operations2,
|
|
3297
|
-
batchSize: operations2.length
|
|
3371
|
+
batchSize: operations2.length,
|
|
3372
|
+
uniqueOperations,
|
|
3373
|
+
duplicateOperations,
|
|
3374
|
+
rejectedOperations
|
|
3298
3375
|
});
|
|
3299
3376
|
}
|
|
3300
3377
|
const lastOp = operations2[operations2.length - 1];
|
|
@@ -3399,7 +3476,7 @@ var ClientSession = class _ClientSession {
|
|
|
3399
3476
|
const scanChunk = Math.max(this.batchSize, 1) * 5;
|
|
3400
3477
|
let scanCursor = fromDeliverySeq;
|
|
3401
3478
|
let maxScanned = fromDeliverySeq;
|
|
3402
|
-
const
|
|
3479
|
+
const deliverable = [];
|
|
3403
3480
|
while (true) {
|
|
3404
3481
|
const chunk = await this.store.getOperationsAfterDelivery(scanCursor, scanChunk);
|
|
3405
3482
|
const last = chunk[chunk.length - 1];
|
|
@@ -3411,21 +3488,24 @@ var ClientSession = class _ClientSession {
|
|
|
3411
3488
|
continue;
|
|
3412
3489
|
}
|
|
3413
3490
|
if (await this.operationVisibleToClient(delivered.operation)) {
|
|
3414
|
-
|
|
3491
|
+
deliverable.push(delivered);
|
|
3492
|
+
} else if (await this.scopeRetractionFor(delivered.operation)) {
|
|
3493
|
+
deliverable.push({ ...delivered, retraction: true });
|
|
3415
3494
|
}
|
|
3416
3495
|
}
|
|
3417
3496
|
if (chunk.length < scanChunk) break;
|
|
3418
3497
|
}
|
|
3419
|
-
if (
|
|
3420
|
-
|
|
3421
|
-
|
|
3498
|
+
if (deliverable.length === 0) {
|
|
3499
|
+
let sent = false;
|
|
3500
|
+
if (finalizeWhenEmpty || maxScanned > fromDeliverySeq) {
|
|
3501
|
+
sent = this.sendDeliveryBatch([], fromDeliverySeq, maxScanned, 0, true);
|
|
3422
3502
|
}
|
|
3423
|
-
return 0;
|
|
3503
|
+
return { sentOperations: 0, maxScanned, sent };
|
|
3424
3504
|
}
|
|
3425
|
-
const totalBatches = Math.ceil(
|
|
3505
|
+
const totalBatches = Math.ceil(deliverable.length / this.batchSize);
|
|
3426
3506
|
let base = fromDeliverySeq;
|
|
3427
3507
|
for (let i = 0; i < totalBatches; i++) {
|
|
3428
|
-
const slice =
|
|
3508
|
+
const slice = deliverable.slice(i * this.batchSize, (i + 1) * this.batchSize);
|
|
3429
3509
|
const lastInSlice = slice[slice.length - 1];
|
|
3430
3510
|
if (lastInSlice === void 0) continue;
|
|
3431
3511
|
const isFinal = i === totalBatches - 1;
|
|
@@ -3434,30 +3514,42 @@ var ClientSession = class _ClientSession {
|
|
|
3434
3514
|
this.sendDeliveryBatch(slice, base, max, i, isFinal);
|
|
3435
3515
|
base = max;
|
|
3436
3516
|
}
|
|
3437
|
-
return
|
|
3517
|
+
return {
|
|
3518
|
+
sentOperations: deliverable.filter((item) => !item.retraction).length,
|
|
3519
|
+
maxScanned,
|
|
3520
|
+
sent: true
|
|
3521
|
+
};
|
|
3438
3522
|
}
|
|
3439
3523
|
/** Build, track, and send one chained delivery-stream batch. */
|
|
3440
3524
|
sendDeliveryBatch(slice, base, max, batchIndex, isFinal) {
|
|
3441
3525
|
const batchMsg = {
|
|
3442
3526
|
type: "operation-batch",
|
|
3443
3527
|
messageId: generateUUIDv74(),
|
|
3444
|
-
operations: slice.map((delivered) => this.serializer.encodeOperation(delivered.operation)),
|
|
3528
|
+
operations: slice.filter((delivered) => !delivered.retraction).map((delivered) => this.serializer.encodeOperation(delivered.operation)),
|
|
3529
|
+
...slice.some((delivered) => delivered.retraction) ? {
|
|
3530
|
+
retractions: slice.filter((delivered) => delivered.retraction).map((delivered) => ({
|
|
3531
|
+
collection: delivered.operation.collection,
|
|
3532
|
+
recordId: delivered.operation.recordId
|
|
3533
|
+
}))
|
|
3534
|
+
} : {},
|
|
3445
3535
|
isFinal,
|
|
3446
3536
|
batchIndex,
|
|
3447
3537
|
baseDeliverySequence: base,
|
|
3448
3538
|
maxDeliverySequence: max
|
|
3449
3539
|
};
|
|
3450
|
-
this.sendToClient(batchMsg);
|
|
3451
|
-
|
|
3540
|
+
const sent = this.sendToClient(batchMsg);
|
|
3541
|
+
const sentOperations = slice.filter((delivered) => !delivered.retraction);
|
|
3542
|
+
if (sentOperations.length > 0) {
|
|
3452
3543
|
this.emitter?.emit({
|
|
3453
3544
|
type: "sync:sent",
|
|
3454
|
-
operations:
|
|
3455
|
-
batchSize:
|
|
3545
|
+
operations: sentOperations.map((delivered) => delivered.operation),
|
|
3546
|
+
batchSize: sentOperations.length
|
|
3456
3547
|
});
|
|
3457
3548
|
}
|
|
3549
|
+
return sent;
|
|
3458
3550
|
}
|
|
3459
3551
|
async operationVisibleToClient(op) {
|
|
3460
|
-
const scopes = this.authContext?.scopes;
|
|
3552
|
+
const scopes = this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
3461
3553
|
const subsets = this.syncQuerySubsets;
|
|
3462
3554
|
const needsBackfill = missingScopeFields(op, scopes).length > 0 || subsets !== void 0 && subsets.length > 0;
|
|
3463
3555
|
const fullRecord = needsBackfill ? await this.lookupRecordFields(op.collection, op.recordId) : void 0;
|
|
@@ -3466,6 +3558,20 @@ var ClientSession = class _ClientSession {
|
|
|
3466
3558
|
}
|
|
3467
3559
|
return operationMatchesQuerySubsets(op, subsets, fullRecord);
|
|
3468
3560
|
}
|
|
3561
|
+
/** Upload authorization is independent from the client's downloaded/query view. */
|
|
3562
|
+
async operationAllowedFromClient(op) {
|
|
3563
|
+
const scopes = this.authContext?.uplinkScopes ?? this.authContext?.scopes;
|
|
3564
|
+
const needsBackfill = missingScopeFields(op, scopes).length > 0;
|
|
3565
|
+
const fullRecord = needsBackfill ? await this.lookupRecordFields(op.collection, op.recordId) : void 0;
|
|
3566
|
+
return operationMatchesScopes(op, scopes, fullRecord);
|
|
3567
|
+
}
|
|
3568
|
+
async scopeRetractionFor(op) {
|
|
3569
|
+
if (this.scopeExitPolicy !== "retract") return false;
|
|
3570
|
+
const scopes = this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
3571
|
+
if (!scopes) return false;
|
|
3572
|
+
const resultingRecord = await this.lookupRecordFields(op.collection, op.recordId);
|
|
3573
|
+
return operationExitsScopes(op, scopes, resultingRecord);
|
|
3574
|
+
}
|
|
3469
3575
|
/**
|
|
3470
3576
|
* Read a record's current field values from the materialized store for scope /
|
|
3471
3577
|
* query-subset backfill. Includes soft-deleted rows so a relayed delete (whose op
|
|
@@ -3654,6 +3760,10 @@ var ServerMetricsCollector = class {
|
|
|
3654
3760
|
peakConnections = 0;
|
|
3655
3761
|
connectionsTotal = 0;
|
|
3656
3762
|
operationsReceived = 0;
|
|
3763
|
+
batchesReceived = 0;
|
|
3764
|
+
uniqueOperationsReceived = 0;
|
|
3765
|
+
duplicateOperationsReceived = 0;
|
|
3766
|
+
rejectedOperations = 0;
|
|
3657
3767
|
operationsSent = 0;
|
|
3658
3768
|
bytesReceived = 0;
|
|
3659
3769
|
bytesSent = 0;
|
|
@@ -3669,6 +3779,10 @@ var ServerMetricsCollector = class {
|
|
|
3669
3779
|
state: "connected",
|
|
3670
3780
|
connectedAt: Date.now(),
|
|
3671
3781
|
operationsReceived: 0,
|
|
3782
|
+
batchesReceived: 0,
|
|
3783
|
+
uniqueOperationsReceived: 0,
|
|
3784
|
+
duplicateOperationsReceived: 0,
|
|
3785
|
+
rejectedOperations: 0,
|
|
3672
3786
|
operationsSent: 0,
|
|
3673
3787
|
authContext: null
|
|
3674
3788
|
});
|
|
@@ -3700,12 +3814,24 @@ var ServerMetricsCollector = class {
|
|
|
3700
3814
|
}
|
|
3701
3815
|
}
|
|
3702
3816
|
/** Record operations received from a client. */
|
|
3703
|
-
recordReceived(sessionId, count3, byteSize
|
|
3817
|
+
recordReceived(sessionId, count3, byteSize, outcomes = {
|
|
3818
|
+
unique: count3,
|
|
3819
|
+
duplicates: 0,
|
|
3820
|
+
rejected: 0
|
|
3821
|
+
}) {
|
|
3704
3822
|
this.operationsReceived += count3;
|
|
3823
|
+
this.batchesReceived += 1;
|
|
3824
|
+
this.uniqueOperationsReceived += outcomes.unique;
|
|
3825
|
+
this.duplicateOperationsReceived += outcomes.duplicates;
|
|
3826
|
+
this.rejectedOperations += outcomes.rejected;
|
|
3705
3827
|
this.bytesReceived += byteSize;
|
|
3706
3828
|
const client = this.clientMetrics.get(sessionId);
|
|
3707
3829
|
if (client) {
|
|
3708
3830
|
client.operationsReceived += count3;
|
|
3831
|
+
client.batchesReceived += 1;
|
|
3832
|
+
client.uniqueOperationsReceived += outcomes.unique;
|
|
3833
|
+
client.duplicateOperationsReceived += outcomes.duplicates;
|
|
3834
|
+
client.rejectedOperations += outcomes.rejected;
|
|
3709
3835
|
}
|
|
3710
3836
|
}
|
|
3711
3837
|
/** Record operations sent to a client. */
|
|
@@ -3733,6 +3859,10 @@ var ServerMetricsCollector = class {
|
|
|
3733
3859
|
peakConnections: this.peakConnections,
|
|
3734
3860
|
connectionsTotal: this.connectionsTotal,
|
|
3735
3861
|
operationsReceived: this.operationsReceived,
|
|
3862
|
+
batchesReceived: this.batchesReceived,
|
|
3863
|
+
uniqueOperationsReceived: this.uniqueOperationsReceived,
|
|
3864
|
+
duplicateOperationsReceived: this.duplicateOperationsReceived,
|
|
3865
|
+
rejectedOperations: this.rejectedOperations,
|
|
3736
3866
|
operationsSent: this.operationsSent,
|
|
3737
3867
|
bytesReceived: this.bytesReceived,
|
|
3738
3868
|
bytesSent: this.bytesSent,
|
|
@@ -3749,6 +3879,10 @@ var ServerMetricsCollector = class {
|
|
|
3749
3879
|
this.peakConnections = 0;
|
|
3750
3880
|
this.connectionsTotal = 0;
|
|
3751
3881
|
this.operationsReceived = 0;
|
|
3882
|
+
this.batchesReceived = 0;
|
|
3883
|
+
this.uniqueOperationsReceived = 0;
|
|
3884
|
+
this.duplicateOperationsReceived = 0;
|
|
3885
|
+
this.rejectedOperations = 0;
|
|
3752
3886
|
this.operationsSent = 0;
|
|
3753
3887
|
this.bytesReceived = 0;
|
|
3754
3888
|
this.bytesSent = 0;
|
|
@@ -4435,14 +4569,17 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4435
4569
|
if (maxDeliverySequence <= this.lastObservedDeliverySequence) {
|
|
4436
4570
|
for (const session of this.sessions.values()) {
|
|
4437
4571
|
session.pushDeliveryStreamIfSupported(this.deliveryPollIntervalMs, {
|
|
4438
|
-
trackStall: true
|
|
4572
|
+
trackStall: true,
|
|
4573
|
+
serverFrontier: maxDeliverySequence
|
|
4439
4574
|
});
|
|
4440
4575
|
}
|
|
4441
4576
|
return;
|
|
4442
4577
|
}
|
|
4443
4578
|
this.lastObservedDeliverySequence = maxDeliverySequence;
|
|
4444
4579
|
for (const session of this.sessions.values()) {
|
|
4445
|
-
session.pushDeliveryStreamIfSupported(0
|
|
4580
|
+
session.pushDeliveryStreamIfSupported(0, {
|
|
4581
|
+
serverFrontier: maxDeliverySequence
|
|
4582
|
+
});
|
|
4446
4583
|
}
|
|
4447
4584
|
} catch (error) {
|
|
4448
4585
|
this.logger.log({
|
|
@@ -4516,7 +4653,11 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4516
4653
|
});
|
|
4517
4654
|
sessionEmitter.on("sync:received", (event) => {
|
|
4518
4655
|
const byteSize = estimateByteSize(event.operations);
|
|
4519
|
-
this.metrics.recordReceived(sessionId, event.batchSize, byteSize
|
|
4656
|
+
this.metrics.recordReceived(sessionId, event.batchSize, byteSize, {
|
|
4657
|
+
unique: event.uniqueOperations ?? event.batchSize,
|
|
4658
|
+
duplicates: event.duplicateOperations ?? 0,
|
|
4659
|
+
rejected: event.rejectedOperations ?? 0
|
|
4660
|
+
});
|
|
4520
4661
|
this.logger.log({
|
|
4521
4662
|
timestamp: Date.now(),
|
|
4522
4663
|
level: "info",
|
|
@@ -4739,7 +4880,11 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4739
4880
|
});
|
|
4740
4881
|
sessionEmitter.on("sync:received", (event) => {
|
|
4741
4882
|
const byteSize = estimateOperationByteSize(event.operations);
|
|
4742
|
-
this.metrics.recordReceived(sessionId, event.batchSize, byteSize
|
|
4883
|
+
this.metrics.recordReceived(sessionId, event.batchSize, byteSize, {
|
|
4884
|
+
unique: event.uniqueOperations ?? event.batchSize,
|
|
4885
|
+
duplicates: event.duplicateOperations ?? 0,
|
|
4886
|
+
rejected: event.rejectedOperations ?? 0
|
|
4887
|
+
});
|
|
4743
4888
|
this.logger.log({
|
|
4744
4889
|
timestamp: Date.now(),
|
|
4745
4890
|
level: "info",
|
|
@@ -4792,7 +4937,9 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
4792
4937
|
sessionId,
|
|
4793
4938
|
details: {
|
|
4794
4939
|
watermark: event.watermark,
|
|
4795
|
-
|
|
4940
|
+
outstandingMaxDeliverySequence: event.outstandingMaxDeliverySequence,
|
|
4941
|
+
repeatCount: event.repeatCount,
|
|
4942
|
+
reason: event.reason
|
|
4796
4943
|
}
|
|
4797
4944
|
});
|
|
4798
4945
|
});
|
|
@@ -5189,6 +5336,22 @@ function createProductionServer(config) {
|
|
|
5189
5336
|
"# TYPE kora_operations_received_total counter",
|
|
5190
5337
|
`kora_operations_received_total ${status.operationsReceived}`,
|
|
5191
5338
|
"",
|
|
5339
|
+
"# HELP kora_operation_batches_received_total Total operation batches received from clients",
|
|
5340
|
+
"# TYPE kora_operation_batches_received_total counter",
|
|
5341
|
+
`kora_operation_batches_received_total ${status.batchesReceived}`,
|
|
5342
|
+
"",
|
|
5343
|
+
"# HELP kora_unique_operations_received_total Newly materialized operations received from clients",
|
|
5344
|
+
"# TYPE kora_unique_operations_received_total counter",
|
|
5345
|
+
`kora_unique_operations_received_total ${status.uniqueOperationsReceived}`,
|
|
5346
|
+
"",
|
|
5347
|
+
"# HELP kora_duplicate_operations_received_total Duplicate operations received from clients",
|
|
5348
|
+
"# TYPE kora_duplicate_operations_received_total counter",
|
|
5349
|
+
`kora_duplicate_operations_received_total ${status.duplicateOperationsReceived}`,
|
|
5350
|
+
"",
|
|
5351
|
+
"# HELP kora_rejected_operations_total Operations rejected before authoritative materialization",
|
|
5352
|
+
"# TYPE kora_rejected_operations_total counter",
|
|
5353
|
+
`kora_rejected_operations_total ${status.rejectedOperations}`,
|
|
5354
|
+
"",
|
|
5192
5355
|
"# HELP kora_operations_sent_total Total operations sent to clients",
|
|
5193
5356
|
"# TYPE kora_operations_sent_total counter",
|
|
5194
5357
|
`kora_operations_sent_total ${status.operationsSent}`,
|