@korajs/sync 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 +164 -12
- 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 +164 -12
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{transport-iXX0KA_W.d.cts → transport-CJvRO5ix.d.cts} +18 -0
- package/dist/{transport-iXX0KA_W.d.ts → transport-CJvRO5ix.d.ts} +18 -0
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-
|
|
2
|
-
export { A as AcknowledgmentMessage, p as ActiveApplyFailure, q as AwarenessStateWire, r as AwarenessUpdateMessage, E as EncryptedPayload, s as ErrorMessage, H as HandshakeMessage, t as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, u as OperationRejectedMessage, v as SYNC_STATES, w as SYNC_STATUSES, x as ScopeViolationError, y as SyncEncryptionAlgorithm, z as SyncPhase, C as SyncScopeContext, F as SyncSettlementOptions, G as SyncSettlementResult, J as SyncStatus, K as dedupeQuerySubsets, L as isAcknowledgmentMessage, N as isAwarenessUpdateMessage, P as isBlobChunkPushMessage, U as isBlobChunkRequestMessage, X as isBlobChunkResponseMessage, Z as isErrorMessage, _ as isHandshakeMessage, $ as isHandshakeResponseMessage, a0 as isOperationBatchMessage, a1 as isOperationRejectedMessage, a2 as isSyncMessage, a3 as isYjsDocUpdateMessage, a4 as operationMatchesQuerySubsets, a5 as querySubsetContains } from './transport-
|
|
1
|
+
import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-CJvRO5ix.cjs';
|
|
2
|
+
export { A as AcknowledgmentMessage, p as ActiveApplyFailure, q as AwarenessStateWire, r as AwarenessUpdateMessage, E as EncryptedPayload, s as ErrorMessage, H as HandshakeMessage, t as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, u as OperationRejectedMessage, v as SYNC_STATES, w as SYNC_STATUSES, x as ScopeViolationError, y as SyncEncryptionAlgorithm, z as SyncPhase, C as SyncScopeContext, F as SyncSettlementOptions, G as SyncSettlementResult, J as SyncStatus, K as dedupeQuerySubsets, L as isAcknowledgmentMessage, N as isAwarenessUpdateMessage, P as isBlobChunkPushMessage, U as isBlobChunkRequestMessage, X as isBlobChunkResponseMessage, Z as isErrorMessage, _ as isHandshakeMessage, $ as isHandshakeResponseMessage, a0 as isOperationBatchMessage, a1 as isOperationRejectedMessage, a2 as isSyncMessage, a3 as isYjsDocUpdateMessage, a4 as operationMatchesQuerySubsets, a5 as querySubsetContains } from './transport-CJvRO5ix.cjs';
|
|
3
3
|
import * as _korajs_core from '@korajs/core';
|
|
4
4
|
import { Operation, VersionVector, ApplyResult, KoraEventEmitter, TimeSource, SyncError, ConnectionQuality } from '@korajs/core';
|
|
5
5
|
export { ApplyFailureReason, ApplyResult } from '@korajs/core';
|
|
@@ -106,6 +106,16 @@ interface SyncStore {
|
|
|
106
106
|
* @returns the record's fields, or null when it cannot be read.
|
|
107
107
|
*/
|
|
108
108
|
readRecordFields?(collection: string, recordId: string): Promise<Record<string, unknown> | null>;
|
|
109
|
+
/**
|
|
110
|
+
* Remove a record from this client's materialized authorization view without
|
|
111
|
+
* writing a domain delete to the replicated operation log.
|
|
112
|
+
*/
|
|
113
|
+
applyScopeRetraction?(collection: string, recordId: string): Promise<void>;
|
|
114
|
+
/** Retract every currently materialized row outside a newly accepted scope. */
|
|
115
|
+
applyScopeNarrowing?(scopes: Record<string, Record<string, unknown>>): Promise<Array<{
|
|
116
|
+
collection: string;
|
|
117
|
+
recordId: string;
|
|
118
|
+
}>>;
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
/** Prefix for handshake rejection when client schema version is unsupported. */
|
|
@@ -879,6 +889,8 @@ declare class OutboundQueue {
|
|
|
879
889
|
* or null if it was not present (already acknowledged and removed).
|
|
880
890
|
*/
|
|
881
891
|
reject(opId: string): Promise<Operation | null>;
|
|
892
|
+
/** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
|
|
893
|
+
rejectRecord(collection: string, recordId: string): Promise<Operation[]>;
|
|
882
894
|
/**
|
|
883
895
|
* Remove operations by id from queue and persistent storage.
|
|
884
896
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -956,6 +968,8 @@ declare class SyncEngine {
|
|
|
956
968
|
private readonly rejectedStorage;
|
|
957
969
|
private readonly batchSize;
|
|
958
970
|
private readonly outboundAckTimeoutMs;
|
|
971
|
+
private readonly outboundRetryBaseDelayMs;
|
|
972
|
+
private readonly outboundRetryMaxDelayMs;
|
|
959
973
|
private readonly encryptor;
|
|
960
974
|
private readonly awarenessManager;
|
|
961
975
|
private readonly richtextDocChannel;
|
|
@@ -971,6 +985,9 @@ declare class SyncEngine {
|
|
|
971
985
|
private conflictCount;
|
|
972
986
|
private currentBatch;
|
|
973
987
|
private currentBatchRequiresPartialAck;
|
|
988
|
+
private currentBatchRequiresRetryBackoff;
|
|
989
|
+
private outboundRetryAttempt;
|
|
990
|
+
private outboundRetryTimer;
|
|
974
991
|
private reconnecting;
|
|
975
992
|
private schemaBlocked;
|
|
976
993
|
private clockBlocked;
|
|
@@ -998,6 +1015,9 @@ declare class SyncEngine {
|
|
|
998
1015
|
* with the server-accepted scope (server is authoritative).
|
|
999
1016
|
*/
|
|
1000
1017
|
private activeScope;
|
|
1018
|
+
/** Server-authoritative upload authorization, separate from the downloaded view. */
|
|
1019
|
+
private activeUplinkScope;
|
|
1020
|
+
private hasDirectionalScopes;
|
|
1001
1021
|
/** Live query subsets registered from reactive subscriptions */
|
|
1002
1022
|
private querySubsets;
|
|
1003
1023
|
private staticQuerySubsets;
|
|
@@ -1169,6 +1189,8 @@ declare class SyncEngine {
|
|
|
1169
1189
|
* Get the currently active scope map. Returns undefined if no scope is configured.
|
|
1170
1190
|
*/
|
|
1171
1191
|
getActiveScope(): SyncScopeMap | undefined;
|
|
1192
|
+
/** Get the server-accepted upload authorization scope. */
|
|
1193
|
+
getActiveUplinkScope(): SyncScopeMap | undefined;
|
|
1172
1194
|
/**
|
|
1173
1195
|
* Register a live query subset that narrows synced data for a collection.
|
|
1174
1196
|
* Takes effect on the next connection; reconnects when already connected.
|
|
@@ -1242,6 +1264,7 @@ declare class SyncEngine {
|
|
|
1242
1264
|
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
1243
1265
|
* signal, so the connection stays up.
|
|
1244
1266
|
*/
|
|
1267
|
+
private applyScopeRetraction;
|
|
1245
1268
|
private handleOperationRejected;
|
|
1246
1269
|
/**
|
|
1247
1270
|
* Every operation the server rejected that has not yet been reconciled. The app
|
|
@@ -1276,6 +1299,8 @@ declare class SyncEngine {
|
|
|
1276
1299
|
private flushQueue;
|
|
1277
1300
|
private startOutboundAckTimer;
|
|
1278
1301
|
private clearOutboundAckTimer;
|
|
1302
|
+
private scheduleOutboundRetry;
|
|
1303
|
+
private clearOutboundRetryTimer;
|
|
1279
1304
|
private handleOutboundAckTimeout;
|
|
1280
1305
|
private handleAwarenessUpdate;
|
|
1281
1306
|
private handleTransportClose;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-
|
|
2
|
-
export { A as AcknowledgmentMessage, p as ActiveApplyFailure, q as AwarenessStateWire, r as AwarenessUpdateMessage, E as EncryptedPayload, s as ErrorMessage, H as HandshakeMessage, t as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, u as OperationRejectedMessage, v as SYNC_STATES, w as SYNC_STATUSES, x as ScopeViolationError, y as SyncEncryptionAlgorithm, z as SyncPhase, C as SyncScopeContext, F as SyncSettlementOptions, G as SyncSettlementResult, J as SyncStatus, K as dedupeQuerySubsets, L as isAcknowledgmentMessage, N as isAwarenessUpdateMessage, P as isBlobChunkPushMessage, U as isBlobChunkRequestMessage, X as isBlobChunkResponseMessage, Z as isErrorMessage, _ as isHandshakeMessage, $ as isHandshakeResponseMessage, a0 as isOperationBatchMessage, a1 as isOperationRejectedMessage, a2 as isSyncMessage, a3 as isYjsDocUpdateMessage, a4 as operationMatchesQuerySubsets, a5 as querySubsetContains } from './transport-
|
|
1
|
+
import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-CJvRO5ix.js';
|
|
2
|
+
export { A as AcknowledgmentMessage, p as ActiveApplyFailure, q as AwarenessStateWire, r as AwarenessUpdateMessage, E as EncryptedPayload, s as ErrorMessage, H as HandshakeMessage, t as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, u as OperationRejectedMessage, v as SYNC_STATES, w as SYNC_STATUSES, x as ScopeViolationError, y as SyncEncryptionAlgorithm, z as SyncPhase, C as SyncScopeContext, F as SyncSettlementOptions, G as SyncSettlementResult, J as SyncStatus, K as dedupeQuerySubsets, L as isAcknowledgmentMessage, N as isAwarenessUpdateMessage, P as isBlobChunkPushMessage, U as isBlobChunkRequestMessage, X as isBlobChunkResponseMessage, Z as isErrorMessage, _ as isHandshakeMessage, $ as isHandshakeResponseMessage, a0 as isOperationBatchMessage, a1 as isOperationRejectedMessage, a2 as isSyncMessage, a3 as isYjsDocUpdateMessage, a4 as operationMatchesQuerySubsets, a5 as querySubsetContains } from './transport-CJvRO5ix.js';
|
|
3
3
|
import * as _korajs_core from '@korajs/core';
|
|
4
4
|
import { Operation, VersionVector, ApplyResult, KoraEventEmitter, TimeSource, SyncError, ConnectionQuality } from '@korajs/core';
|
|
5
5
|
export { ApplyFailureReason, ApplyResult } from '@korajs/core';
|
|
@@ -106,6 +106,16 @@ interface SyncStore {
|
|
|
106
106
|
* @returns the record's fields, or null when it cannot be read.
|
|
107
107
|
*/
|
|
108
108
|
readRecordFields?(collection: string, recordId: string): Promise<Record<string, unknown> | null>;
|
|
109
|
+
/**
|
|
110
|
+
* Remove a record from this client's materialized authorization view without
|
|
111
|
+
* writing a domain delete to the replicated operation log.
|
|
112
|
+
*/
|
|
113
|
+
applyScopeRetraction?(collection: string, recordId: string): Promise<void>;
|
|
114
|
+
/** Retract every currently materialized row outside a newly accepted scope. */
|
|
115
|
+
applyScopeNarrowing?(scopes: Record<string, Record<string, unknown>>): Promise<Array<{
|
|
116
|
+
collection: string;
|
|
117
|
+
recordId: string;
|
|
118
|
+
}>>;
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
/** Prefix for handshake rejection when client schema version is unsupported. */
|
|
@@ -879,6 +889,8 @@ declare class OutboundQueue {
|
|
|
879
889
|
* or null if it was not present (already acknowledged and removed).
|
|
880
890
|
*/
|
|
881
891
|
reject(opId: string): Promise<Operation | null>;
|
|
892
|
+
/** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
|
|
893
|
+
rejectRecord(collection: string, recordId: string): Promise<Operation[]>;
|
|
882
894
|
/**
|
|
883
895
|
* Remove operations by id from queue and persistent storage.
|
|
884
896
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -956,6 +968,8 @@ declare class SyncEngine {
|
|
|
956
968
|
private readonly rejectedStorage;
|
|
957
969
|
private readonly batchSize;
|
|
958
970
|
private readonly outboundAckTimeoutMs;
|
|
971
|
+
private readonly outboundRetryBaseDelayMs;
|
|
972
|
+
private readonly outboundRetryMaxDelayMs;
|
|
959
973
|
private readonly encryptor;
|
|
960
974
|
private readonly awarenessManager;
|
|
961
975
|
private readonly richtextDocChannel;
|
|
@@ -971,6 +985,9 @@ declare class SyncEngine {
|
|
|
971
985
|
private conflictCount;
|
|
972
986
|
private currentBatch;
|
|
973
987
|
private currentBatchRequiresPartialAck;
|
|
988
|
+
private currentBatchRequiresRetryBackoff;
|
|
989
|
+
private outboundRetryAttempt;
|
|
990
|
+
private outboundRetryTimer;
|
|
974
991
|
private reconnecting;
|
|
975
992
|
private schemaBlocked;
|
|
976
993
|
private clockBlocked;
|
|
@@ -998,6 +1015,9 @@ declare class SyncEngine {
|
|
|
998
1015
|
* with the server-accepted scope (server is authoritative).
|
|
999
1016
|
*/
|
|
1000
1017
|
private activeScope;
|
|
1018
|
+
/** Server-authoritative upload authorization, separate from the downloaded view. */
|
|
1019
|
+
private activeUplinkScope;
|
|
1020
|
+
private hasDirectionalScopes;
|
|
1001
1021
|
/** Live query subsets registered from reactive subscriptions */
|
|
1002
1022
|
private querySubsets;
|
|
1003
1023
|
private staticQuerySubsets;
|
|
@@ -1169,6 +1189,8 @@ declare class SyncEngine {
|
|
|
1169
1189
|
* Get the currently active scope map. Returns undefined if no scope is configured.
|
|
1170
1190
|
*/
|
|
1171
1191
|
getActiveScope(): SyncScopeMap | undefined;
|
|
1192
|
+
/** Get the server-accepted upload authorization scope. */
|
|
1193
|
+
getActiveUplinkScope(): SyncScopeMap | undefined;
|
|
1172
1194
|
/**
|
|
1173
1195
|
* Register a live query subset that narrows synced data for a collection.
|
|
1174
1196
|
* Takes effect on the next connection; reconnects when already connected.
|
|
@@ -1242,6 +1264,7 @@ declare class SyncEngine {
|
|
|
1242
1264
|
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
1243
1265
|
* signal, so the connection stays up.
|
|
1244
1266
|
*/
|
|
1267
|
+
private applyScopeRetraction;
|
|
1245
1268
|
private handleOperationRejected;
|
|
1246
1269
|
/**
|
|
1247
1270
|
* Every operation the server rejected that has not yet been reconciled. The app
|
|
@@ -1276,6 +1299,8 @@ declare class SyncEngine {
|
|
|
1276
1299
|
private flushQueue;
|
|
1277
1300
|
private startOutboundAckTimer;
|
|
1278
1301
|
private clearOutboundAckTimer;
|
|
1302
|
+
private scheduleOutboundRetry;
|
|
1303
|
+
private clearOutboundRetryTimer;
|
|
1279
1304
|
private handleOutboundAckTimeout;
|
|
1280
1305
|
private handleAwarenessUpdate;
|
|
1281
1306
|
private handleTransportClose;
|
package/dist/index.js
CHANGED
|
@@ -591,7 +591,8 @@ function toProtoEnvelope(message) {
|
|
|
591
591
|
schemaVersion: message.schemaVersion,
|
|
592
592
|
authToken: message.authToken,
|
|
593
593
|
supportedWireFormats: message.supportedWireFormats,
|
|
594
|
-
...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {}
|
|
594
|
+
...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {},
|
|
595
|
+
scopeExitPolicy: message.scopeExitPolicy
|
|
595
596
|
};
|
|
596
597
|
case "handshake-response":
|
|
597
598
|
return {
|
|
@@ -608,7 +609,10 @@ function toProtoEnvelope(message) {
|
|
|
608
609
|
selectedWireFormat: message.selectedWireFormat,
|
|
609
610
|
serverTime: message.serverTime,
|
|
610
611
|
blobStorageEnabled: message.blobStorageEnabled,
|
|
611
|
-
...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {}
|
|
612
|
+
...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {},
|
|
613
|
+
acceptedScopeJson: message.acceptedScope ? JSON.stringify(message.acceptedScope) : void 0,
|
|
614
|
+
acceptedDownlinkScopesJson: message.acceptedDownlinkScopes ? JSON.stringify(message.acceptedDownlinkScopes) : void 0,
|
|
615
|
+
acceptedUplinkScopesJson: message.acceptedUplinkScopes ? JSON.stringify(message.acceptedUplinkScopes) : void 0
|
|
612
616
|
};
|
|
613
617
|
case "operation-batch":
|
|
614
618
|
return {
|
|
@@ -618,7 +622,8 @@ function toProtoEnvelope(message) {
|
|
|
618
622
|
isFinal: message.isFinal,
|
|
619
623
|
batchIndex: message.batchIndex,
|
|
620
624
|
...message.baseDeliverySequence !== void 0 ? { baseDeliverySequence: message.baseDeliverySequence } : {},
|
|
621
|
-
...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {}
|
|
625
|
+
...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {},
|
|
626
|
+
retractionsJson: message.retractions ? JSON.stringify(message.retractions) : void 0
|
|
622
627
|
};
|
|
623
628
|
case "acknowledgment":
|
|
624
629
|
return {
|
|
@@ -692,7 +697,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
692
697
|
supportedWireFormats: envelope.supportedWireFormats?.filter(
|
|
693
698
|
(format) => format === "json" || format === "protobuf"
|
|
694
699
|
),
|
|
695
|
-
...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {}
|
|
700
|
+
...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {},
|
|
701
|
+
...envelope.scopeExitPolicy === "retain" || envelope.scopeExitPolicy === "retract" ? { scopeExitPolicy: envelope.scopeExitPolicy } : {}
|
|
696
702
|
};
|
|
697
703
|
case "handshake-response":
|
|
698
704
|
return {
|
|
@@ -710,7 +716,10 @@ function fromProtoEnvelope(envelope) {
|
|
|
710
716
|
// automatic timestamp rebase work over the protobuf wire, not just JSON.
|
|
711
717
|
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
|
|
712
718
|
...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {},
|
|
713
|
-
...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {}
|
|
719
|
+
...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {},
|
|
720
|
+
...envelope.acceptedScopeJson ? { acceptedScope: JSON.parse(envelope.acceptedScopeJson) } : {},
|
|
721
|
+
...envelope.acceptedDownlinkScopesJson ? { acceptedDownlinkScopes: JSON.parse(envelope.acceptedDownlinkScopesJson) } : {},
|
|
722
|
+
...envelope.acceptedUplinkScopesJson ? { acceptedUplinkScopes: JSON.parse(envelope.acceptedUplinkScopesJson) } : {}
|
|
714
723
|
};
|
|
715
724
|
case "operation-batch":
|
|
716
725
|
return {
|
|
@@ -720,7 +729,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
720
729
|
isFinal: envelope.isFinal ?? false,
|
|
721
730
|
batchIndex: envelope.batchIndex ?? 0,
|
|
722
731
|
...envelope.baseDeliverySequence !== void 0 ? { baseDeliverySequence: envelope.baseDeliverySequence } : {},
|
|
723
|
-
...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {}
|
|
732
|
+
...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {},
|
|
733
|
+
...envelope.retractionsJson ? { retractions: JSON.parse(envelope.retractionsJson) } : {}
|
|
724
734
|
};
|
|
725
735
|
case "acknowledgment":
|
|
726
736
|
return {
|
|
@@ -938,6 +948,13 @@ function encodeEnvelope(envelope) {
|
|
|
938
948
|
if (envelope.deliverySequence !== void 0) writer.uint32(256).int64(envelope.deliverySequence);
|
|
939
949
|
if (envelope.serverMaxDeliverySequence !== void 0)
|
|
940
950
|
writer.uint32(264).int64(envelope.serverMaxDeliverySequence);
|
|
951
|
+
if (envelope.acceptedScopeJson) writer.uint32(274).string(envelope.acceptedScopeJson);
|
|
952
|
+
if (envelope.acceptedDownlinkScopesJson)
|
|
953
|
+
writer.uint32(282).string(envelope.acceptedDownlinkScopesJson);
|
|
954
|
+
if (envelope.acceptedUplinkScopesJson)
|
|
955
|
+
writer.uint32(290).string(envelope.acceptedUplinkScopesJson);
|
|
956
|
+
if (envelope.retractionsJson) writer.uint32(298).string(envelope.retractionsJson);
|
|
957
|
+
if (envelope.scopeExitPolicy) writer.uint32(306).string(envelope.scopeExitPolicy);
|
|
941
958
|
return writer.finish();
|
|
942
959
|
}
|
|
943
960
|
function decodeEnvelope(bytes) {
|
|
@@ -1048,6 +1065,21 @@ function decodeEnvelope(bytes) {
|
|
|
1048
1065
|
case 33:
|
|
1049
1066
|
envelope.serverMaxDeliverySequence = longToNumber(reader.int64());
|
|
1050
1067
|
break;
|
|
1068
|
+
case 34:
|
|
1069
|
+
envelope.acceptedScopeJson = reader.string();
|
|
1070
|
+
break;
|
|
1071
|
+
case 35:
|
|
1072
|
+
envelope.acceptedDownlinkScopesJson = reader.string();
|
|
1073
|
+
break;
|
|
1074
|
+
case 36:
|
|
1075
|
+
envelope.acceptedUplinkScopesJson = reader.string();
|
|
1076
|
+
break;
|
|
1077
|
+
case 37:
|
|
1078
|
+
envelope.retractionsJson = reader.string();
|
|
1079
|
+
break;
|
|
1080
|
+
case 38:
|
|
1081
|
+
envelope.scopeExitPolicy = reader.string();
|
|
1082
|
+
break;
|
|
1051
1083
|
default:
|
|
1052
1084
|
reader.skipType(tag & 7);
|
|
1053
1085
|
}
|
|
@@ -2701,6 +2733,18 @@ var OutboundQueue = class {
|
|
|
2701
2733
|
await this.storage.dequeue([opId]);
|
|
2702
2734
|
return removed;
|
|
2703
2735
|
}
|
|
2736
|
+
/** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
|
|
2737
|
+
async rejectRecord(collection, recordId) {
|
|
2738
|
+
const matching = [...this.queue, ...[...this.inFlight.values()].flat()].filter(
|
|
2739
|
+
(op) => op.collection === collection && op.recordId === recordId
|
|
2740
|
+
);
|
|
2741
|
+
const removed = [];
|
|
2742
|
+
for (const operation of matching) {
|
|
2743
|
+
const rejected = await this.reject(operation.id);
|
|
2744
|
+
if (rejected) removed.push(rejected);
|
|
2745
|
+
}
|
|
2746
|
+
return removed;
|
|
2747
|
+
}
|
|
2704
2748
|
/**
|
|
2705
2749
|
* Remove operations by id from queue and persistent storage.
|
|
2706
2750
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -2720,6 +2764,8 @@ var OutboundQueue = class {
|
|
|
2720
2764
|
var DEFAULT_BATCH_SIZE = 100;
|
|
2721
2765
|
var DEFAULT_SCHEMA_VERSION = 1;
|
|
2722
2766
|
var DEFAULT_OUTBOUND_ACK_TIMEOUT_MS = 3e4;
|
|
2767
|
+
var DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS = 250;
|
|
2768
|
+
var DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS = 3e4;
|
|
2723
2769
|
var VALID_TRANSITIONS = {
|
|
2724
2770
|
disconnected: ["connecting"],
|
|
2725
2771
|
connecting: ["handshaking", "error", "disconnected"],
|
|
@@ -2755,6 +2801,8 @@ var SyncEngine = class {
|
|
|
2755
2801
|
rejectedStorage;
|
|
2756
2802
|
batchSize;
|
|
2757
2803
|
outboundAckTimeoutMs;
|
|
2804
|
+
outboundRetryBaseDelayMs;
|
|
2805
|
+
outboundRetryMaxDelayMs;
|
|
2758
2806
|
encryptor;
|
|
2759
2807
|
awarenessManager;
|
|
2760
2808
|
richtextDocChannel;
|
|
@@ -2770,6 +2818,9 @@ var SyncEngine = class {
|
|
|
2770
2818
|
conflictCount = 0;
|
|
2771
2819
|
currentBatch = null;
|
|
2772
2820
|
currentBatchRequiresPartialAck = false;
|
|
2821
|
+
currentBatchRequiresRetryBackoff = false;
|
|
2822
|
+
outboundRetryAttempt = 0;
|
|
2823
|
+
outboundRetryTimer = null;
|
|
2773
2824
|
reconnecting = false;
|
|
2774
2825
|
schemaBlocked = false;
|
|
2775
2826
|
clockBlocked = false;
|
|
@@ -2798,6 +2849,9 @@ var SyncEngine = class {
|
|
|
2798
2849
|
* with the server-accepted scope (server is authoritative).
|
|
2799
2850
|
*/
|
|
2800
2851
|
activeScope;
|
|
2852
|
+
/** Server-authoritative upload authorization, separate from the downloaded view. */
|
|
2853
|
+
activeUplinkScope;
|
|
2854
|
+
hasDirectionalScopes = false;
|
|
2801
2855
|
/** Live query subsets registered from reactive subscriptions */
|
|
2802
2856
|
querySubsets = /* @__PURE__ */ new Map();
|
|
2803
2857
|
staticQuerySubsets = [];
|
|
@@ -2834,9 +2888,18 @@ var SyncEngine = class {
|
|
|
2834
2888
|
this.emitter = options.emitter ?? null;
|
|
2835
2889
|
this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2836
2890
|
this.outboundAckTimeoutMs = options.config.outboundAckTimeoutMs ?? DEFAULT_OUTBOUND_ACK_TIMEOUT_MS;
|
|
2891
|
+
this.outboundRetryBaseDelayMs = Math.max(
|
|
2892
|
+
0,
|
|
2893
|
+
options.config.outboundRetryBaseDelayMs ?? DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS
|
|
2894
|
+
);
|
|
2895
|
+
this.outboundRetryMaxDelayMs = Math.max(
|
|
2896
|
+
this.outboundRetryBaseDelayMs,
|
|
2897
|
+
options.config.outboundRetryMaxDelayMs ?? DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS
|
|
2898
|
+
);
|
|
2837
2899
|
this.encryptor = options.encryptor ?? null;
|
|
2838
2900
|
this.syncState = options.syncState ?? null;
|
|
2839
2901
|
this.activeScope = options.config.scopeMap;
|
|
2902
|
+
this.activeUplinkScope = options.config.scopeMap;
|
|
2840
2903
|
const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
|
|
2841
2904
|
this.outboundQueue = new OutboundQueue(queueStorage);
|
|
2842
2905
|
this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
|
|
@@ -2959,6 +3022,7 @@ var SyncEngine = class {
|
|
|
2959
3022
|
authToken,
|
|
2960
3023
|
supportedWireFormats: ["json", "protobuf"],
|
|
2961
3024
|
...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
|
|
3025
|
+
...this.config.scopeExit ? { scopeExitPolicy: this.config.scopeExit } : {},
|
|
2962
3026
|
...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
|
|
2963
3027
|
...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {},
|
|
2964
3028
|
// Resume the server's gap-free delivery stream from our watermark. A server
|
|
@@ -2983,13 +3047,15 @@ var SyncEngine = class {
|
|
|
2983
3047
|
return this.stopPromise;
|
|
2984
3048
|
}
|
|
2985
3049
|
async stopInternal() {
|
|
2986
|
-
if (this.state === "disconnected") return;
|
|
2987
3050
|
this.clearOutboundAckTimer();
|
|
3051
|
+
this.clearOutboundRetryTimer();
|
|
3052
|
+
if (this.state === "disconnected") return;
|
|
2988
3053
|
this.awarenessManager.stopCleanupTimer();
|
|
2989
3054
|
if (this.currentBatch) {
|
|
2990
3055
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
2991
3056
|
this.currentBatch = null;
|
|
2992
3057
|
this.currentBatchRequiresPartialAck = false;
|
|
3058
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
2993
3059
|
}
|
|
2994
3060
|
try {
|
|
2995
3061
|
await this.transport.disconnect();
|
|
@@ -3272,6 +3338,8 @@ var SyncEngine = class {
|
|
|
3272
3338
|
updateScope(scopeMap) {
|
|
3273
3339
|
const previousSignature = this.deliverySignature();
|
|
3274
3340
|
this.activeScope = scopeMap;
|
|
3341
|
+
this.activeUplinkScope = scopeMap;
|
|
3342
|
+
this.hasDirectionalScopes = false;
|
|
3275
3343
|
this.config.scopeMap = scopeMap;
|
|
3276
3344
|
this.switchDeliveryView(previousSignature);
|
|
3277
3345
|
}
|
|
@@ -3355,6 +3423,10 @@ var SyncEngine = class {
|
|
|
3355
3423
|
getActiveScope() {
|
|
3356
3424
|
return this.activeScope;
|
|
3357
3425
|
}
|
|
3426
|
+
/** Get the server-accepted upload authorization scope. */
|
|
3427
|
+
getActiveUplinkScope() {
|
|
3428
|
+
return this.activeUplinkScope;
|
|
3429
|
+
}
|
|
3358
3430
|
/**
|
|
3359
3431
|
* Register a live query subset that narrows synced data for a collection.
|
|
3360
3432
|
* Takes effect on the next connection; reconnects when already connected.
|
|
@@ -3557,8 +3629,17 @@ var SyncEngine = class {
|
|
|
3557
3629
|
if (msg.selectedWireFormat) {
|
|
3558
3630
|
this.setSerializerWireFormat(msg.selectedWireFormat);
|
|
3559
3631
|
}
|
|
3560
|
-
if (msg.acceptedScope) {
|
|
3561
|
-
this.activeScope = msg.acceptedScope;
|
|
3632
|
+
if (msg.acceptedDownlinkScopes ?? msg.acceptedScope) {
|
|
3633
|
+
this.activeScope = msg.acceptedDownlinkScopes ?? msg.acceptedScope;
|
|
3634
|
+
}
|
|
3635
|
+
this.hasDirectionalScopes = msg.acceptedUplinkScopes !== void 0;
|
|
3636
|
+
this.activeUplinkScope = msg.acceptedUplinkScopes ?? msg.acceptedScope ?? this.activeScope;
|
|
3637
|
+
if (this.config.scopeExit === "retract" && this.activeScope && this.store.applyScopeNarrowing) {
|
|
3638
|
+
const narrowed = await this.store.applyScopeNarrowing(this.activeScope);
|
|
3639
|
+
for (const retraction of narrowed) {
|
|
3640
|
+
await this.applyScopeRetraction(retraction, false);
|
|
3641
|
+
}
|
|
3642
|
+
await this.refreshPendingCount();
|
|
3562
3643
|
}
|
|
3563
3644
|
this.emitter?.emit({ type: "sync:connected", nodeId: this.store.getNodeId() });
|
|
3564
3645
|
this.metricsCollector.recordConnected();
|
|
@@ -3723,6 +3804,14 @@ var SyncEngine = class {
|
|
|
3723
3804
|
const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
3724
3805
|
const transforms = this.config.operationTransforms ?? [];
|
|
3725
3806
|
let fullyApplied = true;
|
|
3807
|
+
for (const retraction of msg.retractions ?? []) {
|
|
3808
|
+
try {
|
|
3809
|
+
await this.applyScopeRetraction(retraction, true);
|
|
3810
|
+
} catch {
|
|
3811
|
+
fullyApplied = false;
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
await this.refreshPendingCount();
|
|
3726
3815
|
for (const op of inScopeOps) {
|
|
3727
3816
|
const transformed = transforms.length > 0 ? applyOperationTransforms(op, targetSchemaVersion, transforms) : op;
|
|
3728
3817
|
if (transformed === null) {
|
|
@@ -3830,6 +3919,7 @@ var SyncEngine = class {
|
|
|
3830
3919
|
}
|
|
3831
3920
|
async handleAcknowledgment(msg) {
|
|
3832
3921
|
this.clearOutboundAckTimer();
|
|
3922
|
+
const requiresRetryBackoff = this.currentBatchRequiresRetryBackoff;
|
|
3833
3923
|
if (this.state === "syncing" && this.config.strictHandshake) {
|
|
3834
3924
|
this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
|
|
3835
3925
|
this.markDeltaSendCompleteIfReady();
|
|
@@ -3845,6 +3935,7 @@ var SyncEngine = class {
|
|
|
3845
3935
|
}
|
|
3846
3936
|
this.currentBatch = null;
|
|
3847
3937
|
this.currentBatchRequiresPartialAck = false;
|
|
3938
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3848
3939
|
const now = Date.now();
|
|
3849
3940
|
this.lastSyncedAt = now;
|
|
3850
3941
|
this.lastSuccessfulPush = now;
|
|
@@ -3852,7 +3943,14 @@ var SyncEngine = class {
|
|
|
3852
3943
|
await this.advanceLastAckedForLocalNode(msg.lastSequenceNumber);
|
|
3853
3944
|
await this.refreshPendingCount();
|
|
3854
3945
|
if (this.state === "streaming" && this.outboundQueue.hasOperations) {
|
|
3855
|
-
|
|
3946
|
+
if (requiresRetryBackoff) {
|
|
3947
|
+
this.scheduleOutboundRetry();
|
|
3948
|
+
} else {
|
|
3949
|
+
this.outboundRetryAttempt = 0;
|
|
3950
|
+
this.flushQueue();
|
|
3951
|
+
}
|
|
3952
|
+
} else if (!requiresRetryBackoff) {
|
|
3953
|
+
this.outboundRetryAttempt = 0;
|
|
3856
3954
|
}
|
|
3857
3955
|
}
|
|
3858
3956
|
handleError(msg) {
|
|
@@ -3861,6 +3959,7 @@ var SyncEngine = class {
|
|
|
3861
3959
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3862
3960
|
this.currentBatch = null;
|
|
3863
3961
|
this.currentBatchRequiresPartialAck = false;
|
|
3962
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3864
3963
|
}
|
|
3865
3964
|
this.transitionTo("error");
|
|
3866
3965
|
if (msg.code === "AUTH_FAILED" || msg.code === "DEVICE_REVOKED") {
|
|
@@ -3890,9 +3989,39 @@ var SyncEngine = class {
|
|
|
3890
3989
|
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
3891
3990
|
* signal, so the connection stays up.
|
|
3892
3991
|
*/
|
|
3992
|
+
async applyScopeRetraction(retraction, applyToStore) {
|
|
3993
|
+
if (applyToStore) {
|
|
3994
|
+
if (!this.store.applyScopeRetraction) {
|
|
3995
|
+
throw new Error("The configured sync store does not support scope retractions");
|
|
3996
|
+
}
|
|
3997
|
+
await this.store.applyScopeRetraction(retraction.collection, retraction.recordId);
|
|
3998
|
+
}
|
|
3999
|
+
const quarantined = await this.outboundQueue.rejectRecord(
|
|
4000
|
+
retraction.collection,
|
|
4001
|
+
retraction.recordId
|
|
4002
|
+
);
|
|
4003
|
+
for (const operation of quarantined) {
|
|
4004
|
+
await this.rejectedStorage.record({
|
|
4005
|
+
operationId: operation.id,
|
|
4006
|
+
collection: operation.collection,
|
|
4007
|
+
recordId: operation.recordId,
|
|
4008
|
+
code: "SCOPE_RETRACTED",
|
|
4009
|
+
message: "Authorization changed before this operation could be uploaded.",
|
|
4010
|
+
retriable: false,
|
|
4011
|
+
rejectedAt: Date.now()
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
this.emitter?.emit({
|
|
4015
|
+
type: "sync:scope-retracted",
|
|
4016
|
+
collection: retraction.collection,
|
|
4017
|
+
recordId: retraction.recordId,
|
|
4018
|
+
quarantinedOperationIds: quarantined.map((operation) => operation.id)
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
3893
4021
|
async handleOperationRejected(msg) {
|
|
3894
4022
|
if (msg.retriable) {
|
|
3895
4023
|
this.currentBatchRequiresPartialAck = true;
|
|
4024
|
+
this.currentBatchRequiresRetryBackoff = true;
|
|
3896
4025
|
await this.refreshPendingCount();
|
|
3897
4026
|
this.emitter?.emit({
|
|
3898
4027
|
type: "sync:operation-rejected",
|
|
@@ -4046,11 +4175,13 @@ var SyncEngine = class {
|
|
|
4046
4175
|
}
|
|
4047
4176
|
flushQueue() {
|
|
4048
4177
|
if (this.currentBatch) return;
|
|
4178
|
+
if (this.outboundRetryTimer) return;
|
|
4049
4179
|
if (!this.outboundQueue.hasOperations) return;
|
|
4050
4180
|
const batch = this.outboundQueue.takeBatch(this.batchSize);
|
|
4051
4181
|
if (!batch) return;
|
|
4052
4182
|
this.currentBatch = batch;
|
|
4053
4183
|
this.currentBatchRequiresPartialAck = false;
|
|
4184
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4054
4185
|
if (this.encryptor) {
|
|
4055
4186
|
this.encryptor.encryptBatch(batch.operations).then(
|
|
4056
4187
|
(encrypted) => {
|
|
@@ -4074,6 +4205,7 @@ var SyncEngine = class {
|
|
|
4074
4205
|
this.outboundQueue.returnBatch(batch.batchId);
|
|
4075
4206
|
this.currentBatch = null;
|
|
4076
4207
|
this.currentBatchRequiresPartialAck = false;
|
|
4208
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4077
4209
|
this.emitter?.emit({
|
|
4078
4210
|
type: "sync:disconnected",
|
|
4079
4211
|
reason: err instanceof Error ? err.message : "Encryption failed"
|
|
@@ -4110,11 +4242,29 @@ var SyncEngine = class {
|
|
|
4110
4242
|
clearTimeout(this.outboundAckTimer);
|
|
4111
4243
|
this.outboundAckTimer = null;
|
|
4112
4244
|
}
|
|
4245
|
+
scheduleOutboundRetry() {
|
|
4246
|
+
if (this.outboundRetryTimer || !this.outboundQueue.hasOperations) return;
|
|
4247
|
+
const delay = Math.min(
|
|
4248
|
+
this.outboundRetryMaxDelayMs,
|
|
4249
|
+
this.outboundRetryBaseDelayMs * 2 ** this.outboundRetryAttempt
|
|
4250
|
+
);
|
|
4251
|
+
this.outboundRetryAttempt += 1;
|
|
4252
|
+
this.outboundRetryTimer = setTimeout(() => {
|
|
4253
|
+
this.outboundRetryTimer = null;
|
|
4254
|
+
if (this.state === "streaming") this.flushQueue();
|
|
4255
|
+
}, delay);
|
|
4256
|
+
}
|
|
4257
|
+
clearOutboundRetryTimer() {
|
|
4258
|
+
if (!this.outboundRetryTimer) return;
|
|
4259
|
+
clearTimeout(this.outboundRetryTimer);
|
|
4260
|
+
this.outboundRetryTimer = null;
|
|
4261
|
+
}
|
|
4113
4262
|
async handleOutboundAckTimeout(batchId) {
|
|
4114
4263
|
if (!this.currentBatch || this.currentBatch.batchId !== batchId) return;
|
|
4115
4264
|
this.outboundQueue.returnBatch(batchId);
|
|
4116
4265
|
this.currentBatch = null;
|
|
4117
4266
|
this.currentBatchRequiresPartialAck = false;
|
|
4267
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4118
4268
|
this.clearOutboundAckTimer();
|
|
4119
4269
|
await this.refreshPendingCount();
|
|
4120
4270
|
if (this.state === "disconnected") return;
|
|
@@ -4136,10 +4286,12 @@ var SyncEngine = class {
|
|
|
4136
4286
|
}
|
|
4137
4287
|
handleTransportClose(reason) {
|
|
4138
4288
|
this.clearOutboundAckTimer();
|
|
4289
|
+
this.clearOutboundRetryTimer();
|
|
4139
4290
|
if (this.currentBatch) {
|
|
4140
4291
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
4141
4292
|
this.currentBatch = null;
|
|
4142
4293
|
this.currentBatchRequiresPartialAck = false;
|
|
4294
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4143
4295
|
}
|
|
4144
4296
|
if (this.schemaBlocked) {
|
|
4145
4297
|
return;
|
|
@@ -4182,10 +4334,10 @@ var SyncEngine = class {
|
|
|
4182
4334
|
return this.matchesScopeAndSubsets(op, fullRecord);
|
|
4183
4335
|
}
|
|
4184
4336
|
matchesScopeAndSubsets(op, fullRecord) {
|
|
4185
|
-
if (!operationMatchesScope(op, this.
|
|
4337
|
+
if (!operationMatchesScope(op, this.activeUplinkScope, fullRecord)) {
|
|
4186
4338
|
return false;
|
|
4187
4339
|
}
|
|
4188
|
-
return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
|
|
4340
|
+
return this.hasDirectionalScopes || operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
|
|
4189
4341
|
}
|
|
4190
4342
|
async readRecordForBackfill(op) {
|
|
4191
4343
|
if (!this.store.readRecordFields) {
|