@salesforce/lds-worker-api 1.451.0-dev2 → 1.452.0

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.
@@ -1373,4 +1373,4 @@ if (process.env.NODE_ENV !== 'production') {
1373
1373
  }
1374
1374
 
1375
1375
  export { createPrimingSession, draftManager, draftQueue, evictCacheRecordsByIds, evictExpiredCacheEntries, executeAdapter, executeMutatingAdapter, getImperativeAdapterNames, importLuvioAdapterModule, importOneStoreAdapterModule, invokeAdapter, invokeAdapterWithDraftToMerge, invokeAdapterWithDraftToReplace, invokeAdapterWithMetadata, nimbusDraftQueue, setMetadataTTL, setUiApiRecordTTL, stopEviction, subscribeToAdapter };
1376
- // version: 1.451.0-dev2-a5cd560c12
1376
+ // version: 1.452.0-bcd113b7fd
@@ -4276,7 +4276,7 @@ function withDefaultLuvio(callback) {
4276
4276
  }
4277
4277
  callbacks.push(callback);
4278
4278
  }
4279
- // version: 1.451.0-dev2-a5cd560c12
4279
+ // version: 1.452.0-bcd113b7fd
4280
4280
 
4281
4281
  // TODO [TD-0081508]: once that TD is fulfilled we can probably change this file
4282
4282
  function instrumentAdapter$1(createFunction, _metadata) {
@@ -5320,7 +5320,7 @@ function createGraphQLWireAdapterConstructor(luvio, adapter, metadata, astResolv
5320
5320
  const { apiFamily, name } = metadata;
5321
5321
  return createGraphQLWireAdapterConstructor$1(adapter, `${apiFamily}.${name}`, luvio, astResolver);
5322
5322
  }
5323
- // version: 1.451.0-dev2-a5cd560c12
5323
+ // version: 1.452.0-bcd113b7fd
5324
5324
 
5325
5325
  function isSupportedEntity(_objectApiName) {
5326
5326
  return true;
@@ -32547,7 +32547,7 @@ withDefaultLuvio((luvio) => {
32547
32547
  throttle(60, 60000, setupNotifyAllListRecordUpdateAvailable(luvio));
32548
32548
  throttle(60, 60000, setupNotifyAllListInfoSummaryUpdateAvailable(luvio));
32549
32549
  });
32550
- // version: 1.451.0-dev2-312b888699
32550
+ // version: 1.452.0-1ecfe8af95
32551
32551
 
32552
32552
  var draftQueueMaxRetryAttemptsGate = {
32553
32553
  isOpen: function (e) {
@@ -32558,6 +32558,15 @@ var draftQueueMaxRetryAttemptsGate = {
32558
32558
  },
32559
32559
  };
32560
32560
 
32561
+ var draftQueueRedirectScanGate = {
32562
+ isOpen: function (e) {
32563
+ return e.fallback;
32564
+ },
32565
+ hasError: function () {
32566
+ return !0;
32567
+ },
32568
+ };
32569
+
32561
32570
  var rotateIdempotencyKeyOn400Killswitch = {
32562
32571
  isOpen: function (e) {
32563
32572
  return e.fallback;
@@ -44103,6 +44112,15 @@ var graphqlPartialEmitParity = {
44103
44112
  },
44104
44113
  };
44105
44114
 
44115
+ var draftQueueStoreSnapshotGate = {
44116
+ isOpen: function (e) {
44117
+ return e.fallback;
44118
+ },
44119
+ hasError: function () {
44120
+ return !0;
44121
+ },
44122
+ };
44123
+
44106
44124
  var ldsAdapterO11yLoggingGate = {
44107
44125
  isOpen: function (e) {
44108
44126
  return e.fallback;
@@ -45892,6 +45910,15 @@ function resolveAndValidateGraphQLDocument(query, operationName, options) {
45892
45910
  return ok$3(document);
45893
45911
  }
45894
45912
 
45913
+ var draftQueueSideEffectMapGate = {
45914
+ isOpen: function (e) {
45915
+ return e.fallback;
45916
+ },
45917
+ hasError: function () {
45918
+ return !0;
45919
+ },
45920
+ };
45921
+
45895
45922
  var productConsumedSideEffects = {
45896
45923
  isOpen: function (e) {
45897
45924
  return e.fallback;
@@ -48214,7 +48241,7 @@ class DurableDraftQueue {
48214
48241
  }
48215
48242
  return handler;
48216
48243
  }
48217
- constructor(draftStore) {
48244
+ constructor(draftStore, instrumentation) {
48218
48245
  this.retryIntervalMilliseconds = 0;
48219
48246
  this.minimumRetryInterval = 250;
48220
48247
  this.maximumRetryInterval = 32000;
@@ -48231,6 +48258,7 @@ class DurableDraftQueue {
48231
48258
  this.handlers = {};
48232
48259
  this.draftStore = draftStore;
48233
48260
  this.workerPool = new AsyncWorkerPool(1);
48261
+ this.instrumentation = instrumentation;
48234
48262
  }
48235
48263
  addHandler(handler) {
48236
48264
  const id = handler.handlerId;
@@ -48321,15 +48349,22 @@ class DurableDraftQueue {
48321
48349
  this.state = DraftQueueState.Stopped;
48322
48350
  }
48323
48351
  async getQueueActions() {
48352
+ // W-23515682: time the whole call, including this queue's own internal
48353
+ // this.getQueueActions() during a drain — the amplification signal an external wrapper
48354
+ // cannot see. Measure-only; control flow below is unchanged.
48355
+ const { instrumentation } = this;
48356
+ const start = instrumentation?.onGetQueueActions !== undefined ? Date.now() : 0;
48324
48357
  const drafts = (await this.draftStore.getAllDrafts());
48325
48358
  const queue = [];
48326
48359
  drafts.forEach((draft) => {
48327
48360
  if (draft.id === this.uploadingActionId) {
48328
- draft.status = DraftActionStatus.Uploading;
48361
+ queue.push({ ...draft, status: DraftActionStatus.Uploading });
48362
+ }
48363
+ else {
48364
+ queue.push(draft);
48329
48365
  }
48330
- queue.push(draft);
48331
48366
  });
48332
- return queue.sort((a, b) => {
48367
+ const sorted = queue.sort((a, b) => {
48333
48368
  const aTime = parseInt(a.id, 10);
48334
48369
  const bTime = parseInt(b.id, 10);
48335
48370
  // safety check
@@ -48341,6 +48376,8 @@ class DurableDraftQueue {
48341
48376
  }
48342
48377
  return aTime - bTime;
48343
48378
  });
48379
+ instrumentation?.onGetQueueActions?.(Date.now() - start, sorted.length);
48380
+ return sorted;
48344
48381
  }
48345
48382
  async enqueue(handlerId, data, observabilityContext) {
48346
48383
  return this.workerPool.push({
@@ -48489,6 +48526,8 @@ class DurableDraftQueue {
48489
48526
  const listener = draftQueueChangedListeners[i];
48490
48527
  results.push(listener(event));
48491
48528
  }
48529
+ // W-23515682: listener fan-out per event type. Measure-only.
48530
+ this.instrumentation?.onNotifyChangedListeners?.(event.type, draftQueueLen);
48492
48531
  await Promise.all(results);
48493
48532
  }
48494
48533
  /**
@@ -48736,36 +48775,66 @@ function buildDraftDurableStoreKey(recordKey, draftActionId) {
48736
48775
  *
48737
48776
  */
48738
48777
  class DurableDraftStore {
48739
- constructor(durableStore) {
48778
+ constructor(durableStore, instrumentation, useSnapshot = true) {
48740
48779
  this.draftStore = {};
48780
+ // Snapshot: holds deep clones of each draft, refreshed at write time.
48781
+ // getAllDrafts serves from this snapshot with zero deep clones per read.
48782
+ this.cleanSnapshot = {};
48741
48783
  // queue of writes that were made during the initial sync
48742
48784
  this.writeQueue = [];
48743
48785
  this.durableStore = durableStore;
48786
+ this.instrumentation = instrumentation;
48787
+ this.useSnapshot = useSnapshot;
48744
48788
  this.resyncDraftStore();
48745
48789
  }
48790
+ // When no instrumentation is injected the promise is returned as-is (zero overhead).
48791
+ timeDurableOp(op, segment, run) {
48792
+ const onDurableOperation = this.instrumentation?.onDurableOperation;
48793
+ if (onDurableOperation === undefined) {
48794
+ return run();
48795
+ }
48796
+ const start = Date.now();
48797
+ return run().finally(() => {
48798
+ onDurableOperation(op, segment, Date.now() - start);
48799
+ });
48800
+ }
48746
48801
  writeAction(action) {
48747
48802
  const addAction = () => {
48748
48803
  const { id, tag } = action;
48749
48804
  this.draftStore[id] = action;
48805
+ this.cleanSnapshot[id] = clone$1(action);
48750
48806
  const durableEntryKey = buildDraftDurableStoreKey(tag, id);
48751
48807
  const entry = {
48752
48808
  data: action,
48753
48809
  };
48754
48810
  const entries = { [durableEntryKey]: entry };
48755
- return this.durableStore.setEntries(entries, DRAFT_SEGMENT);
48811
+ return this.timeDurableOp('setEntries', DRAFT_SEGMENT, () => this.durableStore.setEntries(entries, DRAFT_SEGMENT));
48756
48812
  };
48757
48813
  return this.enqueueAction(addAction);
48758
48814
  }
48759
48815
  getAllDrafts() {
48760
48816
  const waitForOngoingSync = this.syncPromise || Promise.resolve();
48761
48817
  return waitForOngoingSync.then(() => {
48762
- const { draftStore } = this;
48763
- const keys$1 = keys$4(draftStore);
48764
48818
  const actionArray = [];
48765
- for (let i = 0, len = keys$1.length; i < len; i++) {
48766
- const key = keys$1[i];
48767
- // clone draft so we don't expose the internal draft store
48768
- actionArray.push(clone$1(draftStore[key]));
48819
+ if (this.useSnapshot) {
48820
+ const { cleanSnapshot } = this;
48821
+ const keys$1 = keys$4(cleanSnapshot);
48822
+ for (let i = 0, len = keys$1.length; i < len; i++) {
48823
+ const key = keys$1[i];
48824
+ // Return shallow spread of snapshot entries so top-level caller mutations
48825
+ // (e.g., .status =) don't persist into the snapshot. The snapshot itself
48826
+ // is a deep clone of draftStore, so no nested mutation reaches draftStore.
48827
+ actionArray.push({ ...cleanSnapshot[key] });
48828
+ }
48829
+ }
48830
+ else {
48831
+ // Killswitch (W-23515682): original pre-fix behavior — deep-clone every draft on
48832
+ // every read so the internal draft store is never exposed. O(N) clones per read.
48833
+ const { draftStore } = this;
48834
+ const keys$1 = keys$4(draftStore);
48835
+ for (let i = 0, len = keys$1.length; i < len; i++) {
48836
+ actionArray.push(clone$1(draftStore[keys$1[i]]));
48837
+ }
48769
48838
  }
48770
48839
  return actionArray;
48771
48840
  });
@@ -48775,8 +48844,9 @@ class DurableDraftStore {
48775
48844
  const draft = this.draftStore[id];
48776
48845
  if (draft !== undefined) {
48777
48846
  delete this.draftStore[id];
48847
+ delete this.cleanSnapshot[id];
48778
48848
  const durableKey = buildDraftDurableStoreKey(draft.tag, draft.id);
48779
- return this.durableStore.evictEntries([durableKey], DRAFT_SEGMENT);
48849
+ return this.timeDurableOp('evictEntries', DRAFT_SEGMENT, () => this.durableStore.evictEntries([durableKey], DRAFT_SEGMENT));
48780
48850
  }
48781
48851
  return Promise.resolve();
48782
48852
  };
@@ -48792,10 +48862,11 @@ class DurableDraftStore {
48792
48862
  const action = draftStore[key];
48793
48863
  if (action.tag === tag) {
48794
48864
  delete draftStore[action.id];
48865
+ delete this.cleanSnapshot[action.id];
48795
48866
  durableKeys.push(buildDraftDurableStoreKey(action.tag, action.id));
48796
48867
  }
48797
48868
  }
48798
- return this.durableStore.evictEntries(durableKeys, DRAFT_SEGMENT);
48869
+ return this.timeDurableOp('evictEntries', DRAFT_SEGMENT, () => this.durableStore.evictEntries(durableKeys, DRAFT_SEGMENT));
48799
48870
  };
48800
48871
  return this.enqueueAction(deleteAction);
48801
48872
  }
@@ -48809,6 +48880,7 @@ class DurableDraftStore {
48809
48880
  const action = draftStore[operation.id];
48810
48881
  if (action !== undefined) {
48811
48882
  delete draftStore[operation.id];
48883
+ delete this.cleanSnapshot[operation.id];
48812
48884
  const key = buildDraftDurableStoreKey(action.tag, action.id);
48813
48885
  durableStoreOperations.push({
48814
48886
  ids: [key],
@@ -48821,6 +48893,7 @@ class DurableDraftStore {
48821
48893
  const { action } = operation;
48822
48894
  const key = buildDraftDurableStoreKey(action.tag, action.id);
48823
48895
  draftStore[action.id] = action;
48896
+ this.cleanSnapshot[action.id] = clone$1(action);
48824
48897
  durableStoreOperations.push({
48825
48898
  type: 'setEntries',
48826
48899
  segment: DRAFT_SEGMENT,
@@ -48832,7 +48905,7 @@ class DurableDraftStore {
48832
48905
  });
48833
48906
  }
48834
48907
  }
48835
- return this.durableStore.batchOperations(durableStoreOperations);
48908
+ return this.timeDurableOp('batchOperations', DRAFT_SEGMENT, () => this.durableStore.batchOperations(durableStoreOperations));
48836
48909
  };
48837
48910
  return this.enqueueAction(action);
48838
48911
  }
@@ -48875,6 +48948,7 @@ class DurableDraftStore {
48875
48948
  .then((durableEntries) => {
48876
48949
  if (durableEntries === undefined) {
48877
48950
  this.draftStore = {};
48951
+ this.cleanSnapshot = {};
48878
48952
  return this.runQueuedOperations();
48879
48953
  }
48880
48954
  const { draftStore } = this;
@@ -48883,6 +48957,7 @@ class DurableDraftStore {
48883
48957
  const entry = durableEntries[keys$1[i]];
48884
48958
  const action = entry.data;
48885
48959
  draftStore[action.id] = action;
48960
+ this.cleanSnapshot[action.id] = clone$1(action);
48886
48961
  }
48887
48962
  return this.runQueuedOperations();
48888
48963
  })
@@ -80107,6 +80182,14 @@ const DRAFT_QUEUE_TOTAL_MERGE_ACTIONS_CALLS = 'draft-queue-total-mergeActions-ca
80107
80182
  // Used to triage the WOLI duplicate-record investigation in Splunk — correlate by draftId
80108
80183
  // with the other draft-queue events. @W-23428669
80109
80184
  const DRAFT_QUEUE_IDEMPOTENCY_KEY_ROTATED = 'draft-queue-idempotency-key-rotated';
80185
+ // Hotspot-attribution telemetry (@W-23515682). Operational counts/durations only — no
80186
+ // draft content, field values, or server record ids. These attribute the offline-drain wall-clock
80187
+ // to the O(N^2) hotspots (getQueueActions amplification, listener fan-out, durable-op latency).
80188
+ const DRAFT_QUEUE_GET_QUEUE_ACTIONS_COUNT = 'draft-queue-get-queue-actions-count';
80189
+ const DRAFT_QUEUE_GET_QUEUE_ACTIONS_DURATION = 'draft-queue-get-queue-actions-duration';
80190
+ const DRAFT_QUEUE_NOTIFY_CHANGED_LISTENERS_COUNT = 'draft-queue-notify-changed-listeners-count';
80191
+ const DRAFT_QUEUE_DURABLE_OP_COUNT = 'draft-queue-durable-op-count';
80192
+ const DRAFT_QUEUE_DURABLE_OP_DURATION = 'draft-queue-durable-op-duration';
80110
80193
  /** Content Document */
80111
80194
  const CREATE_CONTENT_DOCUMENT_AND_VERSION_TOTAL_SYNTHESIZE_CALLS = 'content-document-version-total-synthesize-calls';
80112
80195
  const CREATE_CONTENT_DOCUMENT_AND_VERSION_DRAFT_SYNTHESIZE_ERROR = 'create-content-document-version-draft-synthesize-error';
@@ -80199,6 +80282,33 @@ function reportDraftActionEvent(state, draftCount, message) {
80199
80282
  break;
80200
80283
  }
80201
80284
  }
80285
+ /**
80286
+ * @W-23515682: count (amplification — queue re-reads per drain) + duration histogram (the
80287
+ * pre/post signal for the getAllDrafts clone-on-write fix). actionCount is queue depth, never content.
80288
+ */
80289
+ function reportDraftQueueGetQueueActions(durationMs, actionCount) {
80290
+ const depthBuckets = [1, 2, 5, 10, 20, 50, 100, 250, 500];
80291
+ ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_GET_QUEUE_ACTIONS_COUNT);
80292
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_GET_QUEUE_ACTIONS_DURATION, durationMs);
80293
+ ldsMobileInstrumentation$2.bucketValue(DRAFT_QUEUE_GET_QUEUE_ACTIONS_COUNT + '-depth', actionCount, depthBuckets);
80294
+ }
80295
+ /**
80296
+ * @W-23515682: count per event type — listener fan-out / amplification per event.
80297
+ */
80298
+ function reportDraftQueueNotifyChangedListeners(eventType, listenerCount) {
80299
+ ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_NOTIFY_CHANGED_LISTENERS_COUNT, listenerCount, undefined, { eventType });
80300
+ }
80301
+ /**
80302
+ * @W-23515682: count + duration histogram by operation kind + segment — attributes the
80303
+ * client-CPU-vs-durable split of per-action processing time.
80304
+ */
80305
+ function reportDraftQueueDurableOperation(op, segment, durationMs) {
80306
+ ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_DURABLE_OP_COUNT, 1, undefined, {
80307
+ op,
80308
+ segment,
80309
+ });
80310
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_DURABLE_OP_DURATION, durationMs);
80311
+ }
80202
80312
  /**
80203
80313
  * Reports an exception thrown while uploading a draft action. The thrown error is
80204
80314
  * otherwise swallowed by the upload handler's retry path, leaving the failure
@@ -80682,14 +80792,45 @@ class AbstractResourceRequestActionHandler {
80682
80792
  let updatedActionTargetId = undefined;
80683
80793
  const { tag: queueActionTag, data: queueActionRequest, id: queueActionId, } = queueAction;
80684
80794
  let { basePath, body } = queueActionRequest;
80795
+ // Literal indexOf pre-check (no RegExp coercion, unlike the old String.search) +
80796
+ // a replace RegExp built only when a reference is present — the win over the old scan.
80685
80797
  let stringifiedBody = stringify$5(body);
80798
+ // Read the killswitch once for the whole scan (W-23515682) — avoids a
80799
+ // per-redirect gate lookup in this hot loop.
80800
+ const useOptimizedScan = draftQueueRedirectScanGate.isOpen({ fallback: true });
80686
80801
  // for each redirected ID/key we loop over the operation to see if it needs
80687
80802
  // to be updated
80688
80803
  for (const { draftId, draftKey, canonicalId, canonicalKey } of redirects) {
80689
- if (basePath.search(draftId) >= 0 || stringifiedBody.search(draftId) >= 0) {
80690
- basePath = basePath.replace(new RegExp(draftId, 'g'), canonicalId);
80691
- stringifiedBody = stringifiedBody.replace(new RegExp(draftId, 'g'), canonicalId);
80692
- queueOperationMutated = true;
80804
+ if (useOptimizedScan) {
80805
+ // basePath AND body are checked independently: a queued action can carry
80806
+ // the draft id in either or BOTH, and each occurrence must be rewritten.
80807
+ const basePathNeedsUpdate = basePath.indexOf(draftId) >= 0;
80808
+ const bodyNeedsUpdate = stringifiedBody.indexOf(draftId) >= 0;
80809
+ if (basePathNeedsUpdate || bodyNeedsUpdate) {
80810
+ // Escape metacharacters so the draft id matches literally. Salesforce
80811
+ // ids carry no regex metacharacters today, so this is defensive against
80812
+ // a future id-format change silently corrupting unrelated ids. W-23515682.
80813
+ const escapedDraftId = draftId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80814
+ const replaceRegex = new RegExp(escapedDraftId, 'g');
80815
+ if (basePathNeedsUpdate) {
80816
+ basePath = basePath.replace(replaceRegex, canonicalId);
80817
+ }
80818
+ if (bodyNeedsUpdate) {
80819
+ stringifiedBody = stringifiedBody.replace(replaceRegex, canonicalId);
80820
+ }
80821
+ queueOperationMutated = true;
80822
+ }
80823
+ }
80824
+ else {
80825
+ // Killswitch closed (W-23515682): original pre-fix scan — String.search
80826
+ // (coerces draftId to a RegExp) + a raw (unescaped) replace RegExp applied
80827
+ // to basePath and body together.
80828
+ if (basePath.search(draftId) >= 0 ||
80829
+ stringifiedBody.search(draftId) >= 0) {
80830
+ basePath = basePath.replace(new RegExp(draftId, 'g'), canonicalId);
80831
+ stringifiedBody = stringifiedBody.replace(new RegExp(draftId, 'g'), canonicalId);
80832
+ queueOperationMutated = true;
80833
+ }
80693
80834
  }
80694
80835
  // if the action is performed on a previous draft id, we need to replace the action
80695
80836
  // with a new one at the updated canonical key
@@ -90262,6 +90403,20 @@ function obtainFailureMessage(error) {
90262
90403
  }
90263
90404
  }
90264
90405
 
90406
+ // Mobile impl of the shared DraftQueueInstrumentation seam (@W-23515682). Forwards to
90407
+ // ldsMobileInstrumentation via metrics.ts; injected at construction (DraftQueueFactory).
90408
+ const draftQueueInstrumentation = {
90409
+ onGetQueueActions(durationMs, actionCount) {
90410
+ reportDraftQueueGetQueueActions(durationMs, actionCount);
90411
+ },
90412
+ onNotifyChangedListeners(eventType, listenerCount) {
90413
+ reportDraftQueueNotifyChangedListeners(eventType, listenerCount);
90414
+ },
90415
+ onDurableOperation(op, segment, durationMs) {
90416
+ reportDraftQueueDurableOperation(op, segment, durationMs);
90417
+ },
90418
+ };
90419
+
90265
90420
  // so eslint doesn't complain about nimbus
90266
90421
  /* global __nimbus */
90267
90422
  function buildLdsDraftQueue(durableStore) {
@@ -90270,7 +90425,8 @@ function buildLdsDraftQueue(durableStore) {
90270
90425
  __nimbus.plugins.LdsDraftQueue !== undefined) {
90271
90426
  return new NimbusDraftQueue();
90272
90427
  }
90273
- const draftQueue = new DurableDraftQueue(new DurableDraftStore(durableStore));
90428
+ const useSnapshot = draftQueueStoreSnapshotGate.isOpen({ fallback: true });
90429
+ const draftQueue = new DurableDraftQueue(new DurableDraftStore(durableStore, draftQueueInstrumentation, useSnapshot), draftQueueInstrumentation);
90274
90430
  return instrumentDraftQueue(draftQueue);
90275
90431
  }
90276
90432
 
@@ -93601,18 +93757,34 @@ class SideEffectStore {
93601
93757
  this.initialize();
93602
93758
  }
93603
93759
  async addEffects(effects) {
93760
+ const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
93604
93761
  const durableEntries = {};
93605
93762
  effects.forEach((effect) => {
93606
93763
  durableEntries[effect.uniqueId] = { data: effect };
93607
93764
  this.allEffects.set(effect.uniqueId, effect);
93765
+ if (incremental) {
93766
+ this.addEffectToKeyMap(effect);
93767
+ }
93608
93768
  });
93609
- this.regenerateKeyToEffectMap();
93769
+ // Killswitch (W-23515682): closed → original O(N) full rebuild on every add.
93770
+ if (!incremental) {
93771
+ this.regenerateKeyToEffectMap();
93772
+ }
93610
93773
  await this.durableStore.setEntries(durableEntries, SIDE_EFFECT_SEGMENT);
93611
93774
  }
93612
93775
  async removeEffects(effects) {
93776
+ const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
93613
93777
  const effectKeys = effects.map((e) => e.uniqueId);
93614
- effects.forEach((k) => this.allEffects.delete(k.uniqueId));
93615
- this.regenerateKeyToEffectMap();
93778
+ effects.forEach((effect) => {
93779
+ this.allEffects.delete(effect.uniqueId);
93780
+ if (incremental) {
93781
+ this.removeEffectFromKeyMap(effect);
93782
+ }
93783
+ });
93784
+ // Killswitch (W-23515682): closed → original O(N) full rebuild on every remove.
93785
+ if (!incremental) {
93786
+ this.regenerateKeyToEffectMap();
93787
+ }
93616
93788
  await this.durableStore.evictEntries(effectKeys, SIDE_EFFECT_SEGMENT);
93617
93789
  }
93618
93790
  getEffects(key) {
@@ -93647,6 +93819,39 @@ class SideEffectStore {
93647
93819
  });
93648
93820
  this.regenerateKeyToEffectMap();
93649
93821
  }
93822
+ addEffectToKeyMap(effect) {
93823
+ const recordEffects = this.keyToEffectMap.get(effect.key);
93824
+ if (recordEffects) {
93825
+ // Effects are keyed by uniqueId (mirroring allEffects.set / regenerateKeyToEffectMap):
93826
+ // a re-add of the same uniqueId must REPLACE the resident entry in place, not append a
93827
+ // duplicate. The action-completion replay path (setSideEffectsForActions on still-queued
93828
+ // actions) re-adds resident effects with no preceding remove; without this dedup a
93829
+ // record-field-increment would sit in the array twice and be applied twice (e.g. a
93830
+ // ProductConsumed QuantityOnHand -=5 becomes -=10). W-23515682.
93831
+ const index = recordEffects.findIndex((e) => e.uniqueId === effect.uniqueId);
93832
+ if (index !== -1) {
93833
+ recordEffects[index] = effect;
93834
+ }
93835
+ else {
93836
+ recordEffects.push(effect);
93837
+ }
93838
+ }
93839
+ else {
93840
+ this.keyToEffectMap.set(effect.key, [effect]);
93841
+ }
93842
+ }
93843
+ removeEffectFromKeyMap(effect) {
93844
+ const recordEffects = this.keyToEffectMap.get(effect.key);
93845
+ if (recordEffects) {
93846
+ const index = recordEffects.findIndex((e) => e.uniqueId === effect.uniqueId);
93847
+ if (index !== -1) {
93848
+ recordEffects.splice(index, 1);
93849
+ if (recordEffects.length === 0) {
93850
+ this.keyToEffectMap.delete(effect.key);
93851
+ }
93852
+ }
93853
+ }
93854
+ }
93650
93855
  regenerateKeyToEffectMap() {
93651
93856
  this.keyToEffectMap.clear();
93652
93857
  this.allEffects.forEach((effect) => {
@@ -96796,7 +97001,7 @@ function buildServiceDescriptor$b(luvio) {
96796
97001
  },
96797
97002
  };
96798
97003
  }
96799
- // version: 1.451.0-dev2-a5cd560c12
97004
+ // version: 1.452.0-bcd113b7fd
96800
97005
 
96801
97006
  /**
96802
97007
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -96822,7 +97027,7 @@ function buildServiceDescriptor$a(notifyRecordUpdateAvailable, getNormalizedLuvi
96822
97027
  },
96823
97028
  };
96824
97029
  }
96825
- // version: 1.451.0-dev2-a5cd560c12
97030
+ // version: 1.452.0-bcd113b7fd
96826
97031
 
96827
97032
  function findExecutableOperation$1(input) {
96828
97033
  const operations = input.query.definitions.filter(
@@ -99531,7 +99736,7 @@ register$1({
99531
99736
  id: '@salesforce/lds-network-adapter',
99532
99737
  instrument: instrument$2,
99533
99738
  });
99534
- // version: 1.451.0-dev2-a5cd560c12
99739
+ // version: 1.452.0-bcd113b7fd
99535
99740
 
99536
99741
  const { create: create$2, keys: keys$2 } = Object;
99537
99742
  const { stringify, parse } = JSON;
@@ -107066,7 +107271,7 @@ function registerCallback(cb) {
107066
107271
  cb(graphql_v1_import, graphql_imperative$1, graphql_imperative_legacy_v1_import, graphql_state_manager, useOneStoreGraphQL);
107067
107272
  }
107068
107273
  }
107069
- // version: 1.451.0-dev2-312b888699
107274
+ // version: 1.452.0-1ecfe8af95
107070
107275
 
107071
107276
  function createFragmentMap(documentNode) {
107072
107277
  const fragments = {};
@@ -136309,7 +136514,7 @@ register$1({
136309
136514
  configuration: { ...configurationForGraphQLAdapters$1 },
136310
136515
  instrument: instrument$1,
136311
136516
  });
136312
- // version: 1.451.0-dev2-312b888699
136517
+ // version: 1.452.0-1ecfe8af95
136313
136518
 
136314
136519
  // On core the unstable adapters are re-exported with different names,
136315
136520
  // we want to match them here.
@@ -136461,7 +136666,7 @@ withDefaultLuvio((luvio) => {
136461
136666
  unstable_graphQL_imperative = createImperativeAdapter(luvio, createInstrumentedAdapter(ldsAdapter, adapterMetadata), adapterMetadata);
136462
136667
  graphQLImperative = ldsAdapter;
136463
136668
  });
136464
- // version: 1.451.0-dev2-312b888699
136669
+ // version: 1.452.0-1ecfe8af95
136465
136670
 
136466
136671
  var gqlApi = /*#__PURE__*/Object.freeze({
136467
136672
  __proto__: null,
@@ -137260,7 +137465,7 @@ const callbacks$1 = [];
137260
137465
  function register(r) {
137261
137466
  callbacks$1.forEach((callback) => callback(r));
137262
137467
  }
137263
- // version: 1.451.0-dev2-a5cd560c12
137468
+ // version: 1.452.0-bcd113b7fd
137264
137469
 
137265
137470
  /**
137266
137471
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -138549,4 +138754,4 @@ const { luvio } = getRuntime();
138549
138754
  setDefaultLuvio({ luvio });
138550
138755
 
138551
138756
  export { createPrimingSession, draftManager, draftQueue, evictCacheRecordsByIds, evictExpiredCacheEntries, executeAdapter, executeMutatingAdapter, getImperativeAdapterNames, importLuvioAdapterModule, importOneStoreAdapterModule, invokeAdapter, invokeAdapterWithDraftToMerge, invokeAdapterWithDraftToReplace, invokeAdapterWithMetadata, nimbusDraftQueue, registerReportObserver, setMetadataTTL, setUiApiRecordTTL, stopEviction, subscribeToAdapter };
138552
- // version: 1.451.0-dev2-a5cd560c12
138757
+ // version: 1.452.0-bcd113b7fd
@@ -4282,7 +4282,7 @@
4282
4282
  }
4283
4283
  callbacks.push(callback);
4284
4284
  }
4285
- // version: 1.451.0-dev2-a5cd560c12
4285
+ // version: 1.452.0-bcd113b7fd
4286
4286
 
4287
4287
  // TODO [TD-0081508]: once that TD is fulfilled we can probably change this file
4288
4288
  function instrumentAdapter$1(createFunction, _metadata) {
@@ -5326,7 +5326,7 @@
5326
5326
  const { apiFamily, name } = metadata;
5327
5327
  return createGraphQLWireAdapterConstructor$1(adapter, `${apiFamily}.${name}`, luvio, astResolver);
5328
5328
  }
5329
- // version: 1.451.0-dev2-a5cd560c12
5329
+ // version: 1.452.0-bcd113b7fd
5330
5330
 
5331
5331
  function isSupportedEntity(_objectApiName) {
5332
5332
  return true;
@@ -32553,7 +32553,7 @@
32553
32553
  throttle(60, 60000, setupNotifyAllListRecordUpdateAvailable(luvio));
32554
32554
  throttle(60, 60000, setupNotifyAllListInfoSummaryUpdateAvailable(luvio));
32555
32555
  });
32556
- // version: 1.451.0-dev2-312b888699
32556
+ // version: 1.452.0-1ecfe8af95
32557
32557
 
32558
32558
  var draftQueueMaxRetryAttemptsGate = {
32559
32559
  isOpen: function (e) {
@@ -32564,6 +32564,15 @@
32564
32564
  },
32565
32565
  };
32566
32566
 
32567
+ var draftQueueRedirectScanGate = {
32568
+ isOpen: function (e) {
32569
+ return e.fallback;
32570
+ },
32571
+ hasError: function () {
32572
+ return !0;
32573
+ },
32574
+ };
32575
+
32567
32576
  var rotateIdempotencyKeyOn400Killswitch = {
32568
32577
  isOpen: function (e) {
32569
32578
  return e.fallback;
@@ -44109,6 +44118,15 @@
44109
44118
  },
44110
44119
  };
44111
44120
 
44121
+ var draftQueueStoreSnapshotGate = {
44122
+ isOpen: function (e) {
44123
+ return e.fallback;
44124
+ },
44125
+ hasError: function () {
44126
+ return !0;
44127
+ },
44128
+ };
44129
+
44112
44130
  var ldsAdapterO11yLoggingGate = {
44113
44131
  isOpen: function (e) {
44114
44132
  return e.fallback;
@@ -45898,6 +45916,15 @@
45898
45916
  return ok$3(document);
45899
45917
  }
45900
45918
 
45919
+ var draftQueueSideEffectMapGate = {
45920
+ isOpen: function (e) {
45921
+ return e.fallback;
45922
+ },
45923
+ hasError: function () {
45924
+ return !0;
45925
+ },
45926
+ };
45927
+
45901
45928
  var productConsumedSideEffects = {
45902
45929
  isOpen: function (e) {
45903
45930
  return e.fallback;
@@ -48220,7 +48247,7 @@
48220
48247
  }
48221
48248
  return handler;
48222
48249
  }
48223
- constructor(draftStore) {
48250
+ constructor(draftStore, instrumentation) {
48224
48251
  this.retryIntervalMilliseconds = 0;
48225
48252
  this.minimumRetryInterval = 250;
48226
48253
  this.maximumRetryInterval = 32000;
@@ -48237,6 +48264,7 @@
48237
48264
  this.handlers = {};
48238
48265
  this.draftStore = draftStore;
48239
48266
  this.workerPool = new AsyncWorkerPool(1);
48267
+ this.instrumentation = instrumentation;
48240
48268
  }
48241
48269
  addHandler(handler) {
48242
48270
  const id = handler.handlerId;
@@ -48327,15 +48355,22 @@
48327
48355
  this.state = DraftQueueState.Stopped;
48328
48356
  }
48329
48357
  async getQueueActions() {
48358
+ // W-23515682: time the whole call, including this queue's own internal
48359
+ // this.getQueueActions() during a drain — the amplification signal an external wrapper
48360
+ // cannot see. Measure-only; control flow below is unchanged.
48361
+ const { instrumentation } = this;
48362
+ const start = instrumentation?.onGetQueueActions !== undefined ? Date.now() : 0;
48330
48363
  const drafts = (await this.draftStore.getAllDrafts());
48331
48364
  const queue = [];
48332
48365
  drafts.forEach((draft) => {
48333
48366
  if (draft.id === this.uploadingActionId) {
48334
- draft.status = DraftActionStatus.Uploading;
48367
+ queue.push({ ...draft, status: DraftActionStatus.Uploading });
48368
+ }
48369
+ else {
48370
+ queue.push(draft);
48335
48371
  }
48336
- queue.push(draft);
48337
48372
  });
48338
- return queue.sort((a, b) => {
48373
+ const sorted = queue.sort((a, b) => {
48339
48374
  const aTime = parseInt(a.id, 10);
48340
48375
  const bTime = parseInt(b.id, 10);
48341
48376
  // safety check
@@ -48347,6 +48382,8 @@
48347
48382
  }
48348
48383
  return aTime - bTime;
48349
48384
  });
48385
+ instrumentation?.onGetQueueActions?.(Date.now() - start, sorted.length);
48386
+ return sorted;
48350
48387
  }
48351
48388
  async enqueue(handlerId, data, observabilityContext) {
48352
48389
  return this.workerPool.push({
@@ -48495,6 +48532,8 @@
48495
48532
  const listener = draftQueueChangedListeners[i];
48496
48533
  results.push(listener(event));
48497
48534
  }
48535
+ // W-23515682: listener fan-out per event type. Measure-only.
48536
+ this.instrumentation?.onNotifyChangedListeners?.(event.type, draftQueueLen);
48498
48537
  await Promise.all(results);
48499
48538
  }
48500
48539
  /**
@@ -48742,36 +48781,66 @@
48742
48781
  *
48743
48782
  */
48744
48783
  class DurableDraftStore {
48745
- constructor(durableStore) {
48784
+ constructor(durableStore, instrumentation, useSnapshot = true) {
48746
48785
  this.draftStore = {};
48786
+ // Snapshot: holds deep clones of each draft, refreshed at write time.
48787
+ // getAllDrafts serves from this snapshot with zero deep clones per read.
48788
+ this.cleanSnapshot = {};
48747
48789
  // queue of writes that were made during the initial sync
48748
48790
  this.writeQueue = [];
48749
48791
  this.durableStore = durableStore;
48792
+ this.instrumentation = instrumentation;
48793
+ this.useSnapshot = useSnapshot;
48750
48794
  this.resyncDraftStore();
48751
48795
  }
48796
+ // When no instrumentation is injected the promise is returned as-is (zero overhead).
48797
+ timeDurableOp(op, segment, run) {
48798
+ const onDurableOperation = this.instrumentation?.onDurableOperation;
48799
+ if (onDurableOperation === undefined) {
48800
+ return run();
48801
+ }
48802
+ const start = Date.now();
48803
+ return run().finally(() => {
48804
+ onDurableOperation(op, segment, Date.now() - start);
48805
+ });
48806
+ }
48752
48807
  writeAction(action) {
48753
48808
  const addAction = () => {
48754
48809
  const { id, tag } = action;
48755
48810
  this.draftStore[id] = action;
48811
+ this.cleanSnapshot[id] = clone$1(action);
48756
48812
  const durableEntryKey = buildDraftDurableStoreKey(tag, id);
48757
48813
  const entry = {
48758
48814
  data: action,
48759
48815
  };
48760
48816
  const entries = { [durableEntryKey]: entry };
48761
- return this.durableStore.setEntries(entries, DRAFT_SEGMENT);
48817
+ return this.timeDurableOp('setEntries', DRAFT_SEGMENT, () => this.durableStore.setEntries(entries, DRAFT_SEGMENT));
48762
48818
  };
48763
48819
  return this.enqueueAction(addAction);
48764
48820
  }
48765
48821
  getAllDrafts() {
48766
48822
  const waitForOngoingSync = this.syncPromise || Promise.resolve();
48767
48823
  return waitForOngoingSync.then(() => {
48768
- const { draftStore } = this;
48769
- const keys$1 = keys$4(draftStore);
48770
48824
  const actionArray = [];
48771
- for (let i = 0, len = keys$1.length; i < len; i++) {
48772
- const key = keys$1[i];
48773
- // clone draft so we don't expose the internal draft store
48774
- actionArray.push(clone$1(draftStore[key]));
48825
+ if (this.useSnapshot) {
48826
+ const { cleanSnapshot } = this;
48827
+ const keys$1 = keys$4(cleanSnapshot);
48828
+ for (let i = 0, len = keys$1.length; i < len; i++) {
48829
+ const key = keys$1[i];
48830
+ // Return shallow spread of snapshot entries so top-level caller mutations
48831
+ // (e.g., .status =) don't persist into the snapshot. The snapshot itself
48832
+ // is a deep clone of draftStore, so no nested mutation reaches draftStore.
48833
+ actionArray.push({ ...cleanSnapshot[key] });
48834
+ }
48835
+ }
48836
+ else {
48837
+ // Killswitch (W-23515682): original pre-fix behavior — deep-clone every draft on
48838
+ // every read so the internal draft store is never exposed. O(N) clones per read.
48839
+ const { draftStore } = this;
48840
+ const keys$1 = keys$4(draftStore);
48841
+ for (let i = 0, len = keys$1.length; i < len; i++) {
48842
+ actionArray.push(clone$1(draftStore[keys$1[i]]));
48843
+ }
48775
48844
  }
48776
48845
  return actionArray;
48777
48846
  });
@@ -48781,8 +48850,9 @@
48781
48850
  const draft = this.draftStore[id];
48782
48851
  if (draft !== undefined) {
48783
48852
  delete this.draftStore[id];
48853
+ delete this.cleanSnapshot[id];
48784
48854
  const durableKey = buildDraftDurableStoreKey(draft.tag, draft.id);
48785
- return this.durableStore.evictEntries([durableKey], DRAFT_SEGMENT);
48855
+ return this.timeDurableOp('evictEntries', DRAFT_SEGMENT, () => this.durableStore.evictEntries([durableKey], DRAFT_SEGMENT));
48786
48856
  }
48787
48857
  return Promise.resolve();
48788
48858
  };
@@ -48798,10 +48868,11 @@
48798
48868
  const action = draftStore[key];
48799
48869
  if (action.tag === tag) {
48800
48870
  delete draftStore[action.id];
48871
+ delete this.cleanSnapshot[action.id];
48801
48872
  durableKeys.push(buildDraftDurableStoreKey(action.tag, action.id));
48802
48873
  }
48803
48874
  }
48804
- return this.durableStore.evictEntries(durableKeys, DRAFT_SEGMENT);
48875
+ return this.timeDurableOp('evictEntries', DRAFT_SEGMENT, () => this.durableStore.evictEntries(durableKeys, DRAFT_SEGMENT));
48805
48876
  };
48806
48877
  return this.enqueueAction(deleteAction);
48807
48878
  }
@@ -48815,6 +48886,7 @@
48815
48886
  const action = draftStore[operation.id];
48816
48887
  if (action !== undefined) {
48817
48888
  delete draftStore[operation.id];
48889
+ delete this.cleanSnapshot[operation.id];
48818
48890
  const key = buildDraftDurableStoreKey(action.tag, action.id);
48819
48891
  durableStoreOperations.push({
48820
48892
  ids: [key],
@@ -48827,6 +48899,7 @@
48827
48899
  const { action } = operation;
48828
48900
  const key = buildDraftDurableStoreKey(action.tag, action.id);
48829
48901
  draftStore[action.id] = action;
48902
+ this.cleanSnapshot[action.id] = clone$1(action);
48830
48903
  durableStoreOperations.push({
48831
48904
  type: 'setEntries',
48832
48905
  segment: DRAFT_SEGMENT,
@@ -48838,7 +48911,7 @@
48838
48911
  });
48839
48912
  }
48840
48913
  }
48841
- return this.durableStore.batchOperations(durableStoreOperations);
48914
+ return this.timeDurableOp('batchOperations', DRAFT_SEGMENT, () => this.durableStore.batchOperations(durableStoreOperations));
48842
48915
  };
48843
48916
  return this.enqueueAction(action);
48844
48917
  }
@@ -48881,6 +48954,7 @@
48881
48954
  .then((durableEntries) => {
48882
48955
  if (durableEntries === undefined) {
48883
48956
  this.draftStore = {};
48957
+ this.cleanSnapshot = {};
48884
48958
  return this.runQueuedOperations();
48885
48959
  }
48886
48960
  const { draftStore } = this;
@@ -48889,6 +48963,7 @@
48889
48963
  const entry = durableEntries[keys$1[i]];
48890
48964
  const action = entry.data;
48891
48965
  draftStore[action.id] = action;
48966
+ this.cleanSnapshot[action.id] = clone$1(action);
48892
48967
  }
48893
48968
  return this.runQueuedOperations();
48894
48969
  })
@@ -80113,6 +80188,14 @@
80113
80188
  // Used to triage the WOLI duplicate-record investigation in Splunk — correlate by draftId
80114
80189
  // with the other draft-queue events. @W-23428669
80115
80190
  const DRAFT_QUEUE_IDEMPOTENCY_KEY_ROTATED = 'draft-queue-idempotency-key-rotated';
80191
+ // Hotspot-attribution telemetry (@W-23515682). Operational counts/durations only — no
80192
+ // draft content, field values, or server record ids. These attribute the offline-drain wall-clock
80193
+ // to the O(N^2) hotspots (getQueueActions amplification, listener fan-out, durable-op latency).
80194
+ const DRAFT_QUEUE_GET_QUEUE_ACTIONS_COUNT = 'draft-queue-get-queue-actions-count';
80195
+ const DRAFT_QUEUE_GET_QUEUE_ACTIONS_DURATION = 'draft-queue-get-queue-actions-duration';
80196
+ const DRAFT_QUEUE_NOTIFY_CHANGED_LISTENERS_COUNT = 'draft-queue-notify-changed-listeners-count';
80197
+ const DRAFT_QUEUE_DURABLE_OP_COUNT = 'draft-queue-durable-op-count';
80198
+ const DRAFT_QUEUE_DURABLE_OP_DURATION = 'draft-queue-durable-op-duration';
80116
80199
  /** Content Document */
80117
80200
  const CREATE_CONTENT_DOCUMENT_AND_VERSION_TOTAL_SYNTHESIZE_CALLS = 'content-document-version-total-synthesize-calls';
80118
80201
  const CREATE_CONTENT_DOCUMENT_AND_VERSION_DRAFT_SYNTHESIZE_ERROR = 'create-content-document-version-draft-synthesize-error';
@@ -80205,6 +80288,33 @@
80205
80288
  break;
80206
80289
  }
80207
80290
  }
80291
+ /**
80292
+ * @W-23515682: count (amplification — queue re-reads per drain) + duration histogram (the
80293
+ * pre/post signal for the getAllDrafts clone-on-write fix). actionCount is queue depth, never content.
80294
+ */
80295
+ function reportDraftQueueGetQueueActions(durationMs, actionCount) {
80296
+ const depthBuckets = [1, 2, 5, 10, 20, 50, 100, 250, 500];
80297
+ ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_GET_QUEUE_ACTIONS_COUNT);
80298
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_GET_QUEUE_ACTIONS_DURATION, durationMs);
80299
+ ldsMobileInstrumentation$2.bucketValue(DRAFT_QUEUE_GET_QUEUE_ACTIONS_COUNT + '-depth', actionCount, depthBuckets);
80300
+ }
80301
+ /**
80302
+ * @W-23515682: count per event type — listener fan-out / amplification per event.
80303
+ */
80304
+ function reportDraftQueueNotifyChangedListeners(eventType, listenerCount) {
80305
+ ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_NOTIFY_CHANGED_LISTENERS_COUNT, listenerCount, undefined, { eventType });
80306
+ }
80307
+ /**
80308
+ * @W-23515682: count + duration histogram by operation kind + segment — attributes the
80309
+ * client-CPU-vs-durable split of per-action processing time.
80310
+ */
80311
+ function reportDraftQueueDurableOperation(op, segment, durationMs) {
80312
+ ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_DURABLE_OP_COUNT, 1, undefined, {
80313
+ op,
80314
+ segment,
80315
+ });
80316
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_DURABLE_OP_DURATION, durationMs);
80317
+ }
80208
80318
  /**
80209
80319
  * Reports an exception thrown while uploading a draft action. The thrown error is
80210
80320
  * otherwise swallowed by the upload handler's retry path, leaving the failure
@@ -80688,14 +80798,45 @@
80688
80798
  let updatedActionTargetId = undefined;
80689
80799
  const { tag: queueActionTag, data: queueActionRequest, id: queueActionId, } = queueAction;
80690
80800
  let { basePath, body } = queueActionRequest;
80801
+ // Literal indexOf pre-check (no RegExp coercion, unlike the old String.search) +
80802
+ // a replace RegExp built only when a reference is present — the win over the old scan.
80691
80803
  let stringifiedBody = stringify$5(body);
80804
+ // Read the killswitch once for the whole scan (W-23515682) — avoids a
80805
+ // per-redirect gate lookup in this hot loop.
80806
+ const useOptimizedScan = draftQueueRedirectScanGate.isOpen({ fallback: true });
80692
80807
  // for each redirected ID/key we loop over the operation to see if it needs
80693
80808
  // to be updated
80694
80809
  for (const { draftId, draftKey, canonicalId, canonicalKey } of redirects) {
80695
- if (basePath.search(draftId) >= 0 || stringifiedBody.search(draftId) >= 0) {
80696
- basePath = basePath.replace(new RegExp(draftId, 'g'), canonicalId);
80697
- stringifiedBody = stringifiedBody.replace(new RegExp(draftId, 'g'), canonicalId);
80698
- queueOperationMutated = true;
80810
+ if (useOptimizedScan) {
80811
+ // basePath AND body are checked independently: a queued action can carry
80812
+ // the draft id in either or BOTH, and each occurrence must be rewritten.
80813
+ const basePathNeedsUpdate = basePath.indexOf(draftId) >= 0;
80814
+ const bodyNeedsUpdate = stringifiedBody.indexOf(draftId) >= 0;
80815
+ if (basePathNeedsUpdate || bodyNeedsUpdate) {
80816
+ // Escape metacharacters so the draft id matches literally. Salesforce
80817
+ // ids carry no regex metacharacters today, so this is defensive against
80818
+ // a future id-format change silently corrupting unrelated ids. W-23515682.
80819
+ const escapedDraftId = draftId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80820
+ const replaceRegex = new RegExp(escapedDraftId, 'g');
80821
+ if (basePathNeedsUpdate) {
80822
+ basePath = basePath.replace(replaceRegex, canonicalId);
80823
+ }
80824
+ if (bodyNeedsUpdate) {
80825
+ stringifiedBody = stringifiedBody.replace(replaceRegex, canonicalId);
80826
+ }
80827
+ queueOperationMutated = true;
80828
+ }
80829
+ }
80830
+ else {
80831
+ // Killswitch closed (W-23515682): original pre-fix scan — String.search
80832
+ // (coerces draftId to a RegExp) + a raw (unescaped) replace RegExp applied
80833
+ // to basePath and body together.
80834
+ if (basePath.search(draftId) >= 0 ||
80835
+ stringifiedBody.search(draftId) >= 0) {
80836
+ basePath = basePath.replace(new RegExp(draftId, 'g'), canonicalId);
80837
+ stringifiedBody = stringifiedBody.replace(new RegExp(draftId, 'g'), canonicalId);
80838
+ queueOperationMutated = true;
80839
+ }
80699
80840
  }
80700
80841
  // if the action is performed on a previous draft id, we need to replace the action
80701
80842
  // with a new one at the updated canonical key
@@ -90268,6 +90409,20 @@
90268
90409
  }
90269
90410
  }
90270
90411
 
90412
+ // Mobile impl of the shared DraftQueueInstrumentation seam (@W-23515682). Forwards to
90413
+ // ldsMobileInstrumentation via metrics.ts; injected at construction (DraftQueueFactory).
90414
+ const draftQueueInstrumentation = {
90415
+ onGetQueueActions(durationMs, actionCount) {
90416
+ reportDraftQueueGetQueueActions(durationMs, actionCount);
90417
+ },
90418
+ onNotifyChangedListeners(eventType, listenerCount) {
90419
+ reportDraftQueueNotifyChangedListeners(eventType, listenerCount);
90420
+ },
90421
+ onDurableOperation(op, segment, durationMs) {
90422
+ reportDraftQueueDurableOperation(op, segment, durationMs);
90423
+ },
90424
+ };
90425
+
90271
90426
  // so eslint doesn't complain about nimbus
90272
90427
  /* global __nimbus */
90273
90428
  function buildLdsDraftQueue(durableStore) {
@@ -90276,7 +90431,8 @@
90276
90431
  __nimbus.plugins.LdsDraftQueue !== undefined) {
90277
90432
  return new NimbusDraftQueue();
90278
90433
  }
90279
- const draftQueue = new DurableDraftQueue(new DurableDraftStore(durableStore));
90434
+ const useSnapshot = draftQueueStoreSnapshotGate.isOpen({ fallback: true });
90435
+ const draftQueue = new DurableDraftQueue(new DurableDraftStore(durableStore, draftQueueInstrumentation, useSnapshot), draftQueueInstrumentation);
90280
90436
  return instrumentDraftQueue(draftQueue);
90281
90437
  }
90282
90438
 
@@ -93607,18 +93763,34 @@
93607
93763
  this.initialize();
93608
93764
  }
93609
93765
  async addEffects(effects) {
93766
+ const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
93610
93767
  const durableEntries = {};
93611
93768
  effects.forEach((effect) => {
93612
93769
  durableEntries[effect.uniqueId] = { data: effect };
93613
93770
  this.allEffects.set(effect.uniqueId, effect);
93771
+ if (incremental) {
93772
+ this.addEffectToKeyMap(effect);
93773
+ }
93614
93774
  });
93615
- this.regenerateKeyToEffectMap();
93775
+ // Killswitch (W-23515682): closed → original O(N) full rebuild on every add.
93776
+ if (!incremental) {
93777
+ this.regenerateKeyToEffectMap();
93778
+ }
93616
93779
  await this.durableStore.setEntries(durableEntries, SIDE_EFFECT_SEGMENT);
93617
93780
  }
93618
93781
  async removeEffects(effects) {
93782
+ const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
93619
93783
  const effectKeys = effects.map((e) => e.uniqueId);
93620
- effects.forEach((k) => this.allEffects.delete(k.uniqueId));
93621
- this.regenerateKeyToEffectMap();
93784
+ effects.forEach((effect) => {
93785
+ this.allEffects.delete(effect.uniqueId);
93786
+ if (incremental) {
93787
+ this.removeEffectFromKeyMap(effect);
93788
+ }
93789
+ });
93790
+ // Killswitch (W-23515682): closed → original O(N) full rebuild on every remove.
93791
+ if (!incremental) {
93792
+ this.regenerateKeyToEffectMap();
93793
+ }
93622
93794
  await this.durableStore.evictEntries(effectKeys, SIDE_EFFECT_SEGMENT);
93623
93795
  }
93624
93796
  getEffects(key) {
@@ -93653,6 +93825,39 @@
93653
93825
  });
93654
93826
  this.regenerateKeyToEffectMap();
93655
93827
  }
93828
+ addEffectToKeyMap(effect) {
93829
+ const recordEffects = this.keyToEffectMap.get(effect.key);
93830
+ if (recordEffects) {
93831
+ // Effects are keyed by uniqueId (mirroring allEffects.set / regenerateKeyToEffectMap):
93832
+ // a re-add of the same uniqueId must REPLACE the resident entry in place, not append a
93833
+ // duplicate. The action-completion replay path (setSideEffectsForActions on still-queued
93834
+ // actions) re-adds resident effects with no preceding remove; without this dedup a
93835
+ // record-field-increment would sit in the array twice and be applied twice (e.g. a
93836
+ // ProductConsumed QuantityOnHand -=5 becomes -=10). W-23515682.
93837
+ const index = recordEffects.findIndex((e) => e.uniqueId === effect.uniqueId);
93838
+ if (index !== -1) {
93839
+ recordEffects[index] = effect;
93840
+ }
93841
+ else {
93842
+ recordEffects.push(effect);
93843
+ }
93844
+ }
93845
+ else {
93846
+ this.keyToEffectMap.set(effect.key, [effect]);
93847
+ }
93848
+ }
93849
+ removeEffectFromKeyMap(effect) {
93850
+ const recordEffects = this.keyToEffectMap.get(effect.key);
93851
+ if (recordEffects) {
93852
+ const index = recordEffects.findIndex((e) => e.uniqueId === effect.uniqueId);
93853
+ if (index !== -1) {
93854
+ recordEffects.splice(index, 1);
93855
+ if (recordEffects.length === 0) {
93856
+ this.keyToEffectMap.delete(effect.key);
93857
+ }
93858
+ }
93859
+ }
93860
+ }
93656
93861
  regenerateKeyToEffectMap() {
93657
93862
  this.keyToEffectMap.clear();
93658
93863
  this.allEffects.forEach((effect) => {
@@ -96802,7 +97007,7 @@
96802
97007
  },
96803
97008
  };
96804
97009
  }
96805
- // version: 1.451.0-dev2-a5cd560c12
97010
+ // version: 1.452.0-bcd113b7fd
96806
97011
 
96807
97012
  /**
96808
97013
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -96828,7 +97033,7 @@
96828
97033
  },
96829
97034
  };
96830
97035
  }
96831
- // version: 1.451.0-dev2-a5cd560c12
97036
+ // version: 1.452.0-bcd113b7fd
96832
97037
 
96833
97038
  function findExecutableOperation$1(input) {
96834
97039
  const operations = input.query.definitions.filter(
@@ -99537,7 +99742,7 @@
99537
99742
  id: '@salesforce/lds-network-adapter',
99538
99743
  instrument: instrument$2,
99539
99744
  });
99540
- // version: 1.451.0-dev2-a5cd560c12
99745
+ // version: 1.452.0-bcd113b7fd
99541
99746
 
99542
99747
  const { create: create$2, keys: keys$2 } = Object;
99543
99748
  const { stringify, parse } = JSON;
@@ -107072,7 +107277,7 @@
107072
107277
  cb(graphql_v1_import, graphql_imperative$1, graphql_imperative_legacy_v1_import, graphql_state_manager, useOneStoreGraphQL);
107073
107278
  }
107074
107279
  }
107075
- // version: 1.451.0-dev2-312b888699
107280
+ // version: 1.452.0-1ecfe8af95
107076
107281
 
107077
107282
  function createFragmentMap(documentNode) {
107078
107283
  const fragments = {};
@@ -136315,7 +136520,7 @@
136315
136520
  configuration: { ...configurationForGraphQLAdapters$1 },
136316
136521
  instrument: instrument$1,
136317
136522
  });
136318
- // version: 1.451.0-dev2-312b888699
136523
+ // version: 1.452.0-1ecfe8af95
136319
136524
 
136320
136525
  // On core the unstable adapters are re-exported with different names,
136321
136526
  // we want to match them here.
@@ -136467,7 +136672,7 @@
136467
136672
  unstable_graphQL_imperative = createImperativeAdapter(luvio, createInstrumentedAdapter(ldsAdapter, adapterMetadata), adapterMetadata);
136468
136673
  graphQLImperative = ldsAdapter;
136469
136674
  });
136470
- // version: 1.451.0-dev2-312b888699
136675
+ // version: 1.452.0-1ecfe8af95
136471
136676
 
136472
136677
  var gqlApi = /*#__PURE__*/Object.freeze({
136473
136678
  __proto__: null,
@@ -137266,7 +137471,7 @@
137266
137471
  function register(r) {
137267
137472
  callbacks$1.forEach((callback) => callback(r));
137268
137473
  }
137269
- // version: 1.451.0-dev2-a5cd560c12
137474
+ // version: 1.452.0-bcd113b7fd
137270
137475
 
137271
137476
  /**
137272
137477
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -138576,4 +138781,4 @@
138576
138781
  exports.subscribeToAdapter = subscribeToAdapter;
138577
138782
 
138578
138783
  }));
138579
- // version: 1.451.0-dev2-a5cd560c12
138784
+ // version: 1.452.0-bcd113b7fd
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-worker-api",
3
- "version": "1.451.0-dev2",
3
+ "version": "1.452.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "",
6
6
  "main": "dist/standalone/es/lds-worker-api.js",
@@ -35,14 +35,14 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@oat-sa/rollup-plugin-wildcard-external": "^1.0.0",
38
- "@salesforce/lds-adapters-graphql": "^1.451.0-dev2",
39
- "@salesforce/lds-adapters-uiapi": "^1.451.0-dev2",
40
- "@salesforce/lds-default-luvio": "^1.451.0-dev2",
41
- "@salesforce/lds-drafts": "^1.451.0-dev2",
42
- "@salesforce/lds-graphql-parser": "^1.451.0-dev2",
43
- "@salesforce/lds-luvio-engine": "^1.451.0-dev2",
44
- "@salesforce/lds-runtime-mobile": "^1.451.0-dev2",
45
- "@salesforce/nimbus-plugin-lds": "^1.451.0-dev2",
38
+ "@salesforce/lds-adapters-graphql": "^1.452.0",
39
+ "@salesforce/lds-adapters-uiapi": "^1.452.0",
40
+ "@salesforce/lds-default-luvio": "^1.452.0",
41
+ "@salesforce/lds-drafts": "^1.452.0",
42
+ "@salesforce/lds-graphql-parser": "^1.452.0",
43
+ "@salesforce/lds-luvio-engine": "^1.452.0",
44
+ "@salesforce/lds-runtime-mobile": "^1.452.0",
45
+ "@salesforce/nimbus-plugin-lds": "^1.452.0",
46
46
  "ajv": "^8.11.0",
47
47
  "glob": "^7.1.5",
48
48
  "nimbus-types": "^2.0.0-alpha1",