@salesforce/lds-worker-api 1.454.0 → 1.455.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.
@@ -4282,7 +4282,7 @@
4282
4282
  }
4283
4283
  callbacks.push(callback);
4284
4284
  }
4285
- // version: 1.454.0-4223ab4365
4285
+ // version: 1.455.0-c53c97de2a
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.454.0-4223ab4365
5329
+ // version: 1.455.0-c53c97de2a
5330
5330
 
5331
5331
  function isSupportedEntity(_objectApiName) {
5332
5332
  return true;
@@ -5436,7 +5436,7 @@
5436
5436
 
5437
5437
  const { keys: ObjectKeys$4, create: ObjectCreate$4 } = Object;
5438
5438
 
5439
- const { assign: assign$7, create: create$9, freeze: freeze$6, isFrozen: isFrozen$4, keys: keys$a } = Object;
5439
+ const { assign: assign$7, create: create$9, freeze: freeze$6, getPrototypeOf: getPrototypeOf$3, isFrozen: isFrozen$4, keys: keys$a } = Object;
5440
5440
 
5441
5441
  /**
5442
5442
  * Defines configuration for the module with a default value which can be overridden by the runtime environment.
@@ -6071,7 +6071,7 @@
6071
6071
  }
6072
6072
  const keyPrefix$3 = 'UiApi';
6073
6073
 
6074
- const { assign: assign$6, create: create$8, freeze: freeze$5, isFrozen: isFrozen$3, keys: keys$9 } = Object;
6074
+ const { assign: assign$6, create: create$8, freeze: freeze$5, getPrototypeOf: getPrototypeOf$2, isFrozen: isFrozen$3, keys: keys$9 } = Object;
6075
6075
  const { hasOwnProperty: hasOwnProperty$5 } = Object.prototype;
6076
6076
  const { split: split$1, endsWith: endsWith$1 } = String.prototype;
6077
6077
  const { isArray: isArray$8 } = Array;
@@ -32553,7 +32553,7 @@
32553
32553
  throttle(60, 60000, setupNotifyAllListRecordUpdateAvailable(luvio));
32554
32554
  throttle(60, 60000, setupNotifyAllListInfoSummaryUpdateAvailable(luvio));
32555
32555
  });
32556
- // version: 1.454.0-761808c4e9
32556
+ // version: 1.455.0-96e9b41a18
32557
32557
 
32558
32558
  var draftQueueMaxRetryAttemptsGate = {
32559
32559
  isOpen: function (e) {
@@ -47855,16 +47855,18 @@
47855
47855
  }
47856
47856
 
47857
47857
  class AsyncWorkerPool {
47858
- constructor(concurrency) {
47858
+ constructor(concurrency, onWorkTiming) {
47859
47859
  this.queue = [];
47860
47860
  this.activeWork = [];
47861
47861
  this.concurrency = concurrency;
47862
+ this.onWorkTiming = onWorkTiming;
47862
47863
  }
47863
47864
  push(work) {
47864
47865
  return new Promise((resolve, reject) => {
47865
47866
  this.queue.push({
47866
47867
  ...work,
47867
47868
  workFn: (abortController) => work.workFn(abortController).then(resolve).catch(reject),
47869
+ enqueuedAt: Date.now(),
47868
47870
  });
47869
47871
  this.doWork();
47870
47872
  });
@@ -47907,10 +47909,30 @@
47907
47909
  const abortController = new LdsAbortController();
47908
47910
  const newWork = { ...work, abortController };
47909
47911
  this.activeWork.push(newWork);
47910
- const { workFn } = work;
47912
+ const { workFn, enqueuedAt } = work;
47913
+ const onWorkTiming = this.onWorkTiming;
47914
+ let startedAt = 0;
47915
+ let waitMs = 0;
47916
+ let queueDepthAtStart = 0;
47917
+ if (onWorkTiming !== undefined) {
47918
+ startedAt = Date.now();
47919
+ waitMs = startedAt - enqueuedAt;
47920
+ // depth AFTER this unit was shifted off — units still waiting behind it.
47921
+ queueDepthAtStart = this.queue.length;
47922
+ }
47911
47923
  workFn(abortController).finally(() => {
47912
- this.activeWork = this.activeWork.filter((w) => w !== newWork);
47913
- this.doWork();
47924
+ try {
47925
+ if (onWorkTiming !== undefined) {
47926
+ onWorkTiming(waitMs, Date.now() - startedAt, queueDepthAtStart);
47927
+ }
47928
+ }
47929
+ catch {
47930
+ // Instrumentation must not prevent the pool from releasing the worker slot.
47931
+ }
47932
+ finally {
47933
+ this.activeWork = this.activeWork.filter((w) => w !== newWork);
47934
+ this.doWork();
47935
+ }
47914
47936
  });
47915
47937
  }
47916
47938
  }
@@ -48240,6 +48262,25 @@
48240
48262
  */
48241
48263
  const DRAFT_ACTION_RETRY_COUNT_METADATA_KEY = 'retryCount';
48242
48264
  class DurableDraftQueue {
48265
+ // Instrumentation is strictly measure-only: a broken sink must never change queue behavior.
48266
+ // Pass the sink into the callback so methods are invoked through their owning object and retain
48267
+ // their receiver (some sinks use `this` to reach their metric registry).
48268
+ emitInstrumentation(emit) {
48269
+ const { instrumentation } = this;
48270
+ if (instrumentation === undefined) {
48271
+ return;
48272
+ }
48273
+ try {
48274
+ emit(instrumentation);
48275
+ }
48276
+ catch {
48277
+ // Telemetry failures must not interrupt draft processing.
48278
+ }
48279
+ }
48280
+ resetDrainCycle() {
48281
+ this.drainCycleStart = undefined;
48282
+ this.drainCycleBatchCount = 0;
48283
+ }
48243
48284
  getHandler(id) {
48244
48285
  const handler = this.handlers[id];
48245
48286
  if (handler === undefined) {
@@ -48262,9 +48303,19 @@
48262
48303
  ? __nimbus.plugins.JSLoggerPlugin
48263
48304
  : undefined;
48264
48305
  this.handlers = {};
48306
+ // Drain-wait attribution state. uploadStartTimes stamps when each action first began
48307
+ // uploading (keyed by action id) so actionCompleted/failed can emit its end-to-end drain time,
48308
+ // including automatic retries/backoff. actionQueueWaitEmittedIds ensures those retries do not
48309
+ // report the original enqueue wait more than once. drainCycleStart/BatchCount track one full
48310
+ // idle->empty sweep. All measure-only; when no instrumentation is injected these stay untouched.
48311
+ this.uploadStartTimes = new Map();
48312
+ this.actionQueueWaitEmittedIds = new Set();
48313
+ this.drainCycleBatchCount = 0;
48265
48314
  this.draftStore = draftStore;
48266
- this.workerPool = new AsyncWorkerPool(1);
48267
48315
  this.instrumentation = instrumentation;
48316
+ this.workerPool = new AsyncWorkerPool(1, instrumentation?.onWorkerTiming !== undefined
48317
+ ? (waitMs, holdMs, queueDepth) => this.emitInstrumentation((sink) => sink.onWorkerTiming?.(waitMs, holdMs, queueDepth))
48318
+ : undefined);
48268
48319
  }
48269
48320
  addHandler(handler) {
48270
48321
  const id = handler.handlerId;
@@ -48352,6 +48403,9 @@
48352
48403
  clearTimeout(this.timeoutHandler);
48353
48404
  this.timeoutHandler = undefined;
48354
48405
  }
48406
+ // A stop/user edit breaks the current sweep. A later restart starts a fresh cycle rather
48407
+ // than attributing stopped time to an offline drain.
48408
+ this.resetDrainCycle();
48355
48409
  this.state = DraftQueueState.Stopped;
48356
48410
  }
48357
48411
  async getQueueActions() {
@@ -48382,7 +48436,7 @@
48382
48436
  }
48383
48437
  return aTime - bTime;
48384
48438
  });
48385
- instrumentation?.onGetQueueActions?.(Date.now() - start, sorted.length);
48439
+ this.emitInstrumentation((sink) => sink.onGetQueueActions?.(Date.now() - start, sorted.length));
48386
48440
  return sorted;
48387
48441
  }
48388
48442
  async enqueue(handlerId, data, observabilityContext) {
@@ -48426,6 +48480,13 @@
48426
48480
  await handler.handleActionCompleted(action, queueOperations, values$2(this.handlers));
48427
48481
  this.retryIntervalMilliseconds = 0;
48428
48482
  this.uploadingActionId = undefined;
48483
+ // The action finished draining — emit its end-to-end drain time and
48484
+ // count it toward the current drain-cycle batch. Measure-only.
48485
+ this.emitActionDrainDuration(action.id, action.handler);
48486
+ this.actionQueueWaitEmittedIds.delete(action.id);
48487
+ if (this.drainCycleStart !== undefined) {
48488
+ this.drainCycleBatchCount += 1;
48489
+ }
48429
48490
  await this.notifyChangedListeners({
48430
48491
  type: DraftQueueEventType.ActionCompleted,
48431
48492
  action,
@@ -48437,6 +48498,16 @@
48437
48498
  },
48438
48499
  });
48439
48500
  }
48501
+ // Read+clear the action's upload-start stamp and emit its drain duration. Safe to
48502
+ // call when instrumentation is absent or the id was never stamped (no-op). Never emits the id.
48503
+ emitActionDrainDuration(id, handlerId) {
48504
+ const start = this.uploadStartTimes.get(id);
48505
+ if (start === undefined) {
48506
+ return;
48507
+ }
48508
+ this.uploadStartTimes.delete(id);
48509
+ this.emitInstrumentation((sink) => sink.onActionDrainDuration?.(handlerId, Date.now() - start));
48510
+ }
48440
48511
  async actionFailed(action, retry, retryDelayInMs, actionDataChanged) {
48441
48512
  if (actionDataChanged === true) {
48442
48513
  await this.draftStore.writeAction({
@@ -48446,14 +48517,24 @@
48446
48517
  }
48447
48518
  this.uploadingActionId = undefined;
48448
48519
  if (retry && this.state !== DraftQueueState.Stopped) {
48520
+ // A retry means this action is NOT done draining. Keep its first-upload stamp and the
48521
+ // open cycle so the eventual outcome includes network time and retry/backoff.
48449
48522
  this.state = DraftQueueState.Waiting;
48450
48523
  return retryDelayInMs !== undefined
48451
48524
  ? this.scheduleRetryWithSpecifiedDelay(retryDelayInMs)
48452
48525
  : this.scheduleRetry();
48453
48526
  }
48454
48527
  else if (isDraftError(action)) {
48528
+ // Terminal failure — this action is done draining. Emit its drain time.
48529
+ this.emitActionDrainDuration(action.id, action.handler);
48530
+ // This was not a clean drain-to-empty. Discard the sweep before user intervention so a
48531
+ // later manual retry/removal cannot inherit its elapsed time or completed batch count.
48532
+ this.resetDrainCycle();
48455
48533
  return this.handleServerError(action, action.error);
48456
48534
  }
48535
+ // Any other terminal path — ensure no stale start stamp lingers.
48536
+ this.emitActionDrainDuration(action.id, action.handler);
48537
+ this.resetDrainCycle();
48457
48538
  }
48458
48539
  handle(action) {
48459
48540
  const handler = this.getHandler(action.handler);
@@ -48473,12 +48554,22 @@
48473
48554
  this.state = DraftQueueState.Started;
48474
48555
  }
48475
48556
  this.processingAction = undefined;
48557
+ // The queue drained empty — close the drain-cycle window if one was open.
48558
+ if (this.drainCycleStart !== undefined) {
48559
+ const durationMs = Date.now() - this.drainCycleStart;
48560
+ const batchSize = this.drainCycleBatchCount;
48561
+ this.resetDrainCycle();
48562
+ this.emitInstrumentation((sink) => sink.onDrainCycle?.(durationMs, batchSize));
48563
+ }
48476
48564
  return ProcessActionResult.NO_ACTION_TO_PROCESS;
48477
48565
  }
48478
48566
  const { status, id } = action;
48479
48567
  if (status === DraftActionStatus.Error) {
48480
48568
  this.state = DraftQueueState.Error;
48481
48569
  this.processingAction = undefined;
48570
+ // The sweep is blocked on an errored action, not a clean drain-to-empty —
48571
+ // discard the open drain-cycle window so it doesn't inflate a later cycle's duration.
48572
+ this.resetDrainCycle();
48482
48573
  await this.notifyChangedListeners({
48483
48574
  type: DraftQueueEventType.ActionFailed,
48484
48575
  action: action,
@@ -48496,6 +48587,31 @@
48496
48587
  if (this.state === DraftQueueState.Waiting) {
48497
48588
  this.state = DraftQueueState.Started;
48498
48589
  }
48590
+ // The action is now leaving the queue to upload. Emit head-of-line FIFO wait
48591
+ // (enqueue -> upload start), stamp its drain-start for end-to-end timing on completion, and
48592
+ // open a drain-cycle window if the queue was previously idle. All measure-only.
48593
+ const { instrumentation } = this;
48594
+ if (instrumentation !== undefined) {
48595
+ const now = Date.now();
48596
+ const { timestamp } = action;
48597
+ if (instrumentation.onActionQueueWait !== undefined &&
48598
+ !this.actionQueueWaitEmittedIds.has(id) &&
48599
+ typeof timestamp === 'number' &&
48600
+ timestamp > 0) {
48601
+ // Mark before invoking the sink: even a throwing callback must not make a retry
48602
+ // report the same action's enqueue wait again.
48603
+ this.actionQueueWaitEmittedIds.add(id);
48604
+ this.emitInstrumentation((sink) => sink.onActionQueueWait?.(action.handler, now - timestamp));
48605
+ }
48606
+ if (instrumentation.onActionDrainDuration !== undefined &&
48607
+ !this.uploadStartTimes.has(id)) {
48608
+ this.uploadStartTimes.set(id, now);
48609
+ }
48610
+ if (instrumentation.onDrainCycle !== undefined && this.drainCycleStart === undefined) {
48611
+ this.drainCycleStart = now;
48612
+ this.drainCycleBatchCount = 0;
48613
+ }
48614
+ }
48499
48615
  await this.notifyChangedListeners({
48500
48616
  type: DraftQueueEventType.ActionUploading,
48501
48617
  action: { ...action, status: DraftActionStatus.Uploading },
@@ -48533,7 +48649,7 @@
48533
48649
  results.push(listener(event));
48534
48650
  }
48535
48651
  // W-23515682: listener fan-out per event type. Measure-only.
48536
- this.instrumentation?.onNotifyChangedListeners?.(event.type, draftQueueLen);
48652
+ this.emitInstrumentation((sink) => sink.onNotifyChangedListeners?.(event.type, draftQueueLen));
48537
48653
  await Promise.all(results);
48538
48654
  }
48539
48655
  /**
@@ -48562,9 +48678,17 @@
48562
48678
  const shouldDeleteRelated = handler.shouldDeleteActionByTagOnRemoval(action);
48563
48679
  if (shouldDeleteRelated) {
48564
48680
  await this.draftStore.deleteByTag(action.tag);
48681
+ queue.forEach((queuedAction) => {
48682
+ if (queuedAction.tag === action.tag) {
48683
+ this.uploadStartTimes.delete(queuedAction.id);
48684
+ this.actionQueueWaitEmittedIds.delete(queuedAction.id);
48685
+ }
48686
+ });
48565
48687
  }
48566
48688
  else {
48567
48689
  await this.draftStore.deleteDraft(action.id);
48690
+ this.uploadStartTimes.delete(action.id);
48691
+ this.actionQueueWaitEmittedIds.delete(action.id);
48568
48692
  }
48569
48693
  await handler.handleActionRemoved(action, queue.filter((x) => x.id !== actionId));
48570
48694
  await this.notifyChangedListeners({
@@ -80196,6 +80320,20 @@
80196
80320
  const DRAFT_QUEUE_NOTIFY_CHANGED_LISTENERS_COUNT = 'draft-queue-notify-changed-listeners-count';
80197
80321
  const DRAFT_QUEUE_DURABLE_OP_COUNT = 'draft-queue-durable-op-count';
80198
80322
  const DRAFT_QUEUE_DURABLE_OP_DURATION = 'draft-queue-durable-op-duration';
80323
+ // Drain-wait attribution telemetry: the wall-clock spine of where an offline drain actually spends
80324
+ // WAITING. Operational counts/durations/depths only — no draft content, field values, or server
80325
+ // record ids (matching the existing draft-queue privacy stance).
80326
+ const DRAFT_QUEUE_WORKER_WAIT_DURATION = 'draft-queue-worker-wait-duration';
80327
+ const DRAFT_QUEUE_WORKER_HOLD_DURATION = 'draft-queue-worker-hold-duration';
80328
+ const DRAFT_QUEUE_ACTION_QUEUE_WAIT_DURATION = 'draft-queue-action-queue-wait-duration';
80329
+ const DRAFT_QUEUE_ACTION_DRAIN_DURATION = 'draft-queue-action-drain-duration';
80330
+ const DRAFT_QUEUE_DRAIN_CYCLE_DURATION = 'draft-queue-drain-cycle-duration';
80331
+ const DRAFT_QUEUE_DRAIN_CYCLE_BATCH_SIZE = 'draft-queue-drain-cycle-batch-size';
80332
+ const DRAFT_QUEUE_ACTION_NETWORK_DURATION = 'draft-queue-action-network-duration';
80333
+ const DRAFT_QUEUE_ACTION_RETRY_ATTEMPT = 'draft-queue-action-retry-attempt';
80334
+ const DRAFT_QUEUE_SIDEEFFECT_OP_DURATION = 'draft-queue-sideeffect-op-duration';
80335
+ const DRAFT_QUEUE_SIDEEFFECT_MAP_SIZE = 'draft-queue-sideeffect-map-size';
80336
+ const DRAFT_QUEUE_SIDEEFFECT_REGION_DURATION = 'draft-queue-sideeffect-region-duration';
80199
80337
  /** Content Document */
80200
80338
  const CREATE_CONTENT_DOCUMENT_AND_VERSION_TOTAL_SYNTHESIZE_CALLS = 'content-document-version-total-synthesize-calls';
80201
80339
  const CREATE_CONTENT_DOCUMENT_AND_VERSION_DRAFT_SYNTHESIZE_ERROR = 'create-content-document-version-draft-synthesize-error';
@@ -80315,6 +80453,134 @@
80315
80453
  });
80316
80454
  ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_DURABLE_OP_DURATION, durationMs);
80317
80455
  }
80456
+ const DRAIN_DURATION_BUCKETS = [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000];
80457
+ const WORKER_DEPTH_BUCKETS = [1, 2, 5, 10, 20, 50, 100, 250, 500];
80458
+ // Handler ids are extensible through DraftManager, so only retain the fixed mobile handlers in
80459
+ // metric tags. Everything else shares one bounded bucket rather than creating an unbounded series.
80460
+ function draftQueueHandlerTagOf(handlerId) {
80461
+ switch (handlerId) {
80462
+ case 'LDS_ACTION_HANDLER':
80463
+ case 'ContentDocumentCompositeRepresentationActionHandler':
80464
+ case 'QUICK_ACTION_HANDLER':
80465
+ case 'UPDATE_RECORD_QUICK_ACTION_HANDLER':
80466
+ return handlerId;
80467
+ default:
80468
+ return 'custom';
80469
+ }
80470
+ }
80471
+ // Coarse, bounded-cardinality label for the pool depth a unit saw when it began, so the
80472
+ // wait/hold durations can be sliced BY backlog size without emitting depth as a free integer tag.
80473
+ function workerDepthBucketOf(queueDepth) {
80474
+ if (queueDepth <= 0) {
80475
+ return '0';
80476
+ }
80477
+ if (queueDepth <= 2) {
80478
+ return '1-2';
80479
+ }
80480
+ if (queueDepth <= 10) {
80481
+ return '3-10';
80482
+ }
80483
+ if (queueDepth <= 50) {
80484
+ return '11-50';
80485
+ }
80486
+ if (queueDepth <= 250) {
80487
+ return '51-250';
80488
+ }
80489
+ return '250+';
80490
+ }
80491
+ /**
80492
+ * Per-unit lifecycle split for the AsyncWorkerPool(1) serial queue, emitted once when a unit settles
80493
+ * so the two halves are a JOINED event (rather than shipping wait and depth as unrelated aggregates).
80494
+ * Isolates WHERE the time goes:
80495
+ * waitMs — enqueue -> work began (waiting on the serial queue before any logic runs).
80496
+ * holdMs — work began -> work settled (the critical section's own O(N) service time).
80497
+ * Both are additionally tagged by a depthBucket so "does per-unit cost scale with backlog depth"
80498
+ * (the O(N^2) signature) survives aggregation. queueDepth itself is also tracked. Operational only.
80499
+ */
80500
+ function reportDraftQueueWorkerTiming(waitMs, holdMs, queueDepth) {
80501
+ const depthBucket = workerDepthBucketOf(queueDepth);
80502
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_WORKER_WAIT_DURATION, waitMs, undefined, {
80503
+ depthBucket,
80504
+ });
80505
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_WORKER_HOLD_DURATION, holdMs, undefined, {
80506
+ depthBucket,
80507
+ });
80508
+ ldsMobileInstrumentation$2.bucketValue(DRAFT_QUEUE_WORKER_WAIT_DURATION + '-depth', queueDepth, WORKER_DEPTH_BUCKETS);
80509
+ }
80510
+ /**
80511
+ * Head-of-line FIFO wait for one draft (enqueue -> upload start), bucketed so the
80512
+ * "does wait grow with backlog depth" (O(N^2)) signal is visible per handler type. Custom handler
80513
+ * ids are normalized to one bounded tag so caller-defined values never become metric dimensions.
80514
+ */
80515
+ function reportDraftQueueActionQueueWait(handlerId, waitMs) {
80516
+ ldsMobileInstrumentation$2.bucketValue(DRAFT_QUEUE_ACTION_QUEUE_WAIT_DURATION, waitMs, DRAIN_DURATION_BUCKETS, undefined, { handlerId: draftQueueHandlerTagOf(handlerId) });
80517
+ }
80518
+ /**
80519
+ * End-to-end drain time for one action (first upload attempt -> completed/failed), including
80520
+ * retry/backoff. Compare it with network-duration and retry-attempt signals to understand the
80521
+ * non-request portion of the drain interval.
80522
+ */
80523
+ function reportDraftQueueActionDrainDuration(handlerId, durationMs) {
80524
+ ldsMobileInstrumentation$2.bucketValue(DRAFT_QUEUE_ACTION_DRAIN_DURATION, durationMs, DRAIN_DURATION_BUCKETS, undefined, { handlerId: draftQueueHandlerTagOf(handlerId) });
80525
+ }
80526
+ /**
80527
+ * One full drain sweep — first action starts uploading -> queue empties. Both raw values share a
80528
+ * bounded batch-size cohort so aggregate duration remains sliceable by the number of actions drained.
80529
+ */
80530
+ function reportDraftQueueDrainCycle(durationMs, batchSize) {
80531
+ const batchSizeBucket = workerDepthBucketOf(batchSize);
80532
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_DRAIN_CYCLE_DURATION, durationMs, undefined, {
80533
+ batchSizeBucket,
80534
+ });
80535
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_DRAIN_CYCLE_BATCH_SIZE, batchSize, undefined, {
80536
+ batchSizeBucket,
80537
+ });
80538
+ }
80539
+ /**
80540
+ * Server round-trip duration for one upload. Isolates slow-network from CPU-bound
80541
+ * quadratic work — today only a THROWN error is captured, so a slow-but-successful (or slow 4xx/5xx)
80542
+ * response is invisible. Tags: handler type, HTTP method, and a bucketed status class ('2xx'/'4xx'/
80543
+ * '5xx'/'0'). No URL, body, or record id.
80544
+ */
80545
+ function reportDraftQueueActionNetworkDuration(handlerId, method, statusBucket, durationMs) {
80546
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_ACTION_NETWORK_DURATION, durationMs, undefined, {
80547
+ handlerId: draftQueueHandlerTagOf(handlerId),
80548
+ method,
80549
+ statusBucket,
80550
+ });
80551
+ }
80552
+ /**
80553
+ * Retry event count. Repeated events identify actions cycling through the drain even when the retry
80554
+ * cap is disabled and an exact attempt number is not persisted. cappedOut marks cap exhaustion.
80555
+ */
80556
+ function reportDraftQueueActionRetryAttempt(handlerId, cappedOut) {
80557
+ ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_ACTION_RETRY_ATTEMPT, 1, undefined, {
80558
+ handlerId: draftQueueHandlerTagOf(handlerId),
80559
+ cappedOut: String(cappedOut),
80560
+ });
80561
+ }
80562
+ /**
80563
+ * SideEffectStore operation duration + map size. The incremental map sped up the by-KEY lookup but
80564
+ * getEffectsByTag still linearly scans all effects — this measures whether that residual scan is
80565
+ * silently quadratic during churn. op is 'add'|'remove'|'getByTag'; incremental flags the fast path;
80566
+ * mapSize is an integer effect count. No record keys, field names, or field values.
80567
+ */
80568
+ function reportDraftQueueSideEffectOp(op, incremental, durationMs, mapSize) {
80569
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_SIDEEFFECT_OP_DURATION, durationMs, undefined, {
80570
+ op,
80571
+ incremental: String(incremental),
80572
+ });
80573
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_SIDEEFFECT_MAP_SIZE, mapSize, undefined, {
80574
+ op,
80575
+ });
80576
+ }
80577
+ /**
80578
+ * Broad side-effect lifecycle duration. The fixed region and lifecycle tags identify which part of
80579
+ * serialized draft processing is slow without emitting record keys, action ids, or field data.
80580
+ */
80581
+ function reportDraftQueueSideEffectRegionDuration(region, lifecycle, durationMs) {
80582
+ ldsMobileInstrumentation$2.trackValue(DRAFT_QUEUE_SIDEEFFECT_REGION_DURATION, durationMs, undefined, { region, lifecycle });
80583
+ }
80318
80584
  /**
80319
80585
  * Reports an exception thrown while uploading a draft action. The thrown error is
80320
80586
  * otherwise swallowed by the upload handler's retry path, leaving the failure
@@ -80522,8 +80788,16 @@
80522
80788
  const dispatchOptions = action.observabilityContext !== undefined
80523
80789
  ? { requestCorrelator: { observabilityContext: action.observabilityContext } }
80524
80790
  : {};
80791
+ const method = request?.method?.toUpperCase() ?? 'UNKNOWN';
80792
+ const networkStart = Date.now();
80793
+ let responseReceived = false;
80525
80794
  try {
80526
80795
  const response = await this.networkAdapter(request, dispatchOptions);
80796
+ responseReceived = true;
80797
+ // Time the round-trip regardless of outcome (ok, retryable HTTP error, or non-ok
80798
+ // terminal error) — today only a THROWN error is visible; a slow-but-successful or
80799
+ // slow 4xx/5xx response is not.
80800
+ this.reportNetworkDuration(method, statusBucketOf(response.status), networkStart);
80527
80801
  if (response.ok) {
80528
80802
  await actionCompleted({
80529
80803
  ...action,
@@ -80679,6 +80953,11 @@
80679
80953
  const error = normalizeError$1(e);
80680
80954
  const attempt = this.getUploadRetryCount(action) + 1;
80681
80955
  reportDraftActionUploadError(error, this.handlerId, attempt);
80956
+ // A thrown network call still has a round-trip duration worth surfacing — a slow
80957
+ // FAILING call is otherwise as invisible as a slow successful one.
80958
+ if (!responseReceived) {
80959
+ this.reportNetworkDuration(method, 'error', networkStart);
80960
+ }
80682
80961
  await this.retryOrCap(action, error, actionErrored);
80683
80962
  return ProcessActionResult.NETWORK_ERROR;
80684
80963
  }
@@ -80714,12 +80993,17 @@
80714
80993
  */
80715
80994
  async retryOrCap(action, lastFailure, actionErrored, retryDelayInMs, actionDataChanged) {
80716
80995
  if (!draftQueueMaxRetryAttemptsGate.isOpen({ fallback: false })) {
80717
- // Gate closed: preserve the pre-existing unbounded-retry behavior exactly.
80996
+ // Gate closed: preserve the pre-existing unbounded-retry behavior exactly. The
80997
+ // metric is still emitted (measure-only) so retry attempts are visible even with
80998
+ // the cap gate closed — cappedOut is always false here since nothing ever caps
80999
+ // out on this path.
81000
+ this.reportRetryAttempt(false);
80718
81001
  await actionErrored(action, true, retryDelayInMs, actionDataChanged);
80719
81002
  return;
80720
81003
  }
80721
81004
  const attempt = this.getUploadRetryCount(action) + 1;
80722
81005
  if (attempt >= MAX_RETRY_ATTEMPTS) {
81006
+ this.reportRetryAttempt(true);
80723
81007
  await actionErrored({
80724
81008
  ...action,
80725
81009
  error: buildMaxRetryError(lastFailure),
@@ -80727,6 +81011,7 @@
80727
81011
  }, false);
80728
81012
  return;
80729
81013
  }
81014
+ this.reportRetryAttempt(false);
80730
81015
  // Persist the incremented attempt count so it survives restarts. Forcing
80731
81016
  // actionDataChanged ensures the queue writes the updated metadata before the
80732
81017
  // action is rescheduled for retry.
@@ -80740,12 +81025,12 @@
80740
81025
  await actionErrored(retryingAction, true, retryDelayInMs, true);
80741
81026
  }
80742
81027
  async handleActionEnqueued(action) {
80743
- const impactedKeys = await this.recordService.setSideEffectsForActions([action]);
80744
- await this.recordService.reapplyRecordSideEffects(impactedKeys);
81028
+ const impactedKeys = await this.recordService.setSideEffectsForActions([action], 'enqueued');
81029
+ await this.recordService.reapplyRecordSideEffects(impactedKeys, 'enqueued');
80745
81030
  }
80746
81031
  async handleActionRemoved(action) {
80747
81032
  const impactedKeys = await this.recordService.removeSideEffectsForAction(action);
80748
- await this.recordService.reapplyRecordSideEffects(impactedKeys);
81033
+ await this.recordService.reapplyRecordSideEffects(impactedKeys, 'removed');
80749
81034
  }
80750
81035
  async handleActionReplaced(target, source) {
80751
81036
  // remove both the siden effects from the actions being merged before we
@@ -80753,10 +81038,10 @@
80753
81038
  const impactedKeysFromRemove = await this.recordService.removeSideEffectsForAction(source);
80754
81039
  const removedSideEffectFromSet = await this.recordService.removeSideEffectsForAction(target);
80755
81040
  // revert the values in the DEFAULT store back to what it was before the removed side effects
80756
- await this.recordService.reapplyRecordSideEffects(new Set([...impactedKeysFromRemove, ...removedSideEffectFromSet]));
81041
+ await this.recordService.reapplyRecordSideEffects(new Set([...impactedKeysFromRemove, ...removedSideEffectFromSet]), 'replaced-remove');
80757
81042
  // set the new side effect values for the merged action
80758
- const impactedKeysFromSet = await this.recordService.setSideEffectsForActions([target]);
80759
- await this.recordService.reapplyRecordSideEffects(new Set([...impactedKeysFromSet]));
81043
+ const impactedKeysFromSet = await this.recordService.setSideEffectsForActions([target], 'replaced-set');
81044
+ await this.recordService.reapplyRecordSideEffects(new Set([...impactedKeysFromSet]), 'replaced-set');
80760
81045
  }
80761
81046
  async handleActionCompleted(action, queueOperations) {
80762
81047
  const { data: request, tag } = action;
@@ -80790,8 +81075,8 @@
80790
81075
  actionsNeedingReplay.push(qo.action);
80791
81076
  }
80792
81077
  });
80793
- const impactedKeys = await this.recordService.setSideEffectsForActions(actionsNeedingReplay);
80794
- await this.recordService.reapplyRecordSideEffects(impactedKeys);
81078
+ const impactedKeys = await this.recordService.setSideEffectsForActions(actionsNeedingReplay, 'completed-replay');
81079
+ await this.recordService.reapplyRecordSideEffects(impactedKeys, 'completed-replay');
80795
81080
  }
80796
81081
  getQueueOperationsForCompletingDrafts(queue, action) {
80797
81082
  const queueOperations = [];
@@ -81117,6 +81402,39 @@
81117
81402
  return new StoreKeyMap();
81118
81403
  });
81119
81404
  }
81405
+ reportNetworkDuration(method, statusBucket, startedAt) {
81406
+ try {
81407
+ reportDraftQueueActionNetworkDuration(this.handlerId, method, statusBucket, Date.now() - startedAt);
81408
+ }
81409
+ catch {
81410
+ // Telemetry must not change upload processing behavior.
81411
+ }
81412
+ }
81413
+ reportRetryAttempt(cappedOut) {
81414
+ try {
81415
+ reportDraftQueueActionRetryAttempt(this.handlerId, cappedOut);
81416
+ }
81417
+ catch {
81418
+ // Telemetry must not change retry or cap behavior.
81419
+ }
81420
+ }
81421
+ }
81422
+ // Classifies an HTTP status into a small, greppable tag set for the network-duration metric —
81423
+ // never the raw status, to keep the metric's tag cardinality bounded.
81424
+ function statusBucketOf(status) {
81425
+ if (status === 0) {
81426
+ return '0';
81427
+ }
81428
+ if (status >= 200 && status <= 299) {
81429
+ return '2xx';
81430
+ }
81431
+ if (status >= 400 && status <= 499) {
81432
+ return '4xx';
81433
+ }
81434
+ if (status >= 500) {
81435
+ return '5xx';
81436
+ }
81437
+ return 'other';
81120
81438
  }
81121
81439
  function actionsForTag(tag, queue) {
81122
81440
  return queue.filter((action) => action.tag === tag);
@@ -81480,10 +81798,16 @@
81480
81798
  }
81481
81799
  // calculates the side effects for a given set of actions
81482
81800
  // and returns a set of impacted record keys
81483
- async setSideEffectsForActions(changedActions) {
81484
- const operations = await this.recalculateForChangedActions(changedActions);
81485
- await this.sideEffectStore.addEffects(operations);
81486
- return new Set(operations.map((o) => o.key));
81801
+ async setSideEffectsForActions(changedActions, lifecycle) {
81802
+ const startedAt = Date.now();
81803
+ try {
81804
+ const operations = await this.recalculateForChangedActions(changedActions);
81805
+ await this.sideEffectStore.addEffects(operations);
81806
+ return new Set(operations.map((o) => o.key));
81807
+ }
81808
+ finally {
81809
+ this.reportSideEffectRegionDuration('set', lifecycle, startedAt);
81810
+ }
81487
81811
  }
81488
81812
  // removes the associated side effects for a given action and returns
81489
81813
  // a set of impacted record keys
@@ -81494,31 +81818,45 @@
81494
81818
  }
81495
81819
  // reads a set of records out of the durable store, re-applies current side
81496
81820
  // effects and persists the changed record
81497
- async reapplyRecordSideEffects(recordKeys) {
81498
- const luvio = this.getLuvio();
81499
- const keys = Array.from(recordKeys);
81500
- const recordEntries = await this.durableRecordStore.getRecordsWithMetadata(keys);
81501
- keys.forEach((key) => {
81502
- let record;
81503
- const metadata = {
81504
- namespace: 'UiApi',
81505
- representationName: 'RecordRepresentation',
81506
- version: VERSION$1v$1,
81507
- ingestionTimestamp: Date.now(),
81508
- ttl: Number.MAX_SAFE_INTEGER,
81509
- };
81510
- const recordEntry = recordEntries.get(key);
81511
- if (recordEntry) {
81512
- record = recordEntry.record;
81513
- if (recordEntry.metadata) {
81514
- metadata.ingestionTimestamp = recordEntry.metadata.ingestionTimestamp;
81515
- metadata.ttl = TTL$z;
81821
+ async reapplyRecordSideEffects(recordKeys, lifecycle) {
81822
+ const startedAt = Date.now();
81823
+ try {
81824
+ const luvio = this.getLuvio();
81825
+ const keys = Array.from(recordKeys);
81826
+ const recordEntries = await this.durableRecordStore.getRecordsWithMetadata(keys);
81827
+ keys.forEach((key) => {
81828
+ let record;
81829
+ const metadata = {
81830
+ namespace: 'UiApi',
81831
+ representationName: 'RecordRepresentation',
81832
+ version: VERSION$1v$1,
81833
+ ingestionTimestamp: Date.now(),
81834
+ ttl: Number.MAX_SAFE_INTEGER,
81835
+ };
81836
+ const recordEntry = recordEntries.get(key);
81837
+ if (recordEntry) {
81838
+ record = recordEntry.record;
81839
+ if (recordEntry.metadata) {
81840
+ metadata.ingestionTimestamp = recordEntry.metadata.ingestionTimestamp;
81841
+ metadata.ttl = TTL$z;
81842
+ }
81516
81843
  }
81517
- }
81518
- luvio.storePublish(key, record);
81519
- luvio.publishStoreMetadata(key, metadata);
81520
- });
81521
- await luvio.storeBroadcast();
81844
+ luvio.storePublish(key, record);
81845
+ luvio.publishStoreMetadata(key, metadata);
81846
+ });
81847
+ await luvio.storeBroadcast();
81848
+ }
81849
+ finally {
81850
+ this.reportSideEffectRegionDuration('reapply', lifecycle, startedAt);
81851
+ }
81852
+ }
81853
+ reportSideEffectRegionDuration(region, lifecycle, startedAt) {
81854
+ try {
81855
+ reportDraftQueueSideEffectRegionDuration(region, lifecycle, Date.now() - startedAt);
81856
+ }
81857
+ catch {
81858
+ // Telemetry must not change draft processing behavior.
81859
+ }
81522
81860
  }
81523
81861
  async synthesizeId(apiName) {
81524
81862
  const objectInfo = await this.objectInfoService.getObjectInfo(apiName);
@@ -81754,7 +82092,7 @@
81754
82092
  // since we don't ingest a record from the response, we need to manually trigger a re-apply here to ensure we
81755
82093
  // re-ingest the record without effects applied
81756
82094
  await this.recordService.removeSideEffectsForAction(action);
81757
- await this.recordService.reapplyRecordSideEffects(new Set([action.tag]));
82095
+ await this.recordService.reapplyRecordSideEffects(new Set([action.tag]), 'quick-action-completed');
81758
82096
  // notify luvio that record is now changed and needs to be refreshed
81759
82097
  await luvio.notifyStoreUpdateAvailable([action.tag]);
81760
82098
  }
@@ -90434,6 +90772,18 @@
90434
90772
  onDurableOperation(op, segment, durationMs) {
90435
90773
  reportDraftQueueDurableOperation(op, segment, durationMs);
90436
90774
  },
90775
+ onWorkerTiming(waitMs, holdMs, queueDepth) {
90776
+ reportDraftQueueWorkerTiming(waitMs, holdMs, queueDepth);
90777
+ },
90778
+ onActionQueueWait(handlerId, waitMs) {
90779
+ reportDraftQueueActionQueueWait(handlerId, waitMs);
90780
+ },
90781
+ onActionDrainDuration(handlerId, durationMs) {
90782
+ reportDraftQueueActionDrainDuration(handlerId, durationMs);
90783
+ },
90784
+ onDrainCycle(durationMs, batchSize) {
90785
+ reportDraftQueueDrainCycle(durationMs, batchSize);
90786
+ },
90437
90787
  };
90438
90788
 
90439
90789
  // so eslint doesn't complain about nimbus
@@ -93776,6 +94126,9 @@
93776
94126
  this.initialize();
93777
94127
  }
93778
94128
  async addEffects(effects) {
94129
+ // Wall-clock across the whole method (including the durable write), matching
94130
+ // how the equivalent removeEffects/getEffectsByTag windows are measured.
94131
+ const startTime = Date.now();
93779
94132
  const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
93780
94133
  const durableEntries = {};
93781
94134
  effects.forEach((effect) => {
@@ -93790,8 +94143,11 @@
93790
94143
  this.regenerateKeyToEffectMap();
93791
94144
  }
93792
94145
  await this.durableStore.setEntries(durableEntries, SIDE_EFFECT_SEGMENT);
94146
+ this.reportOperation('add', incremental, startTime);
93793
94147
  }
93794
94148
  async removeEffects(effects) {
94149
+ // See addEffects — same wall-clock-across-the-method measurement.
94150
+ const startTime = Date.now();
93795
94151
  const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
93796
94152
  const effectKeys = effects.map((e) => e.uniqueId);
93797
94153
  effects.forEach((effect) => {
@@ -93805,6 +94161,7 @@
93805
94161
  this.regenerateKeyToEffectMap();
93806
94162
  }
93807
94163
  await this.durableStore.evictEntries(effectKeys, SIDE_EFFECT_SEGMENT);
94164
+ this.reportOperation('remove', incremental, startTime);
93808
94165
  }
93809
94166
  getEffects(key) {
93810
94167
  const effects = this.keyToEffectMap.get(key);
@@ -93814,6 +94171,10 @@
93814
94171
  return effects;
93815
94172
  }
93816
94173
  getEffectsByTag(tag) {
94174
+ // This scan is still O(N) over every effect regardless of the incremental-map
94175
+ // gate — timing it tells us whether that residual scan is the quadratic hotspot during churn.
94176
+ const startTime = Date.now();
94177
+ const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
93817
94178
  const filteredEffects = [];
93818
94179
  const { keyToEffectMap } = this;
93819
94180
  // Loop through every entry in the map
@@ -93823,7 +94184,17 @@
93823
94184
  // Add the matching effects to the filtered list
93824
94185
  filteredEffects.push(...matchingEffects);
93825
94186
  }
93826
- return filteredEffects.sort((a, b) => a.timestamp - b.timestamp);
94187
+ const result = filteredEffects.sort((a, b) => a.timestamp - b.timestamp);
94188
+ this.reportOperation('getByTag', incremental, startTime);
94189
+ return result;
94190
+ }
94191
+ reportOperation(op, incremental, startedAt) {
94192
+ try {
94193
+ reportDraftQueueSideEffectOp(op, incremental, Date.now() - startedAt, this.allEffects.size);
94194
+ }
94195
+ catch {
94196
+ // Telemetry must not change side-effect store behavior.
94197
+ }
93827
94198
  }
93828
94199
  async initialize() {
93829
94200
  const effects = await this.durableStore.getAllEntries(SIDE_EFFECT_SEGMENT);
@@ -97020,7 +97391,7 @@
97020
97391
  },
97021
97392
  };
97022
97393
  }
97023
- // version: 1.454.0-4223ab4365
97394
+ // version: 1.455.0-c53c97de2a
97024
97395
 
97025
97396
  /**
97026
97397
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -97046,7 +97417,7 @@
97046
97417
  },
97047
97418
  };
97048
97419
  }
97049
- // version: 1.454.0-4223ab4365
97420
+ // version: 1.455.0-c53c97de2a
97050
97421
 
97051
97422
  function findExecutableOperation$1(input) {
97052
97423
  const operations = input.query.definitions.filter(
@@ -99755,7 +100126,7 @@
99755
100126
  id: '@salesforce/lds-network-adapter',
99756
100127
  instrument: instrument$2,
99757
100128
  });
99758
- // version: 1.454.0-4223ab4365
100129
+ // version: 1.455.0-c53c97de2a
99759
100130
 
99760
100131
  const { create: create$2, keys: keys$2 } = Object;
99761
100132
  const { stringify, parse } = JSON;
@@ -107290,7 +107661,7 @@
107290
107661
  cb(graphql_v1_import, graphql_imperative$1, graphql_imperative_legacy_v1_import, graphql_state_manager, useOneStoreGraphQL);
107291
107662
  }
107292
107663
  }
107293
- // version: 1.454.0-761808c4e9
107664
+ // version: 1.455.0-96e9b41a18
107294
107665
 
107295
107666
  function createFragmentMap(documentNode) {
107296
107667
  const fragments = {};
@@ -124019,7 +124390,7 @@
124019
124390
  return keyBuilder$z(luvio, keyParams);
124020
124391
  }
124021
124392
 
124022
- const { assign: assign$1, create: create$1, freeze: freeze$1, isFrozen: isFrozen$1, keys: keys$1 } = Object;
124393
+ const { assign: assign$1, create: create$1, freeze: freeze$1, getPrototypeOf: getPrototypeOf$1, isFrozen: isFrozen$1, keys: keys$1 } = Object;
124023
124394
  const { isArray: isArray$2 } = Array;
124024
124395
  const { concat: concat$1, filter: filter$1, includes: includes$1, push: push$1, reduce: reduce$1 } = Array.prototype;
124025
124396
 
@@ -136533,7 +136904,7 @@
136533
136904
  configuration: { ...configurationForGraphQLAdapters$1 },
136534
136905
  instrument: instrument$1,
136535
136906
  });
136536
- // version: 1.454.0-761808c4e9
136907
+ // version: 1.455.0-96e9b41a18
136537
136908
 
136538
136909
  // On core the unstable adapters are re-exported with different names,
136539
136910
  // we want to match them here.
@@ -136685,7 +137056,7 @@
136685
137056
  unstable_graphQL_imperative = createImperativeAdapter(luvio, createInstrumentedAdapter(ldsAdapter, adapterMetadata), adapterMetadata);
136686
137057
  graphQLImperative = ldsAdapter;
136687
137058
  });
136688
- // version: 1.454.0-761808c4e9
137059
+ // version: 1.455.0-96e9b41a18
136689
137060
 
136690
137061
  var gqlApi = /*#__PURE__*/Object.freeze({
136691
137062
  __proto__: null,
@@ -137484,7 +137855,7 @@
137484
137855
  function register(r) {
137485
137856
  callbacks$1.forEach((callback) => callback(r));
137486
137857
  }
137487
- // version: 1.454.0-4223ab4365
137858
+ // version: 1.455.0-c53c97de2a
137488
137859
 
137489
137860
  /**
137490
137861
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -137770,7 +138141,7 @@
137770
138141
  }
137771
138142
  const keyPrefix = 'UiApi';
137772
138143
 
137773
- const { assign, create, freeze, isFrozen, keys } = Object;
138144
+ const { assign, create, freeze, getPrototypeOf, isFrozen, keys } = Object;
137774
138145
  const { isArray } = Array;
137775
138146
  const { concat, filter, includes, push, reduce } = Array.prototype;
137776
138147
 
@@ -138794,4 +139165,4 @@
138794
139165
  exports.subscribeToAdapter = subscribeToAdapter;
138795
139166
 
138796
139167
  }));
138797
- // version: 1.454.0-4223ab4365
139168
+ // version: 1.455.0-c53c97de2a