@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 CHANGED
@@ -684,7 +684,8 @@ function toProtoEnvelope(message) {
684
684
  schemaVersion: message.schemaVersion,
685
685
  authToken: message.authToken,
686
686
  supportedWireFormats: message.supportedWireFormats,
687
- ...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {}
687
+ ...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {},
688
+ scopeExitPolicy: message.scopeExitPolicy
688
689
  };
689
690
  case "handshake-response":
690
691
  return {
@@ -701,7 +702,10 @@ function toProtoEnvelope(message) {
701
702
  selectedWireFormat: message.selectedWireFormat,
702
703
  serverTime: message.serverTime,
703
704
  blobStorageEnabled: message.blobStorageEnabled,
704
- ...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {}
705
+ ...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {},
706
+ acceptedScopeJson: message.acceptedScope ? JSON.stringify(message.acceptedScope) : void 0,
707
+ acceptedDownlinkScopesJson: message.acceptedDownlinkScopes ? JSON.stringify(message.acceptedDownlinkScopes) : void 0,
708
+ acceptedUplinkScopesJson: message.acceptedUplinkScopes ? JSON.stringify(message.acceptedUplinkScopes) : void 0
705
709
  };
706
710
  case "operation-batch":
707
711
  return {
@@ -711,7 +715,8 @@ function toProtoEnvelope(message) {
711
715
  isFinal: message.isFinal,
712
716
  batchIndex: message.batchIndex,
713
717
  ...message.baseDeliverySequence !== void 0 ? { baseDeliverySequence: message.baseDeliverySequence } : {},
714
- ...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {}
718
+ ...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {},
719
+ retractionsJson: message.retractions ? JSON.stringify(message.retractions) : void 0
715
720
  };
716
721
  case "acknowledgment":
717
722
  return {
@@ -785,7 +790,8 @@ function fromProtoEnvelope(envelope) {
785
790
  supportedWireFormats: envelope.supportedWireFormats?.filter(
786
791
  (format) => format === "json" || format === "protobuf"
787
792
  ),
788
- ...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {}
793
+ ...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {},
794
+ ...envelope.scopeExitPolicy === "retain" || envelope.scopeExitPolicy === "retract" ? { scopeExitPolicy: envelope.scopeExitPolicy } : {}
789
795
  };
790
796
  case "handshake-response":
791
797
  return {
@@ -803,7 +809,10 @@ function fromProtoEnvelope(envelope) {
803
809
  // automatic timestamp rebase work over the protobuf wire, not just JSON.
804
810
  ...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
805
811
  ...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {},
806
- ...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {}
812
+ ...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {},
813
+ ...envelope.acceptedScopeJson ? { acceptedScope: JSON.parse(envelope.acceptedScopeJson) } : {},
814
+ ...envelope.acceptedDownlinkScopesJson ? { acceptedDownlinkScopes: JSON.parse(envelope.acceptedDownlinkScopesJson) } : {},
815
+ ...envelope.acceptedUplinkScopesJson ? { acceptedUplinkScopes: JSON.parse(envelope.acceptedUplinkScopesJson) } : {}
807
816
  };
808
817
  case "operation-batch":
809
818
  return {
@@ -813,7 +822,8 @@ function fromProtoEnvelope(envelope) {
813
822
  isFinal: envelope.isFinal ?? false,
814
823
  batchIndex: envelope.batchIndex ?? 0,
815
824
  ...envelope.baseDeliverySequence !== void 0 ? { baseDeliverySequence: envelope.baseDeliverySequence } : {},
816
- ...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {}
825
+ ...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {},
826
+ ...envelope.retractionsJson ? { retractions: JSON.parse(envelope.retractionsJson) } : {}
817
827
  };
818
828
  case "acknowledgment":
819
829
  return {
@@ -1031,6 +1041,13 @@ function encodeEnvelope(envelope) {
1031
1041
  if (envelope.deliverySequence !== void 0) writer.uint32(256).int64(envelope.deliverySequence);
1032
1042
  if (envelope.serverMaxDeliverySequence !== void 0)
1033
1043
  writer.uint32(264).int64(envelope.serverMaxDeliverySequence);
1044
+ if (envelope.acceptedScopeJson) writer.uint32(274).string(envelope.acceptedScopeJson);
1045
+ if (envelope.acceptedDownlinkScopesJson)
1046
+ writer.uint32(282).string(envelope.acceptedDownlinkScopesJson);
1047
+ if (envelope.acceptedUplinkScopesJson)
1048
+ writer.uint32(290).string(envelope.acceptedUplinkScopesJson);
1049
+ if (envelope.retractionsJson) writer.uint32(298).string(envelope.retractionsJson);
1050
+ if (envelope.scopeExitPolicy) writer.uint32(306).string(envelope.scopeExitPolicy);
1034
1051
  return writer.finish();
1035
1052
  }
1036
1053
  function decodeEnvelope(bytes) {
@@ -1141,6 +1158,21 @@ function decodeEnvelope(bytes) {
1141
1158
  case 33:
1142
1159
  envelope.serverMaxDeliverySequence = longToNumber(reader.int64());
1143
1160
  break;
1161
+ case 34:
1162
+ envelope.acceptedScopeJson = reader.string();
1163
+ break;
1164
+ case 35:
1165
+ envelope.acceptedDownlinkScopesJson = reader.string();
1166
+ break;
1167
+ case 36:
1168
+ envelope.acceptedUplinkScopesJson = reader.string();
1169
+ break;
1170
+ case 37:
1171
+ envelope.retractionsJson = reader.string();
1172
+ break;
1173
+ case 38:
1174
+ envelope.scopeExitPolicy = reader.string();
1175
+ break;
1144
1176
  default:
1145
1177
  reader.skipType(tag & 7);
1146
1178
  }
@@ -2806,6 +2838,18 @@ var OutboundQueue = class {
2806
2838
  await this.storage.dequeue([opId]);
2807
2839
  return removed;
2808
2840
  }
2841
+ /** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
2842
+ async rejectRecord(collection, recordId) {
2843
+ const matching = [...this.queue, ...[...this.inFlight.values()].flat()].filter(
2844
+ (op) => op.collection === collection && op.recordId === recordId
2845
+ );
2846
+ const removed = [];
2847
+ for (const operation of matching) {
2848
+ const rejected = await this.reject(operation.id);
2849
+ if (rejected) removed.push(rejected);
2850
+ }
2851
+ return removed;
2852
+ }
2809
2853
  /**
2810
2854
  * Remove operations by id from queue and persistent storage.
2811
2855
  * Used when ops were already sent during handshake delta exchange.
@@ -2825,6 +2869,8 @@ var OutboundQueue = class {
2825
2869
  var DEFAULT_BATCH_SIZE = 100;
2826
2870
  var DEFAULT_SCHEMA_VERSION = 1;
2827
2871
  var DEFAULT_OUTBOUND_ACK_TIMEOUT_MS = 3e4;
2872
+ var DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS = 250;
2873
+ var DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS = 3e4;
2828
2874
  var VALID_TRANSITIONS = {
2829
2875
  disconnected: ["connecting"],
2830
2876
  connecting: ["handshaking", "error", "disconnected"],
@@ -2860,6 +2906,8 @@ var SyncEngine = class {
2860
2906
  rejectedStorage;
2861
2907
  batchSize;
2862
2908
  outboundAckTimeoutMs;
2909
+ outboundRetryBaseDelayMs;
2910
+ outboundRetryMaxDelayMs;
2863
2911
  encryptor;
2864
2912
  awarenessManager;
2865
2913
  richtextDocChannel;
@@ -2875,6 +2923,9 @@ var SyncEngine = class {
2875
2923
  conflictCount = 0;
2876
2924
  currentBatch = null;
2877
2925
  currentBatchRequiresPartialAck = false;
2926
+ currentBatchRequiresRetryBackoff = false;
2927
+ outboundRetryAttempt = 0;
2928
+ outboundRetryTimer = null;
2878
2929
  reconnecting = false;
2879
2930
  schemaBlocked = false;
2880
2931
  clockBlocked = false;
@@ -2903,6 +2954,9 @@ var SyncEngine = class {
2903
2954
  * with the server-accepted scope (server is authoritative).
2904
2955
  */
2905
2956
  activeScope;
2957
+ /** Server-authoritative upload authorization, separate from the downloaded view. */
2958
+ activeUplinkScope;
2959
+ hasDirectionalScopes = false;
2906
2960
  /** Live query subsets registered from reactive subscriptions */
2907
2961
  querySubsets = /* @__PURE__ */ new Map();
2908
2962
  staticQuerySubsets = [];
@@ -2939,9 +2993,18 @@ var SyncEngine = class {
2939
2993
  this.emitter = options.emitter ?? null;
2940
2994
  this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
2941
2995
  this.outboundAckTimeoutMs = options.config.outboundAckTimeoutMs ?? DEFAULT_OUTBOUND_ACK_TIMEOUT_MS;
2996
+ this.outboundRetryBaseDelayMs = Math.max(
2997
+ 0,
2998
+ options.config.outboundRetryBaseDelayMs ?? DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS
2999
+ );
3000
+ this.outboundRetryMaxDelayMs = Math.max(
3001
+ this.outboundRetryBaseDelayMs,
3002
+ options.config.outboundRetryMaxDelayMs ?? DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS
3003
+ );
2942
3004
  this.encryptor = options.encryptor ?? null;
2943
3005
  this.syncState = options.syncState ?? null;
2944
3006
  this.activeScope = options.config.scopeMap;
3007
+ this.activeUplinkScope = options.config.scopeMap;
2945
3008
  const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
2946
3009
  this.outboundQueue = new OutboundQueue(queueStorage);
2947
3010
  this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
@@ -3064,6 +3127,7 @@ var SyncEngine = class {
3064
3127
  authToken,
3065
3128
  supportedWireFormats: ["json", "protobuf"],
3066
3129
  ...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
3130
+ ...this.config.scopeExit ? { scopeExitPolicy: this.config.scopeExit } : {},
3067
3131
  ...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
3068
3132
  ...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {},
3069
3133
  // Resume the server's gap-free delivery stream from our watermark. A server
@@ -3088,13 +3152,15 @@ var SyncEngine = class {
3088
3152
  return this.stopPromise;
3089
3153
  }
3090
3154
  async stopInternal() {
3091
- if (this.state === "disconnected") return;
3092
3155
  this.clearOutboundAckTimer();
3156
+ this.clearOutboundRetryTimer();
3157
+ if (this.state === "disconnected") return;
3093
3158
  this.awarenessManager.stopCleanupTimer();
3094
3159
  if (this.currentBatch) {
3095
3160
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
3096
3161
  this.currentBatch = null;
3097
3162
  this.currentBatchRequiresPartialAck = false;
3163
+ this.currentBatchRequiresRetryBackoff = false;
3098
3164
  }
3099
3165
  try {
3100
3166
  await this.transport.disconnect();
@@ -3377,6 +3443,8 @@ var SyncEngine = class {
3377
3443
  updateScope(scopeMap) {
3378
3444
  const previousSignature = this.deliverySignature();
3379
3445
  this.activeScope = scopeMap;
3446
+ this.activeUplinkScope = scopeMap;
3447
+ this.hasDirectionalScopes = false;
3380
3448
  this.config.scopeMap = scopeMap;
3381
3449
  this.switchDeliveryView(previousSignature);
3382
3450
  }
@@ -3460,6 +3528,10 @@ var SyncEngine = class {
3460
3528
  getActiveScope() {
3461
3529
  return this.activeScope;
3462
3530
  }
3531
+ /** Get the server-accepted upload authorization scope. */
3532
+ getActiveUplinkScope() {
3533
+ return this.activeUplinkScope;
3534
+ }
3463
3535
  /**
3464
3536
  * Register a live query subset that narrows synced data for a collection.
3465
3537
  * Takes effect on the next connection; reconnects when already connected.
@@ -3662,8 +3734,17 @@ var SyncEngine = class {
3662
3734
  if (msg.selectedWireFormat) {
3663
3735
  this.setSerializerWireFormat(msg.selectedWireFormat);
3664
3736
  }
3665
- if (msg.acceptedScope) {
3666
- this.activeScope = msg.acceptedScope;
3737
+ if (msg.acceptedDownlinkScopes ?? msg.acceptedScope) {
3738
+ this.activeScope = msg.acceptedDownlinkScopes ?? msg.acceptedScope;
3739
+ }
3740
+ this.hasDirectionalScopes = msg.acceptedUplinkScopes !== void 0;
3741
+ this.activeUplinkScope = msg.acceptedUplinkScopes ?? msg.acceptedScope ?? this.activeScope;
3742
+ if (this.config.scopeExit === "retract" && this.activeScope && this.store.applyScopeNarrowing) {
3743
+ const narrowed = await this.store.applyScopeNarrowing(this.activeScope);
3744
+ for (const retraction of narrowed) {
3745
+ await this.applyScopeRetraction(retraction, false);
3746
+ }
3747
+ await this.refreshPendingCount();
3667
3748
  }
3668
3749
  this.emitter?.emit({ type: "sync:connected", nodeId: this.store.getNodeId() });
3669
3750
  this.metricsCollector.recordConnected();
@@ -3828,6 +3909,14 @@ var SyncEngine = class {
3828
3909
  const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
3829
3910
  const transforms = this.config.operationTransforms ?? [];
3830
3911
  let fullyApplied = true;
3912
+ for (const retraction of msg.retractions ?? []) {
3913
+ try {
3914
+ await this.applyScopeRetraction(retraction, true);
3915
+ } catch {
3916
+ fullyApplied = false;
3917
+ }
3918
+ }
3919
+ await this.refreshPendingCount();
3831
3920
  for (const op of inScopeOps) {
3832
3921
  const transformed = transforms.length > 0 ? (0, import_core7.applyOperationTransforms)(op, targetSchemaVersion, transforms) : op;
3833
3922
  if (transformed === null) {
@@ -3935,6 +4024,7 @@ var SyncEngine = class {
3935
4024
  }
3936
4025
  async handleAcknowledgment(msg) {
3937
4026
  this.clearOutboundAckTimer();
4027
+ const requiresRetryBackoff = this.currentBatchRequiresRetryBackoff;
3938
4028
  if (this.state === "syncing" && this.config.strictHandshake) {
3939
4029
  this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
3940
4030
  this.markDeltaSendCompleteIfReady();
@@ -3950,6 +4040,7 @@ var SyncEngine = class {
3950
4040
  }
3951
4041
  this.currentBatch = null;
3952
4042
  this.currentBatchRequiresPartialAck = false;
4043
+ this.currentBatchRequiresRetryBackoff = false;
3953
4044
  const now = Date.now();
3954
4045
  this.lastSyncedAt = now;
3955
4046
  this.lastSuccessfulPush = now;
@@ -3957,7 +4048,14 @@ var SyncEngine = class {
3957
4048
  await this.advanceLastAckedForLocalNode(msg.lastSequenceNumber);
3958
4049
  await this.refreshPendingCount();
3959
4050
  if (this.state === "streaming" && this.outboundQueue.hasOperations) {
3960
- this.flushQueue();
4051
+ if (requiresRetryBackoff) {
4052
+ this.scheduleOutboundRetry();
4053
+ } else {
4054
+ this.outboundRetryAttempt = 0;
4055
+ this.flushQueue();
4056
+ }
4057
+ } else if (!requiresRetryBackoff) {
4058
+ this.outboundRetryAttempt = 0;
3961
4059
  }
3962
4060
  }
3963
4061
  handleError(msg) {
@@ -3966,6 +4064,7 @@ var SyncEngine = class {
3966
4064
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
3967
4065
  this.currentBatch = null;
3968
4066
  this.currentBatchRequiresPartialAck = false;
4067
+ this.currentBatchRequiresRetryBackoff = false;
3969
4068
  }
3970
4069
  this.transitionTo("error");
3971
4070
  if (msg.code === "AUTH_FAILED" || msg.code === "DEVICE_REVOKED") {
@@ -3995,9 +4094,39 @@ var SyncEngine = class {
3995
4094
  * the app can reconcile. Unlike {@link handleError}, this is a normal per-op
3996
4095
  * signal, so the connection stays up.
3997
4096
  */
4097
+ async applyScopeRetraction(retraction, applyToStore) {
4098
+ if (applyToStore) {
4099
+ if (!this.store.applyScopeRetraction) {
4100
+ throw new Error("The configured sync store does not support scope retractions");
4101
+ }
4102
+ await this.store.applyScopeRetraction(retraction.collection, retraction.recordId);
4103
+ }
4104
+ const quarantined = await this.outboundQueue.rejectRecord(
4105
+ retraction.collection,
4106
+ retraction.recordId
4107
+ );
4108
+ for (const operation of quarantined) {
4109
+ await this.rejectedStorage.record({
4110
+ operationId: operation.id,
4111
+ collection: operation.collection,
4112
+ recordId: operation.recordId,
4113
+ code: "SCOPE_RETRACTED",
4114
+ message: "Authorization changed before this operation could be uploaded.",
4115
+ retriable: false,
4116
+ rejectedAt: Date.now()
4117
+ });
4118
+ }
4119
+ this.emitter?.emit({
4120
+ type: "sync:scope-retracted",
4121
+ collection: retraction.collection,
4122
+ recordId: retraction.recordId,
4123
+ quarantinedOperationIds: quarantined.map((operation) => operation.id)
4124
+ });
4125
+ }
3998
4126
  async handleOperationRejected(msg) {
3999
4127
  if (msg.retriable) {
4000
4128
  this.currentBatchRequiresPartialAck = true;
4129
+ this.currentBatchRequiresRetryBackoff = true;
4001
4130
  await this.refreshPendingCount();
4002
4131
  this.emitter?.emit({
4003
4132
  type: "sync:operation-rejected",
@@ -4151,11 +4280,13 @@ var SyncEngine = class {
4151
4280
  }
4152
4281
  flushQueue() {
4153
4282
  if (this.currentBatch) return;
4283
+ if (this.outboundRetryTimer) return;
4154
4284
  if (!this.outboundQueue.hasOperations) return;
4155
4285
  const batch = this.outboundQueue.takeBatch(this.batchSize);
4156
4286
  if (!batch) return;
4157
4287
  this.currentBatch = batch;
4158
4288
  this.currentBatchRequiresPartialAck = false;
4289
+ this.currentBatchRequiresRetryBackoff = false;
4159
4290
  if (this.encryptor) {
4160
4291
  this.encryptor.encryptBatch(batch.operations).then(
4161
4292
  (encrypted) => {
@@ -4179,6 +4310,7 @@ var SyncEngine = class {
4179
4310
  this.outboundQueue.returnBatch(batch.batchId);
4180
4311
  this.currentBatch = null;
4181
4312
  this.currentBatchRequiresPartialAck = false;
4313
+ this.currentBatchRequiresRetryBackoff = false;
4182
4314
  this.emitter?.emit({
4183
4315
  type: "sync:disconnected",
4184
4316
  reason: err instanceof Error ? err.message : "Encryption failed"
@@ -4215,11 +4347,29 @@ var SyncEngine = class {
4215
4347
  clearTimeout(this.outboundAckTimer);
4216
4348
  this.outboundAckTimer = null;
4217
4349
  }
4350
+ scheduleOutboundRetry() {
4351
+ if (this.outboundRetryTimer || !this.outboundQueue.hasOperations) return;
4352
+ const delay = Math.min(
4353
+ this.outboundRetryMaxDelayMs,
4354
+ this.outboundRetryBaseDelayMs * 2 ** this.outboundRetryAttempt
4355
+ );
4356
+ this.outboundRetryAttempt += 1;
4357
+ this.outboundRetryTimer = setTimeout(() => {
4358
+ this.outboundRetryTimer = null;
4359
+ if (this.state === "streaming") this.flushQueue();
4360
+ }, delay);
4361
+ }
4362
+ clearOutboundRetryTimer() {
4363
+ if (!this.outboundRetryTimer) return;
4364
+ clearTimeout(this.outboundRetryTimer);
4365
+ this.outboundRetryTimer = null;
4366
+ }
4218
4367
  async handleOutboundAckTimeout(batchId) {
4219
4368
  if (!this.currentBatch || this.currentBatch.batchId !== batchId) return;
4220
4369
  this.outboundQueue.returnBatch(batchId);
4221
4370
  this.currentBatch = null;
4222
4371
  this.currentBatchRequiresPartialAck = false;
4372
+ this.currentBatchRequiresRetryBackoff = false;
4223
4373
  this.clearOutboundAckTimer();
4224
4374
  await this.refreshPendingCount();
4225
4375
  if (this.state === "disconnected") return;
@@ -4241,10 +4391,12 @@ var SyncEngine = class {
4241
4391
  }
4242
4392
  handleTransportClose(reason) {
4243
4393
  this.clearOutboundAckTimer();
4394
+ this.clearOutboundRetryTimer();
4244
4395
  if (this.currentBatch) {
4245
4396
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
4246
4397
  this.currentBatch = null;
4247
4398
  this.currentBatchRequiresPartialAck = false;
4399
+ this.currentBatchRequiresRetryBackoff = false;
4248
4400
  }
4249
4401
  if (this.schemaBlocked) {
4250
4402
  return;
@@ -4287,10 +4439,10 @@ var SyncEngine = class {
4287
4439
  return this.matchesScopeAndSubsets(op, fullRecord);
4288
4440
  }
4289
4441
  matchesScopeAndSubsets(op, fullRecord) {
4290
- if (!operationMatchesScope(op, this.activeScope, fullRecord)) {
4442
+ if (!operationMatchesScope(op, this.activeUplinkScope, fullRecord)) {
4291
4443
  return false;
4292
4444
  }
4293
- return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
4445
+ return this.hasDirectionalScopes || operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
4294
4446
  }
4295
4447
  async readRecordForBackfill(op) {
4296
4448
  if (!this.store.readRecordFields) {