@salesforce/lds-drafts 1.454.0 → 1.456.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/ldsDrafts.js
CHANGED
|
@@ -315,6 +315,25 @@ const DRAFT_SEGMENT = 'DRAFT';
|
|
|
315
315
|
*/
|
|
316
316
|
const DRAFT_ACTION_RETRY_COUNT_METADATA_KEY = 'retryCount';
|
|
317
317
|
class DurableDraftQueue {
|
|
318
|
+
// Instrumentation is strictly measure-only: a broken sink must never change queue behavior.
|
|
319
|
+
// Pass the sink into the callback so methods are invoked through their owning object and retain
|
|
320
|
+
// their receiver (some sinks use `this` to reach their metric registry).
|
|
321
|
+
emitInstrumentation(emit) {
|
|
322
|
+
const { instrumentation } = this;
|
|
323
|
+
if (instrumentation === undefined) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
emit(instrumentation);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// Telemetry failures must not interrupt draft processing.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
resetDrainCycle() {
|
|
334
|
+
this.drainCycleStart = undefined;
|
|
335
|
+
this.drainCycleBatchCount = 0;
|
|
336
|
+
}
|
|
318
337
|
getHandler(id) {
|
|
319
338
|
const handler = this.handlers[id];
|
|
320
339
|
if (handler === undefined) {
|
|
@@ -337,9 +356,19 @@ class DurableDraftQueue {
|
|
|
337
356
|
? __nimbus.plugins.JSLoggerPlugin
|
|
338
357
|
: undefined;
|
|
339
358
|
this.handlers = {};
|
|
359
|
+
// Drain-wait attribution state. uploadStartTimes stamps when each action first began
|
|
360
|
+
// uploading (keyed by action id) so actionCompleted/failed can emit its end-to-end drain time,
|
|
361
|
+
// including automatic retries/backoff. actionQueueWaitEmittedIds ensures those retries do not
|
|
362
|
+
// report the original enqueue wait more than once. drainCycleStart/BatchCount track one full
|
|
363
|
+
// idle->empty sweep. All measure-only; when no instrumentation is injected these stay untouched.
|
|
364
|
+
this.uploadStartTimes = new Map();
|
|
365
|
+
this.actionQueueWaitEmittedIds = new Set();
|
|
366
|
+
this.drainCycleBatchCount = 0;
|
|
340
367
|
this.draftStore = draftStore;
|
|
341
|
-
this.workerPool = new AsyncWorkerPool(1);
|
|
342
368
|
this.instrumentation = instrumentation;
|
|
369
|
+
this.workerPool = new AsyncWorkerPool(1, instrumentation?.onWorkerTiming !== undefined
|
|
370
|
+
? (waitMs, holdMs, queueDepth) => this.emitInstrumentation((sink) => sink.onWorkerTiming?.(waitMs, holdMs, queueDepth))
|
|
371
|
+
: undefined);
|
|
343
372
|
}
|
|
344
373
|
addHandler(handler) {
|
|
345
374
|
const id = handler.handlerId;
|
|
@@ -427,6 +456,9 @@ class DurableDraftQueue {
|
|
|
427
456
|
clearTimeout(this.timeoutHandler);
|
|
428
457
|
this.timeoutHandler = undefined;
|
|
429
458
|
}
|
|
459
|
+
// A stop/user edit breaks the current sweep. A later restart starts a fresh cycle rather
|
|
460
|
+
// than attributing stopped time to an offline drain.
|
|
461
|
+
this.resetDrainCycle();
|
|
430
462
|
this.state = DraftQueueState.Stopped;
|
|
431
463
|
}
|
|
432
464
|
async getQueueActions() {
|
|
@@ -457,7 +489,7 @@ class DurableDraftQueue {
|
|
|
457
489
|
}
|
|
458
490
|
return aTime - bTime;
|
|
459
491
|
});
|
|
460
|
-
|
|
492
|
+
this.emitInstrumentation((sink) => sink.onGetQueueActions?.(Date.now() - start, sorted.length));
|
|
461
493
|
return sorted;
|
|
462
494
|
}
|
|
463
495
|
async enqueue(handlerId, data, observabilityContext) {
|
|
@@ -501,6 +533,13 @@ class DurableDraftQueue {
|
|
|
501
533
|
await handler.handleActionCompleted(action, queueOperations, values(this.handlers));
|
|
502
534
|
this.retryIntervalMilliseconds = 0;
|
|
503
535
|
this.uploadingActionId = undefined;
|
|
536
|
+
// The action finished draining — emit its end-to-end drain time and
|
|
537
|
+
// count it toward the current drain-cycle batch. Measure-only.
|
|
538
|
+
this.emitActionDrainDuration(action.id, action.handler);
|
|
539
|
+
this.actionQueueWaitEmittedIds.delete(action.id);
|
|
540
|
+
if (this.drainCycleStart !== undefined) {
|
|
541
|
+
this.drainCycleBatchCount += 1;
|
|
542
|
+
}
|
|
504
543
|
await this.notifyChangedListeners({
|
|
505
544
|
type: DraftQueueEventType.ActionCompleted,
|
|
506
545
|
action,
|
|
@@ -512,6 +551,16 @@ class DurableDraftQueue {
|
|
|
512
551
|
},
|
|
513
552
|
});
|
|
514
553
|
}
|
|
554
|
+
// Read+clear the action's upload-start stamp and emit its drain duration. Safe to
|
|
555
|
+
// call when instrumentation is absent or the id was never stamped (no-op). Never emits the id.
|
|
556
|
+
emitActionDrainDuration(id, handlerId) {
|
|
557
|
+
const start = this.uploadStartTimes.get(id);
|
|
558
|
+
if (start === undefined) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.uploadStartTimes.delete(id);
|
|
562
|
+
this.emitInstrumentation((sink) => sink.onActionDrainDuration?.(handlerId, Date.now() - start));
|
|
563
|
+
}
|
|
515
564
|
async actionFailed(action, retry, retryDelayInMs, actionDataChanged) {
|
|
516
565
|
if (actionDataChanged === true) {
|
|
517
566
|
await this.draftStore.writeAction({
|
|
@@ -521,14 +570,24 @@ class DurableDraftQueue {
|
|
|
521
570
|
}
|
|
522
571
|
this.uploadingActionId = undefined;
|
|
523
572
|
if (retry && this.state !== DraftQueueState.Stopped) {
|
|
573
|
+
// A retry means this action is NOT done draining. Keep its first-upload stamp and the
|
|
574
|
+
// open cycle so the eventual outcome includes network time and retry/backoff.
|
|
524
575
|
this.state = DraftQueueState.Waiting;
|
|
525
576
|
return retryDelayInMs !== undefined
|
|
526
577
|
? this.scheduleRetryWithSpecifiedDelay(retryDelayInMs)
|
|
527
578
|
: this.scheduleRetry();
|
|
528
579
|
}
|
|
529
580
|
else if (isDraftError(action)) {
|
|
581
|
+
// Terminal failure — this action is done draining. Emit its drain time.
|
|
582
|
+
this.emitActionDrainDuration(action.id, action.handler);
|
|
583
|
+
// This was not a clean drain-to-empty. Discard the sweep before user intervention so a
|
|
584
|
+
// later manual retry/removal cannot inherit its elapsed time or completed batch count.
|
|
585
|
+
this.resetDrainCycle();
|
|
530
586
|
return this.handleServerError(action, action.error);
|
|
531
587
|
}
|
|
588
|
+
// Any other terminal path — ensure no stale start stamp lingers.
|
|
589
|
+
this.emitActionDrainDuration(action.id, action.handler);
|
|
590
|
+
this.resetDrainCycle();
|
|
532
591
|
}
|
|
533
592
|
handle(action) {
|
|
534
593
|
const handler = this.getHandler(action.handler);
|
|
@@ -548,12 +607,22 @@ class DurableDraftQueue {
|
|
|
548
607
|
this.state = DraftQueueState.Started;
|
|
549
608
|
}
|
|
550
609
|
this.processingAction = undefined;
|
|
610
|
+
// The queue drained empty — close the drain-cycle window if one was open.
|
|
611
|
+
if (this.drainCycleStart !== undefined) {
|
|
612
|
+
const durationMs = Date.now() - this.drainCycleStart;
|
|
613
|
+
const batchSize = this.drainCycleBatchCount;
|
|
614
|
+
this.resetDrainCycle();
|
|
615
|
+
this.emitInstrumentation((sink) => sink.onDrainCycle?.(durationMs, batchSize));
|
|
616
|
+
}
|
|
551
617
|
return ProcessActionResult.NO_ACTION_TO_PROCESS;
|
|
552
618
|
}
|
|
553
619
|
const { status, id } = action;
|
|
554
620
|
if (status === DraftActionStatus.Error) {
|
|
555
621
|
this.state = DraftQueueState.Error;
|
|
556
622
|
this.processingAction = undefined;
|
|
623
|
+
// The sweep is blocked on an errored action, not a clean drain-to-empty —
|
|
624
|
+
// discard the open drain-cycle window so it doesn't inflate a later cycle's duration.
|
|
625
|
+
this.resetDrainCycle();
|
|
557
626
|
await this.notifyChangedListeners({
|
|
558
627
|
type: DraftQueueEventType.ActionFailed,
|
|
559
628
|
action: action,
|
|
@@ -571,6 +640,31 @@ class DurableDraftQueue {
|
|
|
571
640
|
if (this.state === DraftQueueState.Waiting) {
|
|
572
641
|
this.state = DraftQueueState.Started;
|
|
573
642
|
}
|
|
643
|
+
// The action is now leaving the queue to upload. Emit head-of-line FIFO wait
|
|
644
|
+
// (enqueue -> upload start), stamp its drain-start for end-to-end timing on completion, and
|
|
645
|
+
// open a drain-cycle window if the queue was previously idle. All measure-only.
|
|
646
|
+
const { instrumentation } = this;
|
|
647
|
+
if (instrumentation !== undefined) {
|
|
648
|
+
const now = Date.now();
|
|
649
|
+
const { timestamp } = action;
|
|
650
|
+
if (instrumentation.onActionQueueWait !== undefined &&
|
|
651
|
+
!this.actionQueueWaitEmittedIds.has(id) &&
|
|
652
|
+
typeof timestamp === 'number' &&
|
|
653
|
+
timestamp > 0) {
|
|
654
|
+
// Mark before invoking the sink: even a throwing callback must not make a retry
|
|
655
|
+
// report the same action's enqueue wait again.
|
|
656
|
+
this.actionQueueWaitEmittedIds.add(id);
|
|
657
|
+
this.emitInstrumentation((sink) => sink.onActionQueueWait?.(action.handler, now - timestamp));
|
|
658
|
+
}
|
|
659
|
+
if (instrumentation.onActionDrainDuration !== undefined &&
|
|
660
|
+
!this.uploadStartTimes.has(id)) {
|
|
661
|
+
this.uploadStartTimes.set(id, now);
|
|
662
|
+
}
|
|
663
|
+
if (instrumentation.onDrainCycle !== undefined && this.drainCycleStart === undefined) {
|
|
664
|
+
this.drainCycleStart = now;
|
|
665
|
+
this.drainCycleBatchCount = 0;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
574
668
|
await this.notifyChangedListeners({
|
|
575
669
|
type: DraftQueueEventType.ActionUploading,
|
|
576
670
|
action: { ...action, status: DraftActionStatus.Uploading },
|
|
@@ -608,7 +702,7 @@ class DurableDraftQueue {
|
|
|
608
702
|
results.push(listener(event));
|
|
609
703
|
}
|
|
610
704
|
// W-23515682: listener fan-out per event type. Measure-only.
|
|
611
|
-
this.
|
|
705
|
+
this.emitInstrumentation((sink) => sink.onNotifyChangedListeners?.(event.type, draftQueueLen));
|
|
612
706
|
await Promise.all(results);
|
|
613
707
|
}
|
|
614
708
|
/**
|
|
@@ -637,9 +731,17 @@ class DurableDraftQueue {
|
|
|
637
731
|
const shouldDeleteRelated = handler.shouldDeleteActionByTagOnRemoval(action);
|
|
638
732
|
if (shouldDeleteRelated) {
|
|
639
733
|
await this.draftStore.deleteByTag(action.tag);
|
|
734
|
+
queue.forEach((queuedAction) => {
|
|
735
|
+
if (queuedAction.tag === action.tag) {
|
|
736
|
+
this.uploadStartTimes.delete(queuedAction.id);
|
|
737
|
+
this.actionQueueWaitEmittedIds.delete(queuedAction.id);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
640
740
|
}
|
|
641
741
|
else {
|
|
642
742
|
await this.draftStore.deleteDraft(action.id);
|
|
743
|
+
this.uploadStartTimes.delete(action.id);
|
|
744
|
+
this.actionQueueWaitEmittedIds.delete(action.id);
|
|
643
745
|
}
|
|
644
746
|
await handler.handleActionRemoved(action, queue.filter((x) => x.id !== actionId));
|
|
645
747
|
await this.notifyChangedListeners({
|
|
@@ -2,4 +2,8 @@ export interface DraftQueueInstrumentation {
|
|
|
2
2
|
onGetQueueActions?(durationMs: number, actionCount: number): void;
|
|
3
3
|
onNotifyChangedListeners?(eventType: string, listenerCount: number): void;
|
|
4
4
|
onDurableOperation?(op: string, segment: string, durationMs: number): void;
|
|
5
|
+
onWorkerTiming?(waitMs: number, holdMs: number, queueDepth: number): void;
|
|
6
|
+
onActionQueueWait?(handlerId: string, waitMs: number): void;
|
|
7
|
+
onActionDrainDuration?(handlerId: string, durationMs: number): void;
|
|
8
|
+
onDrainCycle?(durationMs: number, batchSize: number): void;
|
|
5
9
|
}
|
|
@@ -29,6 +29,12 @@ export declare class DurableDraftQueue implements DraftQueue {
|
|
|
29
29
|
private workerPool;
|
|
30
30
|
private handlers;
|
|
31
31
|
private instrumentation?;
|
|
32
|
+
private uploadStartTimes;
|
|
33
|
+
private actionQueueWaitEmittedIds;
|
|
34
|
+
private drainCycleStart?;
|
|
35
|
+
private drainCycleBatchCount;
|
|
36
|
+
private emitInstrumentation;
|
|
37
|
+
private resetDrainCycle;
|
|
32
38
|
private getHandler;
|
|
33
39
|
constructor(draftStore: DraftStore, instrumentation?: DraftQueueInstrumentation);
|
|
34
40
|
addHandler<Data>(handler: ActionHandler<Data, unknown>): Promise<void>;
|
|
@@ -46,6 +52,7 @@ export declare class DurableDraftQueue implements DraftQueue {
|
|
|
46
52
|
enqueue<Data>(handlerId: string, data: unknown, observabilityContext?: ObservabilityContext): Promise<PendingDraftAction<Data>>;
|
|
47
53
|
registerOnChangedListener(listener: DraftQueueChangeListener): () => Promise<void>;
|
|
48
54
|
actionCompleted(action: CompletedDraftAction<unknown, unknown>): Promise<void>;
|
|
55
|
+
private emitActionDrainDuration;
|
|
49
56
|
actionFailed(action: DraftAction<unknown, unknown>, retry: boolean, retryDelayInMs?: number, actionDataChanged?: boolean): Promise<void>;
|
|
50
57
|
handle(action: DraftAction<unknown, unknown>): Promise<ProcessActionResult>;
|
|
51
58
|
processNextAction(): Promise<ProcessActionResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/lds-drafts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.456.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
5
5
|
"description": "LDS Drafts",
|
|
6
6
|
"main": "dist/ldsDrafts.js",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@luvio/engine": "0.161.2",
|
|
28
28
|
"@luvio/environments": "0.161.2",
|
|
29
|
-
"@salesforce/lds-utils-adapters": "^1.
|
|
29
|
+
"@salesforce/lds-utils-adapters": "^1.456.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@salesforce/nimbus-plugin-lds": "^1.
|
|
32
|
+
"@salesforce/nimbus-plugin-lds": "^1.456.0"
|
|
33
33
|
},
|
|
34
34
|
"volta": {
|
|
35
35
|
"extends": "../../package.json"
|