@salesforce/lds-runtime-mobile 1.453.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.
- package/dist/main.js +442 -58
- package/dist/types/drafts/AbstractResourceRequestActionHandler.d.ts +2 -0
- package/dist/types/drafts/UiApiDraftRecordService.d.ts +4 -2
- package/dist/types/drafts/sideEffects/SideEffectStore.d.ts +1 -0
- package/dist/types/instrumentation/metrics.d.ts +54 -1
- package/package.json +33 -33
- package/sfdc/main.js +442 -58
- package/sfdc/types/drafts/AbstractResourceRequestActionHandler.d.ts +2 -0
- package/sfdc/types/drafts/UiApiDraftRecordService.d.ts +4 -2
- package/sfdc/types/drafts/sideEffects/SideEffectStore.d.ts +1 -0
- package/sfdc/types/instrumentation/metrics.d.ts +54 -1
package/sfdc/main.js
CHANGED
|
@@ -1973,16 +1973,18 @@ class LdsAbortController {
|
|
|
1973
1973
|
}
|
|
1974
1974
|
|
|
1975
1975
|
class AsyncWorkerPool {
|
|
1976
|
-
constructor(concurrency) {
|
|
1976
|
+
constructor(concurrency, onWorkTiming) {
|
|
1977
1977
|
this.queue = [];
|
|
1978
1978
|
this.activeWork = [];
|
|
1979
1979
|
this.concurrency = concurrency;
|
|
1980
|
+
this.onWorkTiming = onWorkTiming;
|
|
1980
1981
|
}
|
|
1981
1982
|
push(work) {
|
|
1982
1983
|
return new Promise((resolve, reject) => {
|
|
1983
1984
|
this.queue.push({
|
|
1984
1985
|
...work,
|
|
1985
1986
|
workFn: (abortController) => work.workFn(abortController).then(resolve).catch(reject),
|
|
1987
|
+
enqueuedAt: Date.now(),
|
|
1986
1988
|
});
|
|
1987
1989
|
this.doWork();
|
|
1988
1990
|
});
|
|
@@ -2025,10 +2027,30 @@ class AsyncWorkerPool {
|
|
|
2025
2027
|
const abortController = new LdsAbortController();
|
|
2026
2028
|
const newWork = { ...work, abortController };
|
|
2027
2029
|
this.activeWork.push(newWork);
|
|
2028
|
-
const { workFn } = work;
|
|
2030
|
+
const { workFn, enqueuedAt } = work;
|
|
2031
|
+
const onWorkTiming = this.onWorkTiming;
|
|
2032
|
+
let startedAt = 0;
|
|
2033
|
+
let waitMs = 0;
|
|
2034
|
+
let queueDepthAtStart = 0;
|
|
2035
|
+
if (onWorkTiming !== undefined) {
|
|
2036
|
+
startedAt = Date.now();
|
|
2037
|
+
waitMs = startedAt - enqueuedAt;
|
|
2038
|
+
// depth AFTER this unit was shifted off — units still waiting behind it.
|
|
2039
|
+
queueDepthAtStart = this.queue.length;
|
|
2040
|
+
}
|
|
2029
2041
|
workFn(abortController).finally(() => {
|
|
2030
|
-
|
|
2031
|
-
|
|
2042
|
+
try {
|
|
2043
|
+
if (onWorkTiming !== undefined) {
|
|
2044
|
+
onWorkTiming(waitMs, Date.now() - startedAt, queueDepthAtStart);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
catch {
|
|
2048
|
+
// Instrumentation must not prevent the pool from releasing the worker slot.
|
|
2049
|
+
}
|
|
2050
|
+
finally {
|
|
2051
|
+
this.activeWork = this.activeWork.filter((w) => w !== newWork);
|
|
2052
|
+
this.doWork();
|
|
2053
|
+
}
|
|
2032
2054
|
});
|
|
2033
2055
|
}
|
|
2034
2056
|
}
|
|
@@ -2358,6 +2380,25 @@ const DRAFT_SEGMENT = 'DRAFT';
|
|
|
2358
2380
|
*/
|
|
2359
2381
|
const DRAFT_ACTION_RETRY_COUNT_METADATA_KEY = 'retryCount';
|
|
2360
2382
|
class DurableDraftQueue {
|
|
2383
|
+
// Instrumentation is strictly measure-only: a broken sink must never change queue behavior.
|
|
2384
|
+
// Pass the sink into the callback so methods are invoked through their owning object and retain
|
|
2385
|
+
// their receiver (some sinks use `this` to reach their metric registry).
|
|
2386
|
+
emitInstrumentation(emit) {
|
|
2387
|
+
const { instrumentation } = this;
|
|
2388
|
+
if (instrumentation === undefined) {
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
try {
|
|
2392
|
+
emit(instrumentation);
|
|
2393
|
+
}
|
|
2394
|
+
catch {
|
|
2395
|
+
// Telemetry failures must not interrupt draft processing.
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
resetDrainCycle() {
|
|
2399
|
+
this.drainCycleStart = undefined;
|
|
2400
|
+
this.drainCycleBatchCount = 0;
|
|
2401
|
+
}
|
|
2361
2402
|
getHandler(id) {
|
|
2362
2403
|
const handler = this.handlers[id];
|
|
2363
2404
|
if (handler === undefined) {
|
|
@@ -2380,9 +2421,19 @@ class DurableDraftQueue {
|
|
|
2380
2421
|
? __nimbus.plugins.JSLoggerPlugin
|
|
2381
2422
|
: undefined;
|
|
2382
2423
|
this.handlers = {};
|
|
2424
|
+
// Drain-wait attribution state. uploadStartTimes stamps when each action first began
|
|
2425
|
+
// uploading (keyed by action id) so actionCompleted/failed can emit its end-to-end drain time,
|
|
2426
|
+
// including automatic retries/backoff. actionQueueWaitEmittedIds ensures those retries do not
|
|
2427
|
+
// report the original enqueue wait more than once. drainCycleStart/BatchCount track one full
|
|
2428
|
+
// idle->empty sweep. All measure-only; when no instrumentation is injected these stay untouched.
|
|
2429
|
+
this.uploadStartTimes = new Map();
|
|
2430
|
+
this.actionQueueWaitEmittedIds = new Set();
|
|
2431
|
+
this.drainCycleBatchCount = 0;
|
|
2383
2432
|
this.draftStore = draftStore;
|
|
2384
|
-
this.workerPool = new AsyncWorkerPool(1);
|
|
2385
2433
|
this.instrumentation = instrumentation;
|
|
2434
|
+
this.workerPool = new AsyncWorkerPool(1, instrumentation?.onWorkerTiming !== undefined
|
|
2435
|
+
? (waitMs, holdMs, queueDepth) => this.emitInstrumentation((sink) => sink.onWorkerTiming?.(waitMs, holdMs, queueDepth))
|
|
2436
|
+
: undefined);
|
|
2386
2437
|
}
|
|
2387
2438
|
addHandler(handler) {
|
|
2388
2439
|
const id = handler.handlerId;
|
|
@@ -2470,6 +2521,9 @@ class DurableDraftQueue {
|
|
|
2470
2521
|
clearTimeout(this.timeoutHandler);
|
|
2471
2522
|
this.timeoutHandler = undefined;
|
|
2472
2523
|
}
|
|
2524
|
+
// A stop/user edit breaks the current sweep. A later restart starts a fresh cycle rather
|
|
2525
|
+
// than attributing stopped time to an offline drain.
|
|
2526
|
+
this.resetDrainCycle();
|
|
2473
2527
|
this.state = DraftQueueState.Stopped;
|
|
2474
2528
|
}
|
|
2475
2529
|
async getQueueActions() {
|
|
@@ -2500,7 +2554,7 @@ class DurableDraftQueue {
|
|
|
2500
2554
|
}
|
|
2501
2555
|
return aTime - bTime;
|
|
2502
2556
|
});
|
|
2503
|
-
|
|
2557
|
+
this.emitInstrumentation((sink) => sink.onGetQueueActions?.(Date.now() - start, sorted.length));
|
|
2504
2558
|
return sorted;
|
|
2505
2559
|
}
|
|
2506
2560
|
async enqueue(handlerId, data, observabilityContext) {
|
|
@@ -2544,6 +2598,13 @@ class DurableDraftQueue {
|
|
|
2544
2598
|
await handler.handleActionCompleted(action, queueOperations, values$2(this.handlers));
|
|
2545
2599
|
this.retryIntervalMilliseconds = 0;
|
|
2546
2600
|
this.uploadingActionId = undefined;
|
|
2601
|
+
// The action finished draining — emit its end-to-end drain time and
|
|
2602
|
+
// count it toward the current drain-cycle batch. Measure-only.
|
|
2603
|
+
this.emitActionDrainDuration(action.id, action.handler);
|
|
2604
|
+
this.actionQueueWaitEmittedIds.delete(action.id);
|
|
2605
|
+
if (this.drainCycleStart !== undefined) {
|
|
2606
|
+
this.drainCycleBatchCount += 1;
|
|
2607
|
+
}
|
|
2547
2608
|
await this.notifyChangedListeners({
|
|
2548
2609
|
type: DraftQueueEventType.ActionCompleted,
|
|
2549
2610
|
action,
|
|
@@ -2555,6 +2616,16 @@ class DurableDraftQueue {
|
|
|
2555
2616
|
},
|
|
2556
2617
|
});
|
|
2557
2618
|
}
|
|
2619
|
+
// Read+clear the action's upload-start stamp and emit its drain duration. Safe to
|
|
2620
|
+
// call when instrumentation is absent or the id was never stamped (no-op). Never emits the id.
|
|
2621
|
+
emitActionDrainDuration(id, handlerId) {
|
|
2622
|
+
const start = this.uploadStartTimes.get(id);
|
|
2623
|
+
if (start === undefined) {
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
this.uploadStartTimes.delete(id);
|
|
2627
|
+
this.emitInstrumentation((sink) => sink.onActionDrainDuration?.(handlerId, Date.now() - start));
|
|
2628
|
+
}
|
|
2558
2629
|
async actionFailed(action, retry, retryDelayInMs, actionDataChanged) {
|
|
2559
2630
|
if (actionDataChanged === true) {
|
|
2560
2631
|
await this.draftStore.writeAction({
|
|
@@ -2564,14 +2635,24 @@ class DurableDraftQueue {
|
|
|
2564
2635
|
}
|
|
2565
2636
|
this.uploadingActionId = undefined;
|
|
2566
2637
|
if (retry && this.state !== DraftQueueState.Stopped) {
|
|
2638
|
+
// A retry means this action is NOT done draining. Keep its first-upload stamp and the
|
|
2639
|
+
// open cycle so the eventual outcome includes network time and retry/backoff.
|
|
2567
2640
|
this.state = DraftQueueState.Waiting;
|
|
2568
2641
|
return retryDelayInMs !== undefined
|
|
2569
2642
|
? this.scheduleRetryWithSpecifiedDelay(retryDelayInMs)
|
|
2570
2643
|
: this.scheduleRetry();
|
|
2571
2644
|
}
|
|
2572
2645
|
else if (isDraftError(action)) {
|
|
2646
|
+
// Terminal failure — this action is done draining. Emit its drain time.
|
|
2647
|
+
this.emitActionDrainDuration(action.id, action.handler);
|
|
2648
|
+
// This was not a clean drain-to-empty. Discard the sweep before user intervention so a
|
|
2649
|
+
// later manual retry/removal cannot inherit its elapsed time or completed batch count.
|
|
2650
|
+
this.resetDrainCycle();
|
|
2573
2651
|
return this.handleServerError(action, action.error);
|
|
2574
2652
|
}
|
|
2653
|
+
// Any other terminal path — ensure no stale start stamp lingers.
|
|
2654
|
+
this.emitActionDrainDuration(action.id, action.handler);
|
|
2655
|
+
this.resetDrainCycle();
|
|
2575
2656
|
}
|
|
2576
2657
|
handle(action) {
|
|
2577
2658
|
const handler = this.getHandler(action.handler);
|
|
@@ -2591,12 +2672,22 @@ class DurableDraftQueue {
|
|
|
2591
2672
|
this.state = DraftQueueState.Started;
|
|
2592
2673
|
}
|
|
2593
2674
|
this.processingAction = undefined;
|
|
2675
|
+
// The queue drained empty — close the drain-cycle window if one was open.
|
|
2676
|
+
if (this.drainCycleStart !== undefined) {
|
|
2677
|
+
const durationMs = Date.now() - this.drainCycleStart;
|
|
2678
|
+
const batchSize = this.drainCycleBatchCount;
|
|
2679
|
+
this.resetDrainCycle();
|
|
2680
|
+
this.emitInstrumentation((sink) => sink.onDrainCycle?.(durationMs, batchSize));
|
|
2681
|
+
}
|
|
2594
2682
|
return ProcessActionResult.NO_ACTION_TO_PROCESS;
|
|
2595
2683
|
}
|
|
2596
2684
|
const { status, id } = action;
|
|
2597
2685
|
if (status === DraftActionStatus.Error) {
|
|
2598
2686
|
this.state = DraftQueueState.Error;
|
|
2599
2687
|
this.processingAction = undefined;
|
|
2688
|
+
// The sweep is blocked on an errored action, not a clean drain-to-empty —
|
|
2689
|
+
// discard the open drain-cycle window so it doesn't inflate a later cycle's duration.
|
|
2690
|
+
this.resetDrainCycle();
|
|
2600
2691
|
await this.notifyChangedListeners({
|
|
2601
2692
|
type: DraftQueueEventType.ActionFailed,
|
|
2602
2693
|
action: action,
|
|
@@ -2614,6 +2705,31 @@ class DurableDraftQueue {
|
|
|
2614
2705
|
if (this.state === DraftQueueState.Waiting) {
|
|
2615
2706
|
this.state = DraftQueueState.Started;
|
|
2616
2707
|
}
|
|
2708
|
+
// The action is now leaving the queue to upload. Emit head-of-line FIFO wait
|
|
2709
|
+
// (enqueue -> upload start), stamp its drain-start for end-to-end timing on completion, and
|
|
2710
|
+
// open a drain-cycle window if the queue was previously idle. All measure-only.
|
|
2711
|
+
const { instrumentation } = this;
|
|
2712
|
+
if (instrumentation !== undefined) {
|
|
2713
|
+
const now = Date.now();
|
|
2714
|
+
const { timestamp } = action;
|
|
2715
|
+
if (instrumentation.onActionQueueWait !== undefined &&
|
|
2716
|
+
!this.actionQueueWaitEmittedIds.has(id) &&
|
|
2717
|
+
typeof timestamp === 'number' &&
|
|
2718
|
+
timestamp > 0) {
|
|
2719
|
+
// Mark before invoking the sink: even a throwing callback must not make a retry
|
|
2720
|
+
// report the same action's enqueue wait again.
|
|
2721
|
+
this.actionQueueWaitEmittedIds.add(id);
|
|
2722
|
+
this.emitInstrumentation((sink) => sink.onActionQueueWait?.(action.handler, now - timestamp));
|
|
2723
|
+
}
|
|
2724
|
+
if (instrumentation.onActionDrainDuration !== undefined &&
|
|
2725
|
+
!this.uploadStartTimes.has(id)) {
|
|
2726
|
+
this.uploadStartTimes.set(id, now);
|
|
2727
|
+
}
|
|
2728
|
+
if (instrumentation.onDrainCycle !== undefined && this.drainCycleStart === undefined) {
|
|
2729
|
+
this.drainCycleStart = now;
|
|
2730
|
+
this.drainCycleBatchCount = 0;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2617
2733
|
await this.notifyChangedListeners({
|
|
2618
2734
|
type: DraftQueueEventType.ActionUploading,
|
|
2619
2735
|
action: { ...action, status: DraftActionStatus.Uploading },
|
|
@@ -2651,7 +2767,7 @@ class DurableDraftQueue {
|
|
|
2651
2767
|
results.push(listener(event));
|
|
2652
2768
|
}
|
|
2653
2769
|
// W-23515682: listener fan-out per event type. Measure-only.
|
|
2654
|
-
this.
|
|
2770
|
+
this.emitInstrumentation((sink) => sink.onNotifyChangedListeners?.(event.type, draftQueueLen));
|
|
2655
2771
|
await Promise.all(results);
|
|
2656
2772
|
}
|
|
2657
2773
|
/**
|
|
@@ -2680,9 +2796,17 @@ class DurableDraftQueue {
|
|
|
2680
2796
|
const shouldDeleteRelated = handler.shouldDeleteActionByTagOnRemoval(action);
|
|
2681
2797
|
if (shouldDeleteRelated) {
|
|
2682
2798
|
await this.draftStore.deleteByTag(action.tag);
|
|
2799
|
+
queue.forEach((queuedAction) => {
|
|
2800
|
+
if (queuedAction.tag === action.tag) {
|
|
2801
|
+
this.uploadStartTimes.delete(queuedAction.id);
|
|
2802
|
+
this.actionQueueWaitEmittedIds.delete(queuedAction.id);
|
|
2803
|
+
}
|
|
2804
|
+
});
|
|
2683
2805
|
}
|
|
2684
2806
|
else {
|
|
2685
2807
|
await this.draftStore.deleteDraft(action.id);
|
|
2808
|
+
this.uploadStartTimes.delete(action.id);
|
|
2809
|
+
this.actionQueueWaitEmittedIds.delete(action.id);
|
|
2686
2810
|
}
|
|
2687
2811
|
await handler.handleActionRemoved(action, queue.filter((x) => x.id !== actionId));
|
|
2688
2812
|
await this.notifyChangedListeners({
|
|
@@ -42336,6 +42460,20 @@ const DRAFT_QUEUE_GET_QUEUE_ACTIONS_DURATION = 'draft-queue-get-queue-actions-du
|
|
|
42336
42460
|
const DRAFT_QUEUE_NOTIFY_CHANGED_LISTENERS_COUNT = 'draft-queue-notify-changed-listeners-count';
|
|
42337
42461
|
const DRAFT_QUEUE_DURABLE_OP_COUNT = 'draft-queue-durable-op-count';
|
|
42338
42462
|
const DRAFT_QUEUE_DURABLE_OP_DURATION = 'draft-queue-durable-op-duration';
|
|
42463
|
+
// Drain-wait attribution telemetry: the wall-clock spine of where an offline drain actually spends
|
|
42464
|
+
// WAITING. Operational counts/durations/depths only — no draft content, field values, or server
|
|
42465
|
+
// record ids (matching the existing draft-queue privacy stance).
|
|
42466
|
+
const DRAFT_QUEUE_WORKER_WAIT_DURATION = 'draft-queue-worker-wait-duration';
|
|
42467
|
+
const DRAFT_QUEUE_WORKER_HOLD_DURATION = 'draft-queue-worker-hold-duration';
|
|
42468
|
+
const DRAFT_QUEUE_ACTION_QUEUE_WAIT_DURATION = 'draft-queue-action-queue-wait-duration';
|
|
42469
|
+
const DRAFT_QUEUE_ACTION_DRAIN_DURATION = 'draft-queue-action-drain-duration';
|
|
42470
|
+
const DRAFT_QUEUE_DRAIN_CYCLE_DURATION = 'draft-queue-drain-cycle-duration';
|
|
42471
|
+
const DRAFT_QUEUE_DRAIN_CYCLE_BATCH_SIZE = 'draft-queue-drain-cycle-batch-size';
|
|
42472
|
+
const DRAFT_QUEUE_ACTION_NETWORK_DURATION = 'draft-queue-action-network-duration';
|
|
42473
|
+
const DRAFT_QUEUE_ACTION_RETRY_ATTEMPT = 'draft-queue-action-retry-attempt';
|
|
42474
|
+
const DRAFT_QUEUE_SIDEEFFECT_OP_DURATION = 'draft-queue-sideeffect-op-duration';
|
|
42475
|
+
const DRAFT_QUEUE_SIDEEFFECT_MAP_SIZE = 'draft-queue-sideeffect-map-size';
|
|
42476
|
+
const DRAFT_QUEUE_SIDEEFFECT_REGION_DURATION = 'draft-queue-sideeffect-region-duration';
|
|
42339
42477
|
/** Content Document */
|
|
42340
42478
|
const CREATE_CONTENT_DOCUMENT_AND_VERSION_TOTAL_SYNTHESIZE_CALLS = 'content-document-version-total-synthesize-calls';
|
|
42341
42479
|
const CREATE_CONTENT_DOCUMENT_AND_VERSION_DRAFT_SYNTHESIZE_ERROR = 'create-content-document-version-draft-synthesize-error';
|
|
@@ -42455,6 +42593,134 @@ function reportDraftQueueDurableOperation(op, segment, durationMs) {
|
|
|
42455
42593
|
});
|
|
42456
42594
|
ldsMobileInstrumentation.trackValue(DRAFT_QUEUE_DURABLE_OP_DURATION, durationMs);
|
|
42457
42595
|
}
|
|
42596
|
+
const DRAIN_DURATION_BUCKETS = [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000];
|
|
42597
|
+
const WORKER_DEPTH_BUCKETS = [1, 2, 5, 10, 20, 50, 100, 250, 500];
|
|
42598
|
+
// Handler ids are extensible through DraftManager, so only retain the fixed mobile handlers in
|
|
42599
|
+
// metric tags. Everything else shares one bounded bucket rather than creating an unbounded series.
|
|
42600
|
+
function draftQueueHandlerTagOf(handlerId) {
|
|
42601
|
+
switch (handlerId) {
|
|
42602
|
+
case 'LDS_ACTION_HANDLER':
|
|
42603
|
+
case 'ContentDocumentCompositeRepresentationActionHandler':
|
|
42604
|
+
case 'QUICK_ACTION_HANDLER':
|
|
42605
|
+
case 'UPDATE_RECORD_QUICK_ACTION_HANDLER':
|
|
42606
|
+
return handlerId;
|
|
42607
|
+
default:
|
|
42608
|
+
return 'custom';
|
|
42609
|
+
}
|
|
42610
|
+
}
|
|
42611
|
+
// Coarse, bounded-cardinality label for the pool depth a unit saw when it began, so the
|
|
42612
|
+
// wait/hold durations can be sliced BY backlog size without emitting depth as a free integer tag.
|
|
42613
|
+
function workerDepthBucketOf(queueDepth) {
|
|
42614
|
+
if (queueDepth <= 0) {
|
|
42615
|
+
return '0';
|
|
42616
|
+
}
|
|
42617
|
+
if (queueDepth <= 2) {
|
|
42618
|
+
return '1-2';
|
|
42619
|
+
}
|
|
42620
|
+
if (queueDepth <= 10) {
|
|
42621
|
+
return '3-10';
|
|
42622
|
+
}
|
|
42623
|
+
if (queueDepth <= 50) {
|
|
42624
|
+
return '11-50';
|
|
42625
|
+
}
|
|
42626
|
+
if (queueDepth <= 250) {
|
|
42627
|
+
return '51-250';
|
|
42628
|
+
}
|
|
42629
|
+
return '250+';
|
|
42630
|
+
}
|
|
42631
|
+
/**
|
|
42632
|
+
* Per-unit lifecycle split for the AsyncWorkerPool(1) serial queue, emitted once when a unit settles
|
|
42633
|
+
* so the two halves are a JOINED event (rather than shipping wait and depth as unrelated aggregates).
|
|
42634
|
+
* Isolates WHERE the time goes:
|
|
42635
|
+
* waitMs — enqueue -> work began (waiting on the serial queue before any logic runs).
|
|
42636
|
+
* holdMs — work began -> work settled (the critical section's own O(N) service time).
|
|
42637
|
+
* Both are additionally tagged by a depthBucket so "does per-unit cost scale with backlog depth"
|
|
42638
|
+
* (the O(N^2) signature) survives aggregation. queueDepth itself is also tracked. Operational only.
|
|
42639
|
+
*/
|
|
42640
|
+
function reportDraftQueueWorkerTiming(waitMs, holdMs, queueDepth) {
|
|
42641
|
+
const depthBucket = workerDepthBucketOf(queueDepth);
|
|
42642
|
+
ldsMobileInstrumentation.trackValue(DRAFT_QUEUE_WORKER_WAIT_DURATION, waitMs, undefined, {
|
|
42643
|
+
depthBucket,
|
|
42644
|
+
});
|
|
42645
|
+
ldsMobileInstrumentation.trackValue(DRAFT_QUEUE_WORKER_HOLD_DURATION, holdMs, undefined, {
|
|
42646
|
+
depthBucket,
|
|
42647
|
+
});
|
|
42648
|
+
ldsMobileInstrumentation.bucketValue(DRAFT_QUEUE_WORKER_WAIT_DURATION + '-depth', queueDepth, WORKER_DEPTH_BUCKETS);
|
|
42649
|
+
}
|
|
42650
|
+
/**
|
|
42651
|
+
* Head-of-line FIFO wait for one draft (enqueue -> upload start), bucketed so the
|
|
42652
|
+
* "does wait grow with backlog depth" (O(N^2)) signal is visible per handler type. Custom handler
|
|
42653
|
+
* ids are normalized to one bounded tag so caller-defined values never become metric dimensions.
|
|
42654
|
+
*/
|
|
42655
|
+
function reportDraftQueueActionQueueWait(handlerId, waitMs) {
|
|
42656
|
+
ldsMobileInstrumentation.bucketValue(DRAFT_QUEUE_ACTION_QUEUE_WAIT_DURATION, waitMs, DRAIN_DURATION_BUCKETS, undefined, { handlerId: draftQueueHandlerTagOf(handlerId) });
|
|
42657
|
+
}
|
|
42658
|
+
/**
|
|
42659
|
+
* End-to-end drain time for one action (first upload attempt -> completed/failed), including
|
|
42660
|
+
* retry/backoff. Compare it with network-duration and retry-attempt signals to understand the
|
|
42661
|
+
* non-request portion of the drain interval.
|
|
42662
|
+
*/
|
|
42663
|
+
function reportDraftQueueActionDrainDuration(handlerId, durationMs) {
|
|
42664
|
+
ldsMobileInstrumentation.bucketValue(DRAFT_QUEUE_ACTION_DRAIN_DURATION, durationMs, DRAIN_DURATION_BUCKETS, undefined, { handlerId: draftQueueHandlerTagOf(handlerId) });
|
|
42665
|
+
}
|
|
42666
|
+
/**
|
|
42667
|
+
* One full drain sweep — first action starts uploading -> queue empties. Both raw values share a
|
|
42668
|
+
* bounded batch-size cohort so aggregate duration remains sliceable by the number of actions drained.
|
|
42669
|
+
*/
|
|
42670
|
+
function reportDraftQueueDrainCycle(durationMs, batchSize) {
|
|
42671
|
+
const batchSizeBucket = workerDepthBucketOf(batchSize);
|
|
42672
|
+
ldsMobileInstrumentation.trackValue(DRAFT_QUEUE_DRAIN_CYCLE_DURATION, durationMs, undefined, {
|
|
42673
|
+
batchSizeBucket,
|
|
42674
|
+
});
|
|
42675
|
+
ldsMobileInstrumentation.trackValue(DRAFT_QUEUE_DRAIN_CYCLE_BATCH_SIZE, batchSize, undefined, {
|
|
42676
|
+
batchSizeBucket,
|
|
42677
|
+
});
|
|
42678
|
+
}
|
|
42679
|
+
/**
|
|
42680
|
+
* Server round-trip duration for one upload. Isolates slow-network from CPU-bound
|
|
42681
|
+
* quadratic work — today only a THROWN error is captured, so a slow-but-successful (or slow 4xx/5xx)
|
|
42682
|
+
* response is invisible. Tags: handler type, HTTP method, and a bucketed status class ('2xx'/'4xx'/
|
|
42683
|
+
* '5xx'/'0'). No URL, body, or record id.
|
|
42684
|
+
*/
|
|
42685
|
+
function reportDraftQueueActionNetworkDuration(handlerId, method, statusBucket, durationMs) {
|
|
42686
|
+
ldsMobileInstrumentation.trackValue(DRAFT_QUEUE_ACTION_NETWORK_DURATION, durationMs, undefined, {
|
|
42687
|
+
handlerId: draftQueueHandlerTagOf(handlerId),
|
|
42688
|
+
method,
|
|
42689
|
+
statusBucket,
|
|
42690
|
+
});
|
|
42691
|
+
}
|
|
42692
|
+
/**
|
|
42693
|
+
* Retry event count. Repeated events identify actions cycling through the drain even when the retry
|
|
42694
|
+
* cap is disabled and an exact attempt number is not persisted. cappedOut marks cap exhaustion.
|
|
42695
|
+
*/
|
|
42696
|
+
function reportDraftQueueActionRetryAttempt(handlerId, cappedOut) {
|
|
42697
|
+
ldsMobileInstrumentation.incrementCounter(DRAFT_QUEUE_ACTION_RETRY_ATTEMPT, 1, undefined, {
|
|
42698
|
+
handlerId: draftQueueHandlerTagOf(handlerId),
|
|
42699
|
+
cappedOut: String(cappedOut),
|
|
42700
|
+
});
|
|
42701
|
+
}
|
|
42702
|
+
/**
|
|
42703
|
+
* SideEffectStore operation duration + map size. The incremental map sped up the by-KEY lookup but
|
|
42704
|
+
* getEffectsByTag still linearly scans all effects — this measures whether that residual scan is
|
|
42705
|
+
* silently quadratic during churn. op is 'add'|'remove'|'getByTag'; incremental flags the fast path;
|
|
42706
|
+
* mapSize is an integer effect count. No record keys, field names, or field values.
|
|
42707
|
+
*/
|
|
42708
|
+
function reportDraftQueueSideEffectOp(op, incremental, durationMs, mapSize) {
|
|
42709
|
+
ldsMobileInstrumentation.trackValue(DRAFT_QUEUE_SIDEEFFECT_OP_DURATION, durationMs, undefined, {
|
|
42710
|
+
op,
|
|
42711
|
+
incremental: String(incremental),
|
|
42712
|
+
});
|
|
42713
|
+
ldsMobileInstrumentation.trackValue(DRAFT_QUEUE_SIDEEFFECT_MAP_SIZE, mapSize, undefined, {
|
|
42714
|
+
op,
|
|
42715
|
+
});
|
|
42716
|
+
}
|
|
42717
|
+
/**
|
|
42718
|
+
* Broad side-effect lifecycle duration. The fixed region and lifecycle tags identify which part of
|
|
42719
|
+
* serialized draft processing is slow without emitting record keys, action ids, or field data.
|
|
42720
|
+
*/
|
|
42721
|
+
function reportDraftQueueSideEffectRegionDuration(region, lifecycle, durationMs) {
|
|
42722
|
+
ldsMobileInstrumentation.trackValue(DRAFT_QUEUE_SIDEEFFECT_REGION_DURATION, durationMs, undefined, { region, lifecycle });
|
|
42723
|
+
}
|
|
42458
42724
|
/**
|
|
42459
42725
|
* Reports an exception thrown while uploading a draft action. The thrown error is
|
|
42460
42726
|
* otherwise swallowed by the upload handler's retry path, leaving the failure
|
|
@@ -42662,8 +42928,16 @@ class AbstractResourceRequestActionHandler {
|
|
|
42662
42928
|
const dispatchOptions = action.observabilityContext !== undefined
|
|
42663
42929
|
? { requestCorrelator: { observabilityContext: action.observabilityContext } }
|
|
42664
42930
|
: {};
|
|
42931
|
+
const method = request?.method?.toUpperCase() ?? 'UNKNOWN';
|
|
42932
|
+
const networkStart = Date.now();
|
|
42933
|
+
let responseReceived = false;
|
|
42665
42934
|
try {
|
|
42666
42935
|
const response = await this.networkAdapter(request, dispatchOptions);
|
|
42936
|
+
responseReceived = true;
|
|
42937
|
+
// Time the round-trip regardless of outcome (ok, retryable HTTP error, or non-ok
|
|
42938
|
+
// terminal error) — today only a THROWN error is visible; a slow-but-successful or
|
|
42939
|
+
// slow 4xx/5xx response is not.
|
|
42940
|
+
this.reportNetworkDuration(method, statusBucketOf(response.status), networkStart);
|
|
42667
42941
|
if (response.ok) {
|
|
42668
42942
|
await actionCompleted({
|
|
42669
42943
|
...action,
|
|
@@ -42749,19 +43023,32 @@ class AbstractResourceRequestActionHandler {
|
|
|
42749
43023
|
actionDataChanged = true;
|
|
42750
43024
|
}
|
|
42751
43025
|
else if (!this.isServerGeneratedError(response.body)) {
|
|
42752
|
-
// Killswitch CLOSED (default) —
|
|
43026
|
+
// Killswitch CLOSED (default) — an edge/gateway-injected 400 (no
|
|
42753
43027
|
// recognizable server error body) is retried with the SAME key, so the
|
|
42754
43028
|
// server can dedupe a write that may already have committed at the origin.
|
|
42755
|
-
//
|
|
42756
|
-
//
|
|
42757
|
-
// retrying it unchanged would only be rejected again, so the draft is
|
|
42758
|
-
// errored for the user to fix or discard — the same terminal outcome as
|
|
42759
|
-
// before this fix, minus the duplicating key rotation. The key is only ever
|
|
42760
|
-
// rotated by the recognized idempotency error codes above, by a backdating
|
|
42761
|
-
// collision (new body), or when the user edits an errored draft
|
|
42762
|
-
// (mergeActions/handleReplaceAction).
|
|
43029
|
+
// Rotating here is what duplicated the WOLI records (@W-23445154), so the
|
|
43030
|
+
// key is preserved.
|
|
42763
43031
|
shouldRetry = true;
|
|
42764
43032
|
}
|
|
43033
|
+
else {
|
|
43034
|
+
// Killswitch CLOSED — a REAL, server-generated 400 (UiApiError[] or an
|
|
43035
|
+
// enhancedErrorType body, e.g. a field-validation failure). The origin
|
|
43036
|
+
// understood the request and rejected it, so it definitively did NOT
|
|
43037
|
+
// commit — there is no already-written record to duplicate, which is what
|
|
43038
|
+
// makes rotation safe here (unlike the edge-injected case above).
|
|
43039
|
+
//
|
|
43040
|
+
// We still do NOT retry (shouldRetry stays false): the request as-is would
|
|
43041
|
+
// just be rejected again and wedge the queue. The draft errors for the user
|
|
43042
|
+
// to fix or discard. But we DO rotate the key: the server's idempotency
|
|
43043
|
+
// layer caches the 400 against the key, so if the same request is later
|
|
43044
|
+
// resubmitted unchanged after the server-side validation rule changes to
|
|
43045
|
+
// allow it, reusing the key would replay that cached 400 and the now-valid
|
|
43046
|
+
// write would never be applied. Rotating gives the retried request a fresh
|
|
43047
|
+
// key so the origin re-evaluates it. @W-23445154
|
|
43048
|
+
updatedAction.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
|
|
43049
|
+
reportIdempotencyKeyRotated(updatedAction.targetId, 'server-validation-400');
|
|
43050
|
+
actionDataChanged = true;
|
|
43051
|
+
}
|
|
42765
43052
|
}
|
|
42766
43053
|
if (response.status === HttpStatusCode$1.BadRequest &&
|
|
42767
43054
|
this.isBackdatingError(response.body, action)) {
|
|
@@ -42806,6 +43093,11 @@ class AbstractResourceRequestActionHandler {
|
|
|
42806
43093
|
const error = normalizeError$1(e);
|
|
42807
43094
|
const attempt = this.getUploadRetryCount(action) + 1;
|
|
42808
43095
|
reportDraftActionUploadError(error, this.handlerId, attempt);
|
|
43096
|
+
// A thrown network call still has a round-trip duration worth surfacing — a slow
|
|
43097
|
+
// FAILING call is otherwise as invisible as a slow successful one.
|
|
43098
|
+
if (!responseReceived) {
|
|
43099
|
+
this.reportNetworkDuration(method, 'error', networkStart);
|
|
43100
|
+
}
|
|
42809
43101
|
await this.retryOrCap(action, error, actionErrored);
|
|
42810
43102
|
return ProcessActionResult.NETWORK_ERROR;
|
|
42811
43103
|
}
|
|
@@ -42841,12 +43133,17 @@ class AbstractResourceRequestActionHandler {
|
|
|
42841
43133
|
*/
|
|
42842
43134
|
async retryOrCap(action, lastFailure, actionErrored, retryDelayInMs, actionDataChanged) {
|
|
42843
43135
|
if (!draftQueueMaxRetryAttemptsGate.isOpen({ fallback: false })) {
|
|
42844
|
-
// Gate closed: preserve the pre-existing unbounded-retry behavior exactly.
|
|
43136
|
+
// Gate closed: preserve the pre-existing unbounded-retry behavior exactly. The
|
|
43137
|
+
// metric is still emitted (measure-only) so retry attempts are visible even with
|
|
43138
|
+
// the cap gate closed — cappedOut is always false here since nothing ever caps
|
|
43139
|
+
// out on this path.
|
|
43140
|
+
this.reportRetryAttempt(false);
|
|
42845
43141
|
await actionErrored(action, true, retryDelayInMs, actionDataChanged);
|
|
42846
43142
|
return;
|
|
42847
43143
|
}
|
|
42848
43144
|
const attempt = this.getUploadRetryCount(action) + 1;
|
|
42849
43145
|
if (attempt >= MAX_RETRY_ATTEMPTS) {
|
|
43146
|
+
this.reportRetryAttempt(true);
|
|
42850
43147
|
await actionErrored({
|
|
42851
43148
|
...action,
|
|
42852
43149
|
error: buildMaxRetryError(lastFailure),
|
|
@@ -42854,6 +43151,7 @@ class AbstractResourceRequestActionHandler {
|
|
|
42854
43151
|
}, false);
|
|
42855
43152
|
return;
|
|
42856
43153
|
}
|
|
43154
|
+
this.reportRetryAttempt(false);
|
|
42857
43155
|
// Persist the incremented attempt count so it survives restarts. Forcing
|
|
42858
43156
|
// actionDataChanged ensures the queue writes the updated metadata before the
|
|
42859
43157
|
// action is rescheduled for retry.
|
|
@@ -42867,12 +43165,12 @@ class AbstractResourceRequestActionHandler {
|
|
|
42867
43165
|
await actionErrored(retryingAction, true, retryDelayInMs, true);
|
|
42868
43166
|
}
|
|
42869
43167
|
async handleActionEnqueued(action) {
|
|
42870
|
-
const impactedKeys = await this.recordService.setSideEffectsForActions([action]);
|
|
42871
|
-
await this.recordService.reapplyRecordSideEffects(impactedKeys);
|
|
43168
|
+
const impactedKeys = await this.recordService.setSideEffectsForActions([action], 'enqueued');
|
|
43169
|
+
await this.recordService.reapplyRecordSideEffects(impactedKeys, 'enqueued');
|
|
42872
43170
|
}
|
|
42873
43171
|
async handleActionRemoved(action) {
|
|
42874
43172
|
const impactedKeys = await this.recordService.removeSideEffectsForAction(action);
|
|
42875
|
-
await this.recordService.reapplyRecordSideEffects(impactedKeys);
|
|
43173
|
+
await this.recordService.reapplyRecordSideEffects(impactedKeys, 'removed');
|
|
42876
43174
|
}
|
|
42877
43175
|
async handleActionReplaced(target, source) {
|
|
42878
43176
|
// remove both the siden effects from the actions being merged before we
|
|
@@ -42880,10 +43178,10 @@ class AbstractResourceRequestActionHandler {
|
|
|
42880
43178
|
const impactedKeysFromRemove = await this.recordService.removeSideEffectsForAction(source);
|
|
42881
43179
|
const removedSideEffectFromSet = await this.recordService.removeSideEffectsForAction(target);
|
|
42882
43180
|
// revert the values in the DEFAULT store back to what it was before the removed side effects
|
|
42883
|
-
await this.recordService.reapplyRecordSideEffects(new Set([...impactedKeysFromRemove, ...removedSideEffectFromSet]));
|
|
43181
|
+
await this.recordService.reapplyRecordSideEffects(new Set([...impactedKeysFromRemove, ...removedSideEffectFromSet]), 'replaced-remove');
|
|
42884
43182
|
// set the new side effect values for the merged action
|
|
42885
|
-
const impactedKeysFromSet = await this.recordService.setSideEffectsForActions([target]);
|
|
42886
|
-
await this.recordService.reapplyRecordSideEffects(new Set([...impactedKeysFromSet]));
|
|
43183
|
+
const impactedKeysFromSet = await this.recordService.setSideEffectsForActions([target], 'replaced-set');
|
|
43184
|
+
await this.recordService.reapplyRecordSideEffects(new Set([...impactedKeysFromSet]), 'replaced-set');
|
|
42887
43185
|
}
|
|
42888
43186
|
async handleActionCompleted(action, queueOperations) {
|
|
42889
43187
|
const { data: request, tag } = action;
|
|
@@ -42917,8 +43215,8 @@ class AbstractResourceRequestActionHandler {
|
|
|
42917
43215
|
actionsNeedingReplay.push(qo.action);
|
|
42918
43216
|
}
|
|
42919
43217
|
});
|
|
42920
|
-
const impactedKeys = await this.recordService.setSideEffectsForActions(actionsNeedingReplay);
|
|
42921
|
-
await this.recordService.reapplyRecordSideEffects(impactedKeys);
|
|
43218
|
+
const impactedKeys = await this.recordService.setSideEffectsForActions(actionsNeedingReplay, 'completed-replay');
|
|
43219
|
+
await this.recordService.reapplyRecordSideEffects(impactedKeys, 'completed-replay');
|
|
42922
43220
|
}
|
|
42923
43221
|
getQueueOperationsForCompletingDrafts(queue, action) {
|
|
42924
43222
|
const queueOperations = [];
|
|
@@ -43244,6 +43542,39 @@ class AbstractResourceRequestActionHandler {
|
|
|
43244
43542
|
return new StoreKeyMap();
|
|
43245
43543
|
});
|
|
43246
43544
|
}
|
|
43545
|
+
reportNetworkDuration(method, statusBucket, startedAt) {
|
|
43546
|
+
try {
|
|
43547
|
+
reportDraftQueueActionNetworkDuration(this.handlerId, method, statusBucket, Date.now() - startedAt);
|
|
43548
|
+
}
|
|
43549
|
+
catch {
|
|
43550
|
+
// Telemetry must not change upload processing behavior.
|
|
43551
|
+
}
|
|
43552
|
+
}
|
|
43553
|
+
reportRetryAttempt(cappedOut) {
|
|
43554
|
+
try {
|
|
43555
|
+
reportDraftQueueActionRetryAttempt(this.handlerId, cappedOut);
|
|
43556
|
+
}
|
|
43557
|
+
catch {
|
|
43558
|
+
// Telemetry must not change retry or cap behavior.
|
|
43559
|
+
}
|
|
43560
|
+
}
|
|
43561
|
+
}
|
|
43562
|
+
// Classifies an HTTP status into a small, greppable tag set for the network-duration metric —
|
|
43563
|
+
// never the raw status, to keep the metric's tag cardinality bounded.
|
|
43564
|
+
function statusBucketOf(status) {
|
|
43565
|
+
if (status === 0) {
|
|
43566
|
+
return '0';
|
|
43567
|
+
}
|
|
43568
|
+
if (status >= 200 && status <= 299) {
|
|
43569
|
+
return '2xx';
|
|
43570
|
+
}
|
|
43571
|
+
if (status >= 400 && status <= 499) {
|
|
43572
|
+
return '4xx';
|
|
43573
|
+
}
|
|
43574
|
+
if (status >= 500) {
|
|
43575
|
+
return '5xx';
|
|
43576
|
+
}
|
|
43577
|
+
return 'other';
|
|
43247
43578
|
}
|
|
43248
43579
|
function actionsForTag(tag, queue) {
|
|
43249
43580
|
return queue.filter((action) => action.tag === tag);
|
|
@@ -43607,10 +43938,16 @@ class UiApiDraftRecordService {
|
|
|
43607
43938
|
}
|
|
43608
43939
|
// calculates the side effects for a given set of actions
|
|
43609
43940
|
// and returns a set of impacted record keys
|
|
43610
|
-
async setSideEffectsForActions(changedActions) {
|
|
43611
|
-
const
|
|
43612
|
-
|
|
43613
|
-
|
|
43941
|
+
async setSideEffectsForActions(changedActions, lifecycle) {
|
|
43942
|
+
const startedAt = Date.now();
|
|
43943
|
+
try {
|
|
43944
|
+
const operations = await this.recalculateForChangedActions(changedActions);
|
|
43945
|
+
await this.sideEffectStore.addEffects(operations);
|
|
43946
|
+
return new Set(operations.map((o) => o.key));
|
|
43947
|
+
}
|
|
43948
|
+
finally {
|
|
43949
|
+
this.reportSideEffectRegionDuration('set', lifecycle, startedAt);
|
|
43950
|
+
}
|
|
43614
43951
|
}
|
|
43615
43952
|
// removes the associated side effects for a given action and returns
|
|
43616
43953
|
// a set of impacted record keys
|
|
@@ -43621,31 +43958,45 @@ class UiApiDraftRecordService {
|
|
|
43621
43958
|
}
|
|
43622
43959
|
// reads a set of records out of the durable store, re-applies current side
|
|
43623
43960
|
// effects and persists the changed record
|
|
43624
|
-
async reapplyRecordSideEffects(recordKeys) {
|
|
43625
|
-
const
|
|
43626
|
-
|
|
43627
|
-
|
|
43628
|
-
|
|
43629
|
-
|
|
43630
|
-
|
|
43631
|
-
|
|
43632
|
-
|
|
43633
|
-
|
|
43634
|
-
|
|
43635
|
-
|
|
43636
|
-
|
|
43637
|
-
|
|
43638
|
-
|
|
43639
|
-
|
|
43640
|
-
if (recordEntry
|
|
43641
|
-
|
|
43642
|
-
metadata
|
|
43961
|
+
async reapplyRecordSideEffects(recordKeys, lifecycle) {
|
|
43962
|
+
const startedAt = Date.now();
|
|
43963
|
+
try {
|
|
43964
|
+
const luvio = this.getLuvio();
|
|
43965
|
+
const keys = Array.from(recordKeys);
|
|
43966
|
+
const recordEntries = await this.durableRecordStore.getRecordsWithMetadata(keys);
|
|
43967
|
+
keys.forEach((key) => {
|
|
43968
|
+
let record;
|
|
43969
|
+
const metadata = {
|
|
43970
|
+
namespace: 'UiApi',
|
|
43971
|
+
representationName: 'RecordRepresentation',
|
|
43972
|
+
version: VERSION$1v,
|
|
43973
|
+
ingestionTimestamp: Date.now(),
|
|
43974
|
+
ttl: Number.MAX_SAFE_INTEGER,
|
|
43975
|
+
};
|
|
43976
|
+
const recordEntry = recordEntries.get(key);
|
|
43977
|
+
if (recordEntry) {
|
|
43978
|
+
record = recordEntry.record;
|
|
43979
|
+
if (recordEntry.metadata) {
|
|
43980
|
+
metadata.ingestionTimestamp = recordEntry.metadata.ingestionTimestamp;
|
|
43981
|
+
metadata.ttl = RecordRepresentationTTL;
|
|
43982
|
+
}
|
|
43643
43983
|
}
|
|
43644
|
-
|
|
43645
|
-
|
|
43646
|
-
|
|
43647
|
-
|
|
43648
|
-
|
|
43984
|
+
luvio.storePublish(key, record);
|
|
43985
|
+
luvio.publishStoreMetadata(key, metadata);
|
|
43986
|
+
});
|
|
43987
|
+
await luvio.storeBroadcast();
|
|
43988
|
+
}
|
|
43989
|
+
finally {
|
|
43990
|
+
this.reportSideEffectRegionDuration('reapply', lifecycle, startedAt);
|
|
43991
|
+
}
|
|
43992
|
+
}
|
|
43993
|
+
reportSideEffectRegionDuration(region, lifecycle, startedAt) {
|
|
43994
|
+
try {
|
|
43995
|
+
reportDraftQueueSideEffectRegionDuration(region, lifecycle, Date.now() - startedAt);
|
|
43996
|
+
}
|
|
43997
|
+
catch {
|
|
43998
|
+
// Telemetry must not change draft processing behavior.
|
|
43999
|
+
}
|
|
43649
44000
|
}
|
|
43650
44001
|
async synthesizeId(apiName) {
|
|
43651
44002
|
const objectInfo = await this.objectInfoService.getObjectInfo(apiName);
|
|
@@ -43881,7 +44232,7 @@ class UpdateRecordQuickActionExecutionRepresentationHandler extends AbstractQuic
|
|
|
43881
44232
|
// since we don't ingest a record from the response, we need to manually trigger a re-apply here to ensure we
|
|
43882
44233
|
// re-ingest the record without effects applied
|
|
43883
44234
|
await this.recordService.removeSideEffectsForAction(action);
|
|
43884
|
-
await this.recordService.reapplyRecordSideEffects(new Set([action.tag]));
|
|
44235
|
+
await this.recordService.reapplyRecordSideEffects(new Set([action.tag]), 'quick-action-completed');
|
|
43885
44236
|
// notify luvio that record is now changed and needs to be refreshed
|
|
43886
44237
|
await luvio.notifyStoreUpdateAvailable([action.tag]);
|
|
43887
44238
|
}
|
|
@@ -52567,6 +52918,18 @@ const draftQueueInstrumentation = {
|
|
|
52567
52918
|
onDurableOperation(op, segment, durationMs) {
|
|
52568
52919
|
reportDraftQueueDurableOperation(op, segment, durationMs);
|
|
52569
52920
|
},
|
|
52921
|
+
onWorkerTiming(waitMs, holdMs, queueDepth) {
|
|
52922
|
+
reportDraftQueueWorkerTiming(waitMs, holdMs, queueDepth);
|
|
52923
|
+
},
|
|
52924
|
+
onActionQueueWait(handlerId, waitMs) {
|
|
52925
|
+
reportDraftQueueActionQueueWait(handlerId, waitMs);
|
|
52926
|
+
},
|
|
52927
|
+
onActionDrainDuration(handlerId, durationMs) {
|
|
52928
|
+
reportDraftQueueActionDrainDuration(handlerId, durationMs);
|
|
52929
|
+
},
|
|
52930
|
+
onDrainCycle(durationMs, batchSize) {
|
|
52931
|
+
reportDraftQueueDrainCycle(durationMs, batchSize);
|
|
52932
|
+
},
|
|
52570
52933
|
};
|
|
52571
52934
|
|
|
52572
52935
|
// so eslint doesn't complain about nimbus
|
|
@@ -56057,6 +56420,9 @@ class SideEffectStore {
|
|
|
56057
56420
|
this.initialize();
|
|
56058
56421
|
}
|
|
56059
56422
|
async addEffects(effects) {
|
|
56423
|
+
// Wall-clock across the whole method (including the durable write), matching
|
|
56424
|
+
// how the equivalent removeEffects/getEffectsByTag windows are measured.
|
|
56425
|
+
const startTime = Date.now();
|
|
56060
56426
|
const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
|
|
56061
56427
|
const durableEntries = {};
|
|
56062
56428
|
effects.forEach((effect) => {
|
|
@@ -56071,8 +56437,11 @@ class SideEffectStore {
|
|
|
56071
56437
|
this.regenerateKeyToEffectMap();
|
|
56072
56438
|
}
|
|
56073
56439
|
await this.durableStore.setEntries(durableEntries, SIDE_EFFECT_SEGMENT);
|
|
56440
|
+
this.reportOperation('add', incremental, startTime);
|
|
56074
56441
|
}
|
|
56075
56442
|
async removeEffects(effects) {
|
|
56443
|
+
// See addEffects — same wall-clock-across-the-method measurement.
|
|
56444
|
+
const startTime = Date.now();
|
|
56076
56445
|
const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
|
|
56077
56446
|
const effectKeys = effects.map((e) => e.uniqueId);
|
|
56078
56447
|
effects.forEach((effect) => {
|
|
@@ -56086,6 +56455,7 @@ class SideEffectStore {
|
|
|
56086
56455
|
this.regenerateKeyToEffectMap();
|
|
56087
56456
|
}
|
|
56088
56457
|
await this.durableStore.evictEntries(effectKeys, SIDE_EFFECT_SEGMENT);
|
|
56458
|
+
this.reportOperation('remove', incremental, startTime);
|
|
56089
56459
|
}
|
|
56090
56460
|
getEffects(key) {
|
|
56091
56461
|
const effects = this.keyToEffectMap.get(key);
|
|
@@ -56095,6 +56465,10 @@ class SideEffectStore {
|
|
|
56095
56465
|
return effects;
|
|
56096
56466
|
}
|
|
56097
56467
|
getEffectsByTag(tag) {
|
|
56468
|
+
// This scan is still O(N) over every effect regardless of the incremental-map
|
|
56469
|
+
// gate — timing it tells us whether that residual scan is the quadratic hotspot during churn.
|
|
56470
|
+
const startTime = Date.now();
|
|
56471
|
+
const incremental = draftQueueSideEffectMapGate.isOpen({ fallback: true });
|
|
56098
56472
|
const filteredEffects = [];
|
|
56099
56473
|
const { keyToEffectMap } = this;
|
|
56100
56474
|
// Loop through every entry in the map
|
|
@@ -56104,7 +56478,17 @@ class SideEffectStore {
|
|
|
56104
56478
|
// Add the matching effects to the filtered list
|
|
56105
56479
|
filteredEffects.push(...matchingEffects);
|
|
56106
56480
|
}
|
|
56107
|
-
|
|
56481
|
+
const result = filteredEffects.sort((a, b) => a.timestamp - b.timestamp);
|
|
56482
|
+
this.reportOperation('getByTag', incremental, startTime);
|
|
56483
|
+
return result;
|
|
56484
|
+
}
|
|
56485
|
+
reportOperation(op, incremental, startedAt) {
|
|
56486
|
+
try {
|
|
56487
|
+
reportDraftQueueSideEffectOp(op, incremental, Date.now() - startedAt, this.allEffects.size);
|
|
56488
|
+
}
|
|
56489
|
+
catch {
|
|
56490
|
+
// Telemetry must not change side-effect store behavior.
|
|
56491
|
+
}
|
|
56108
56492
|
}
|
|
56109
56493
|
async initialize() {
|
|
56110
56494
|
const effects = await this.durableStore.getAllEntries(SIDE_EFFECT_SEGMENT);
|
|
@@ -59318,7 +59702,7 @@ function buildServiceDescriptor$b(luvio) {
|
|
|
59318
59702
|
},
|
|
59319
59703
|
};
|
|
59320
59704
|
}
|
|
59321
|
-
// version: 1.
|
|
59705
|
+
// version: 1.455.0-c53c97de2a
|
|
59322
59706
|
|
|
59323
59707
|
/**
|
|
59324
59708
|
* Copyright (c) 2022, Salesforce, Inc.,
|
|
@@ -59344,7 +59728,7 @@ function buildServiceDescriptor$a(notifyRecordUpdateAvailable, getNormalizedLuvi
|
|
|
59344
59728
|
},
|
|
59345
59729
|
};
|
|
59346
59730
|
}
|
|
59347
|
-
// version: 1.
|
|
59731
|
+
// version: 1.455.0-c53c97de2a
|
|
59348
59732
|
|
|
59349
59733
|
function findExecutableOperation(input) {
|
|
59350
59734
|
const operations = input.query.definitions.filter(
|
|
@@ -62056,4 +62440,4 @@ register({
|
|
|
62056
62440
|
});
|
|
62057
62441
|
|
|
62058
62442
|
export { O11Y_NAMESPACE_LDS_MOBILE, getRuntime, ingest$1o as ingestDenormalizedRecordRepresentation, initializeOneStore, registerReportObserver, reportGraphqlQueryParseError };
|
|
62059
|
-
// version: 1.
|
|
62443
|
+
// version: 1.455.0-c53c97de2a
|