@salesforce/lds-worker-api 1.465.0 → 1.467.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/sfdc/es/ldsWorkerApi.js +1 -1
- package/dist/standalone/es/lds-worker-api.js +3573 -370
- package/dist/standalone/umd/lds-worker-api.js +3573 -370
- package/package.json +10 -10
|
@@ -4276,7 +4276,7 @@ function withDefaultLuvio(callback) {
|
|
|
4276
4276
|
}
|
|
4277
4277
|
callbacks.push(callback);
|
|
4278
4278
|
}
|
|
4279
|
-
// version: 1.
|
|
4279
|
+
// version: 1.467.0-4f065d2a2a
|
|
4280
4280
|
|
|
4281
4281
|
// TODO [TD-0081508]: once that TD is fulfilled we can probably change this file
|
|
4282
4282
|
function instrumentAdapter$1(createFunction, _metadata) {
|
|
@@ -5320,7 +5320,7 @@ function createGraphQLWireAdapterConstructor(luvio, adapter, metadata, astResolv
|
|
|
5320
5320
|
const { apiFamily, name } = metadata;
|
|
5321
5321
|
return createGraphQLWireAdapterConstructor$1(adapter, `${apiFamily}.${name}`, luvio, astResolver);
|
|
5322
5322
|
}
|
|
5323
|
-
// version: 1.
|
|
5323
|
+
// version: 1.467.0-4f065d2a2a
|
|
5324
5324
|
|
|
5325
5325
|
function isSupportedEntity(_objectApiName) {
|
|
5326
5326
|
return true;
|
|
@@ -14373,7 +14373,7 @@ function coerceFormFactor$1(form) {
|
|
|
14373
14373
|
return undefined;
|
|
14374
14374
|
}
|
|
14375
14375
|
|
|
14376
|
-
const VERSION$10$2 = "
|
|
14376
|
+
const VERSION$10$2 = "c42f4294edf6efc71f5ff9b9adf922e8";
|
|
14377
14377
|
const RepresentationType$G = 'PlatformActionRepresentation';
|
|
14378
14378
|
function keyBuilder$1P$1(luvio, config) {
|
|
14379
14379
|
return keyPrefix$3 + '::' + RepresentationType$G + ':' + config.externalId + ':' + (config.relatedSourceObject === null ? '' : config.relatedSourceObject) + ':' + (config.relatedListRecordId === null ? '' : config.relatedListRecordId);
|
|
@@ -14433,6 +14433,11 @@ const select$1G$2 = function PlatformActionRepresentationSelect() {
|
|
|
14433
14433
|
name: 'lwcComponent',
|
|
14434
14434
|
kind: 'Scalar'
|
|
14435
14435
|
},
|
|
14436
|
+
{
|
|
14437
|
+
name: 'modalSize',
|
|
14438
|
+
kind: 'Scalar',
|
|
14439
|
+
required: false
|
|
14440
|
+
},
|
|
14436
14441
|
{
|
|
14437
14442
|
name: 'primaryColor',
|
|
14438
14443
|
kind: 'Scalar'
|
|
@@ -14533,6 +14538,19 @@ function equals$S(existing, incoming) {
|
|
|
14533
14538
|
if (!(existing_lwcComponent === incoming_lwcComponent)) {
|
|
14534
14539
|
return false;
|
|
14535
14540
|
}
|
|
14541
|
+
const existing_modalSize = existing.modalSize;
|
|
14542
|
+
const incoming_modalSize = incoming.modalSize;
|
|
14543
|
+
// if at least one of these optionals is defined
|
|
14544
|
+
if (existing_modalSize !== undefined || incoming_modalSize !== undefined) {
|
|
14545
|
+
// if one of these is not defined we know the other is defined and therefore
|
|
14546
|
+
// not equal
|
|
14547
|
+
if (existing_modalSize === undefined || incoming_modalSize === undefined) {
|
|
14548
|
+
return false;
|
|
14549
|
+
}
|
|
14550
|
+
if (!(existing_modalSize === incoming_modalSize)) {
|
|
14551
|
+
return false;
|
|
14552
|
+
}
|
|
14553
|
+
}
|
|
14536
14554
|
const existing_primaryColor = existing.primaryColor;
|
|
14537
14555
|
const incoming_primaryColor = incoming.primaryColor;
|
|
14538
14556
|
if (!(existing_primaryColor === incoming_primaryColor)) {
|
|
@@ -32547,7 +32565,7 @@ withDefaultLuvio((luvio) => {
|
|
|
32547
32565
|
throttle(60, 60000, setupNotifyAllListRecordUpdateAvailable(luvio));
|
|
32548
32566
|
throttle(60, 60000, setupNotifyAllListInfoSummaryUpdateAvailable(luvio));
|
|
32549
32567
|
});
|
|
32550
|
-
// version: 1.
|
|
32568
|
+
// version: 1.467.0-049a8b6027
|
|
32551
32569
|
|
|
32552
32570
|
var draftQueueMaxRetryAttemptsGate = {
|
|
32553
32571
|
isOpen: function (e) {
|
|
@@ -44079,6 +44097,15 @@ function parseAndVisit(source) {
|
|
|
44079
44097
|
return luvioDocumentNode;
|
|
44080
44098
|
}
|
|
44081
44099
|
|
|
44100
|
+
var parentRelationshipPrefetchKillSwitch = {
|
|
44101
|
+
isOpen: function (e) {
|
|
44102
|
+
return e.fallback;
|
|
44103
|
+
},
|
|
44104
|
+
hasError: function () {
|
|
44105
|
+
return !0;
|
|
44106
|
+
},
|
|
44107
|
+
};
|
|
44108
|
+
|
|
44082
44109
|
var graphqlRelationshipFieldsPerf = {
|
|
44083
44110
|
isOpen: function (e) {
|
|
44084
44111
|
return e.fallback;
|
|
@@ -48032,6 +48059,14 @@ var DraftQueueEventType;
|
|
|
48032
48059
|
* Triggered after the Draft Queue state changes
|
|
48033
48060
|
*/
|
|
48034
48061
|
DraftQueueEventType["QueueStateChanged"] = "state";
|
|
48062
|
+
/**
|
|
48063
|
+
* Triggered when batch-journal crash recovery (B2, Performance Upload path)
|
|
48064
|
+
* could not complete: a maybe-committed all-or-none batch's members could not
|
|
48065
|
+
* be durably quarantined this session, a startup reconcile failed, or a
|
|
48066
|
+
* resolved journal row could not be cleared. Carries only a bounded reason
|
|
48067
|
+
* code (no ids/messages) so it is safe to surface to o11y.
|
|
48068
|
+
*/
|
|
48069
|
+
DraftQueueEventType["BatchRecoveryFailed"] = "batchRecoveryFailed";
|
|
48035
48070
|
})(DraftQueueEventType || (DraftQueueEventType = {}));
|
|
48036
48071
|
var QueueOperationType;
|
|
48037
48072
|
(function (QueueOperationType) {
|
|
@@ -48241,6 +48276,8 @@ function customActionHandler(executor, id, draftQueue) {
|
|
|
48241
48276
|
mergeActions: () => {
|
|
48242
48277
|
throw Error('mergeActions not supported for custom actions');
|
|
48243
48278
|
},
|
|
48279
|
+
getDraftIdsFromAction: (action) => [action.targetId],
|
|
48280
|
+
getRedirectMappings: () => undefined,
|
|
48244
48281
|
};
|
|
48245
48282
|
}
|
|
48246
48283
|
|
|
@@ -48324,6 +48361,13 @@ class DurableDraftQueue {
|
|
|
48324
48361
|
const handler = customActionHandler(executor, id, this);
|
|
48325
48362
|
return this.addHandler(handler);
|
|
48326
48363
|
}
|
|
48364
|
+
/**
|
|
48365
|
+
* True when a handler for `id` is registered on this queue — i.e. this queue owns
|
|
48366
|
+
* (and drains) the actions carrying that handlerId.
|
|
48367
|
+
*/
|
|
48368
|
+
isHandlerRegistered(id) {
|
|
48369
|
+
return this.handlers[id] !== undefined;
|
|
48370
|
+
}
|
|
48327
48371
|
getQueueState() {
|
|
48328
48372
|
return this.state;
|
|
48329
48373
|
}
|
|
@@ -48348,6 +48392,11 @@ class DurableDraftQueue {
|
|
|
48348
48392
|
}
|
|
48349
48393
|
this.retryIntervalMilliseconds = 0;
|
|
48350
48394
|
this.state = DraftQueueState.Started;
|
|
48395
|
+
// Stuck-queue detection: a live upload keeps the queue Started (filtered by the early
|
|
48396
|
+
// return above), so a stale uploadingActionId here strands processNextAction() on it.
|
|
48397
|
+
if (this.uploadingActionId !== undefined && this.logger) {
|
|
48398
|
+
this.logger.logWarning(`Draft queue restarting with action still marked uploading (stale uploadingActionId=${this.uploadingActionId}); possible abandoned-upload stuck queue`);
|
|
48399
|
+
}
|
|
48351
48400
|
await this.notifyChangedListeners({
|
|
48352
48401
|
type: DraftQueueEventType.QueueStateChanged,
|
|
48353
48402
|
state: this.state,
|
|
@@ -48878,239 +48927,1814 @@ class DurableDraftQueue {
|
|
|
48878
48927
|
}
|
|
48879
48928
|
}
|
|
48880
48929
|
|
|
48881
|
-
|
|
48882
|
-
|
|
48930
|
+
/**
|
|
48931
|
+
* Narrows a peer DraftQueue to one that also participates in cross-queue
|
|
48932
|
+
* coordination. A peer may be a NimbusDraftQueue (gate-ON on-device path),
|
|
48933
|
+
* which implements DraftQueue but NOT CrossQueueCoordination, so this check
|
|
48934
|
+
* is load-bearing, not defensive.
|
|
48935
|
+
*/
|
|
48936
|
+
function isCrossQueueCoordination(queue) {
|
|
48937
|
+
const candidate = queue;
|
|
48938
|
+
return (typeof candidate.applyDraftIdRedirects === 'function' &&
|
|
48939
|
+
typeof candidate.resumeDeferredActions === 'function' &&
|
|
48940
|
+
typeof candidate.getInFlightRewritableDraftIds === 'function' &&
|
|
48941
|
+
typeof candidate.getOwnedQueueActions === 'function');
|
|
48883
48942
|
}
|
|
48884
|
-
|
|
48885
|
-
|
|
48886
|
-
|
|
48887
|
-
|
|
48943
|
+
/**
|
|
48944
|
+
* Maximum number of draft actions to combine into a single UI-API Batch
|
|
48945
|
+
* request when batching is enabled. Tunable; chosen as a conservative starting
|
|
48946
|
+
* point that bounds payload size and retry cost while still amortizing TTFB
|
|
48947
|
+
* across many actions.
|
|
48948
|
+
*/
|
|
48949
|
+
const MAX_BATCH_SIZE$1 = 25;
|
|
48950
|
+
/**
|
|
48951
|
+
* How many times clearBatchJournalIfSupported retries a failed journal clear
|
|
48952
|
+
* before giving up and escalating the log line. Bounded so a permanently-failing
|
|
48953
|
+
* store can't spin. A leftover row is mainly a scale concern (unbounded restart
|
|
48954
|
+
* reconcile); it can also cause a rare, fail-safe over-quarantine — a definitively
|
|
48955
|
+
* rolled-back batch's still-Pending siblings surface as commit-ambiguous on the
|
|
48956
|
+
* next restart (never a replay, since the envelope records no rolled-back outcome).
|
|
48957
|
+
* A small bound suffices for the spin guard; a durable rolled-back/resolved
|
|
48958
|
+
* tombstone is the follow-up to close the over-quarantine before queueBatching is
|
|
48959
|
+
* enabled (4077120435).
|
|
48960
|
+
*/
|
|
48961
|
+
const CLEAR_JOURNAL_MAX_ATTEMPTS = 3;
|
|
48962
|
+
/**
|
|
48963
|
+
* Diagnostic log line for the local-experimental batching path. Routes through
|
|
48964
|
+
* JSLoggerPlugin.logInfo so messages surface in the iOS device log, each line
|
|
48965
|
+
* prefixed with `Batch `. Silently no-ops outside the LWR JSC environment.
|
|
48966
|
+
*/
|
|
48967
|
+
function batchLog$1(line) {
|
|
48968
|
+
try {
|
|
48969
|
+
globalThis.__nimbus?.plugins?.JSLoggerPlugin?.logInfo(`Batch ${line}`);
|
|
48970
|
+
}
|
|
48971
|
+
catch {
|
|
48972
|
+
// debug-only; never fail real work because of logging
|
|
48973
|
+
}
|
|
48888
48974
|
}
|
|
48889
48975
|
/**
|
|
48890
|
-
*
|
|
48891
|
-
*
|
|
48892
|
-
*
|
|
48893
|
-
*
|
|
48894
|
-
*
|
|
48895
|
-
*
|
|
48896
|
-
*
|
|
48976
|
+
* Bounded, scrubbed rendering of a caught value for the device log. Emits only
|
|
48977
|
+
* the error constructor NAME (e.g. "TypeError"), never the free-text message,
|
|
48978
|
+
* which can embed record ids (URL path segments) or server field-validation
|
|
48979
|
+
* strings. Mirrors the device-log PII policy in metrics.ts
|
|
48980
|
+
* (reportDraftActionUploadError): bounded metadata only; full detail belongs on
|
|
48981
|
+
* the structured, scrubbed o11y path (4065667273).
|
|
48897
48982
|
*/
|
|
48898
|
-
|
|
48899
|
-
|
|
48900
|
-
|
|
48901
|
-
|
|
48902
|
-
|
|
48903
|
-
|
|
48904
|
-
//
|
|
48905
|
-
|
|
48906
|
-
|
|
48907
|
-
|
|
48908
|
-
|
|
48909
|
-
|
|
48983
|
+
function errName(e) {
|
|
48984
|
+
try {
|
|
48985
|
+
const name = e?.name;
|
|
48986
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
48987
|
+
return 'Error';
|
|
48988
|
+
}
|
|
48989
|
+
// Strip everything outside printable ASCII, then bound the length. The
|
|
48990
|
+
// id/free-text PII vector is already closed by reading `.name` (a
|
|
48991
|
+
// constructor name) and never `.message` (see above) — but a thrown object
|
|
48992
|
+
// can carry an arbitrary `name` (and reading it can trip a getter), so a
|
|
48993
|
+
// newline or control character could still break device-log-line integrity
|
|
48994
|
+
// and a pathological length could blow up a log entry. This drops control
|
|
48995
|
+
// sequences and bounds length; it does NOT attempt to strip a printable
|
|
48996
|
+
// identifier, since nothing in this path populates `.name` with one
|
|
48997
|
+
// (4077120502).
|
|
48998
|
+
const cleaned = name.replace(/[^\x20-\x7E]/g, '');
|
|
48999
|
+
if (cleaned.length === 0) {
|
|
49000
|
+
return 'Error';
|
|
49001
|
+
}
|
|
49002
|
+
return cleaned.length > 64 ? cleaned.slice(0, 64) : cleaned;
|
|
48910
49003
|
}
|
|
48911
|
-
|
|
48912
|
-
|
|
48913
|
-
|
|
48914
|
-
|
|
48915
|
-
return run();
|
|
48916
|
-
}
|
|
48917
|
-
const start = Date.now();
|
|
48918
|
-
return run().finally(() => {
|
|
48919
|
-
onDurableOperation(op, segment, Date.now() - start);
|
|
48920
|
-
});
|
|
49004
|
+
catch {
|
|
49005
|
+
// Reading `.name` threw (a hostile getter). Scrubbing a value for the
|
|
49006
|
+
// log must never itself throw and take down the caller's error path.
|
|
49007
|
+
return 'Error';
|
|
48921
49008
|
}
|
|
48922
|
-
|
|
48923
|
-
|
|
48924
|
-
|
|
48925
|
-
|
|
48926
|
-
|
|
48927
|
-
|
|
48928
|
-
|
|
48929
|
-
|
|
48930
|
-
|
|
48931
|
-
|
|
48932
|
-
|
|
49009
|
+
}
|
|
49010
|
+
/**
|
|
49011
|
+
* Builds the queue operations needed to rewrite every queued action whose
|
|
49012
|
+
* basePath/body text contains a draft id from `redirects`. If a queued
|
|
49013
|
+
* action's tag matches a redirect's draftKey, the action is deleted and
|
|
49014
|
+
* re-added at the canonical key. Pure function — no draft-store side
|
|
49015
|
+
* effects. Used by PerformanceDraftQueue.applyDraftIdRedirects to propagate
|
|
49016
|
+
* a peer queue's POST completion across queues.
|
|
49017
|
+
*/
|
|
49018
|
+
function rewriteQueueActionsForRedirects(queue, redirects) {
|
|
49019
|
+
const queueOperations = [];
|
|
49020
|
+
if (redirects.length === 0) {
|
|
49021
|
+
return queueOperations;
|
|
49022
|
+
}
|
|
49023
|
+
for (let i = 0, len = queue.length; i < len; i++) {
|
|
49024
|
+
const queueAction = queue[i];
|
|
49025
|
+
const data = queueAction.data;
|
|
49026
|
+
if (data === undefined ||
|
|
49027
|
+
data === null ||
|
|
49028
|
+
typeof data.method !== 'string' ||
|
|
49029
|
+
data.body === undefined) {
|
|
49030
|
+
continue;
|
|
49031
|
+
}
|
|
49032
|
+
let basePath = data.basePath || '';
|
|
49033
|
+
let stringifiedBody = JSON.stringify(data.body || '');
|
|
49034
|
+
let mutated = false;
|
|
49035
|
+
let updatedTag;
|
|
49036
|
+
let updatedTargetId;
|
|
49037
|
+
for (const r of redirects) {
|
|
49038
|
+
if (basePath.indexOf(r.draftId) >= 0 || stringifiedBody.indexOf(r.draftId) >= 0) {
|
|
49039
|
+
const re = new RegExp(r.draftId, 'g');
|
|
49040
|
+
basePath = basePath.replace(re, r.canonicalId);
|
|
49041
|
+
stringifiedBody = stringifiedBody.replace(re, r.canonicalId);
|
|
49042
|
+
mutated = true;
|
|
49043
|
+
}
|
|
49044
|
+
if (queueAction.tag === r.draftKey) {
|
|
49045
|
+
updatedTag = r.canonicalKey;
|
|
49046
|
+
updatedTargetId = r.canonicalId;
|
|
49047
|
+
}
|
|
49048
|
+
}
|
|
49049
|
+
if (!mutated) {
|
|
49050
|
+
continue;
|
|
49051
|
+
}
|
|
49052
|
+
const baseUpdated = {
|
|
49053
|
+
...queueAction,
|
|
49054
|
+
data: { ...data, basePath, body: JSON.parse(stringifiedBody) },
|
|
48933
49055
|
};
|
|
48934
|
-
|
|
49056
|
+
if (updatedTag !== undefined && updatedTargetId !== undefined) {
|
|
49057
|
+
queueOperations.push({ type: QueueOperationType.Delete, id: queueAction.id });
|
|
49058
|
+
queueOperations.push({
|
|
49059
|
+
type: QueueOperationType.Add,
|
|
49060
|
+
action: { ...baseUpdated, tag: updatedTag, targetId: updatedTargetId },
|
|
49061
|
+
});
|
|
49062
|
+
}
|
|
49063
|
+
else {
|
|
49064
|
+
queueOperations.push({
|
|
49065
|
+
type: QueueOperationType.Update,
|
|
49066
|
+
id: queueAction.id,
|
|
49067
|
+
action: baseUpdated,
|
|
49068
|
+
});
|
|
49069
|
+
}
|
|
48935
49070
|
}
|
|
48936
|
-
|
|
48937
|
-
|
|
48938
|
-
|
|
48939
|
-
|
|
48940
|
-
|
|
48941
|
-
|
|
48942
|
-
|
|
48943
|
-
|
|
48944
|
-
|
|
48945
|
-
|
|
48946
|
-
|
|
48947
|
-
|
|
48948
|
-
|
|
48949
|
-
|
|
49071
|
+
return queueOperations;
|
|
49072
|
+
}
|
|
49073
|
+
/**
|
|
49074
|
+
* The Performance Upload draft queue. Extends {@link DurableDraftQueue} — the
|
|
49075
|
+
* base class is byte-identical to today's shipped serial queue — and adds the
|
|
49076
|
+
* multi-candidate concurrent scheduler, Set-based in-flight tracking,
|
|
49077
|
+
* cross-queue redirect propagation, and UI-API batching. Only constructed on
|
|
49078
|
+
* the gate-ON code path; gate-off customers never run any of this.
|
|
49079
|
+
*/
|
|
49080
|
+
class PerformanceDraftQueue extends DurableDraftQueue {
|
|
49081
|
+
constructor(draftStore, { getPeerQueues = () => [], concurrency = 1, strictOrdering = true, queueBatching = false, } = {}) {
|
|
49082
|
+
super(draftStore);
|
|
49083
|
+
this.uploadingActionIds = new Set();
|
|
49084
|
+
// Reentrancy guard for processNextAction, which dispatches into free concurrency
|
|
49085
|
+
// slots with eligible actions. It is invoked un-awaited from several sites
|
|
49086
|
+
// (actionCompleted, applyDraftIdRedirects, resumeDeferredActions, enqueue)
|
|
49087
|
+
// and awaits mid-pass to read peer state, so without this two passes could
|
|
49088
|
+
// clear the concurrency guard, select the same action, and dispatch it
|
|
49089
|
+
// twice. `isDispatching` is set synchronously before any await; a concurrent
|
|
49090
|
+
// entrant records `redispatchRequested` and bails, and the active pass re-runs
|
|
49091
|
+
// to honor the missed wakeup.
|
|
49092
|
+
this.isDispatching = false;
|
|
49093
|
+
this.redispatchRequested = false;
|
|
49094
|
+
// Monotonic counter used to mint a unique batchId per dispatched batch
|
|
49095
|
+
// (combined with the head action id). Deterministic and dependency-free —
|
|
49096
|
+
// no Date/random — and unique within a session; across restarts a journal
|
|
49097
|
+
// is either cleared on completion or consumed by reconcile, so ids never
|
|
49098
|
+
// collide meaningfully.
|
|
49099
|
+
this.batchSequence = 0;
|
|
49100
|
+
// Guards the one-time batch-completion journal reconcile so it runs on the
|
|
49101
|
+
// first startQueue of the process (crash recovery) and not on every
|
|
49102
|
+
// stop/start cycle. Holds the in-flight reconcile promise rather than a
|
|
49103
|
+
// set-first boolean: startQueue can be called concurrently at launch (the
|
|
49104
|
+
// first call's reconcile awaits before super.startQueue() marks the queue
|
|
49105
|
+
// Started, so a second call still passes the base state guard). A boolean
|
|
49106
|
+
// set before the await would let that second call skip reconcile and begin
|
|
49107
|
+
// dispatching — re-batching a journaled, committed, not-yet-resolved member
|
|
49108
|
+
// and dropping its idempotency key. Every caller awaits the same promise,
|
|
49109
|
+
// so no dispatch begins until the members are surfaced as errors.
|
|
49110
|
+
this.reconcileBatchJournalsPromise = undefined;
|
|
49111
|
+
// Actions currently in retry backoff. A failed-but-retryable action stays
|
|
49112
|
+
// Pending in the store, so without this a re-entrant dispatch pass (a peer
|
|
49113
|
+
// kick or a sibling completion) could re-select and re-dispatch it before
|
|
49114
|
+
// its backoff timer fires. Populated in actionFailed's retry branch and
|
|
49115
|
+
// cleared per-action when that action's own backoff timer elapses, plus on
|
|
49116
|
+
// permanent failure / removal.
|
|
49117
|
+
this.retryingActionIds = new Set();
|
|
49118
|
+
// Members of a maybe-committed batch whose commit status could NOT be moved
|
|
49119
|
+
// to a durable Error this session (both the atomic completion and the
|
|
49120
|
+
// commit-ambiguous quarantine write failed — the store was rejecting writes
|
|
49121
|
+
// right after the server committed). They are still Pending in the store, so
|
|
49122
|
+
// without this guard a later startQueue / retryAllErrors / reconnect would
|
|
49123
|
+
// re-select them and re-batch keyless, replaying a maybe-committed write
|
|
49124
|
+
// (Option A forbids that). Excluded from dispatch (isDispatchable and the
|
|
49125
|
+
// single-action candidate loop) until they are durably resolved. In-memory
|
|
49126
|
+
// only: it bridges the current session; a restart clears it and the retained
|
|
49127
|
+
// batch journal is surfaced by reconcile before dispatch instead.
|
|
49128
|
+
this.commitAmbiguousUnresolvedIds = new Set();
|
|
49129
|
+
// Per-action retry backoff state. The serial base tracks a single
|
|
49130
|
+
// `timeoutHandler` + single `retryIntervalMilliseconds`, which conflate
|
|
49131
|
+
// concurrent retries: two actions failing at different times would overwrite
|
|
49132
|
+
// each other's timer (leaking the earlier one) and share one exponential
|
|
49133
|
+
// interval, so the first timer to fire would unpark every retrying action on
|
|
49134
|
+
// whichever backoff happened to be shortest. A concurrent queue instead
|
|
49135
|
+
// parks each action under its OWN timer and its OWN growing interval, keyed
|
|
49136
|
+
// by action id, so each retries on its own schedule independent of siblings.
|
|
49137
|
+
this.retryTimers = new Map();
|
|
49138
|
+
this.retryIntervals = new Map();
|
|
49139
|
+
// Draft ids of in-flight/just-completed POSTs whose redirects have not yet
|
|
49140
|
+
// been applied to peer queues. actionCompleted resolves a completing POST
|
|
49141
|
+
// out of the store (completeAction) BEFORE propagateRedirectsToPeers
|
|
49142
|
+
// fire-and-forgets the rewrite to peers, so between those two points
|
|
49143
|
+
// getInFlightRewritableDraftIds would no longer see the id in the store and
|
|
49144
|
+
// a peer could dispatch a queued action still carrying the stale draft id.
|
|
49145
|
+
// Ids are retained here before completeAction and released only once the
|
|
49146
|
+
// peer applyDraftIdRedirects calls settle; getInFlightRewritableDraftIds
|
|
49147
|
+
// unions this set so peers keep deferring across the gap.
|
|
49148
|
+
this.redirectingDraftIds = new Set();
|
|
49149
|
+
this.getPeerQueues = getPeerQueues;
|
|
49150
|
+
this.concurrency = concurrency;
|
|
49151
|
+
this.strictOrdering = strictOrdering;
|
|
49152
|
+
this.queueBatching = queueBatching;
|
|
49153
|
+
}
|
|
49154
|
+
/**
|
|
49155
|
+
* One-time batch-journal crash recovery (B2). A journal entry that survives
|
|
49156
|
+
* a restart means a batch was dispatched but its members were never
|
|
49157
|
+
* confirmed removed — the app may have died after the server committed the
|
|
49158
|
+
* all-or-none request. Any journaled member still Pending might therefore be
|
|
49159
|
+
* committed already, and the batch envelope carried no server-visible
|
|
49160
|
+
* idempotency key, so it must not be re-batched OR re-dispatched (either
|
|
49161
|
+
* would risk replaying a committed write). Surface every still-Pending member
|
|
49162
|
+
* as a commit-ambiguous error for manual resolution (Option A). The journal
|
|
49163
|
+
* entry is cleared only once its members are resolved (surfaced or already
|
|
49164
|
+
* gone), so a failure to record the error leaves the entry for the next
|
|
49165
|
+
* startup.
|
|
49166
|
+
*/
|
|
49167
|
+
reconcileBatchJournalsOnce() {
|
|
49168
|
+
// Memoize the in-flight promise so concurrent startQueue calls all await
|
|
49169
|
+
// the same reconcile (see reconcileBatchJournalsPromise). Assigning
|
|
49170
|
+
// before the first await is the point: a later caller reuses this
|
|
49171
|
+
// promise instead of racing past a boolean that was flipped before the
|
|
49172
|
+
// pinning work ran.
|
|
49173
|
+
//
|
|
49174
|
+
// Reconcile must fail CLOSED (4065665821): if it rejects, startQueue does
|
|
49175
|
+
// NOT dispatch. But the failure must be RETRYABLE — clear the memo on
|
|
49176
|
+
// rejection so the next startQueue re-runs the reconcile rather than
|
|
49177
|
+
// reusing a cached rejection and never recovering. Success stays memoized
|
|
49178
|
+
// (runs at most once per process).
|
|
49179
|
+
if (this.reconcileBatchJournalsPromise === undefined) {
|
|
49180
|
+
this.reconcileBatchJournalsPromise = this.runBatchJournalReconcile().catch((e) => {
|
|
49181
|
+
this.reconcileBatchJournalsPromise = undefined;
|
|
49182
|
+
// Single choke point for every reconcile failure (journal read
|
|
49183
|
+
// error, resolve failure, or missing journal support): surface one
|
|
49184
|
+
// bounded o11y signal before re-throwing so startQueue still fails
|
|
49185
|
+
// closed. Fire-and-forget — the rejection must propagate unchanged.
|
|
49186
|
+
this.signalBatchRecoveryFailed('reconcile-failed');
|
|
49187
|
+
throw e;
|
|
49188
|
+
});
|
|
49189
|
+
}
|
|
49190
|
+
return this.reconcileBatchJournalsPromise;
|
|
49191
|
+
}
|
|
49192
|
+
async runBatchJournalReconcile() {
|
|
49193
|
+
// Nothing journals when batching is off, so there is nothing to reconcile.
|
|
49194
|
+
if (!this.queueBatching) {
|
|
49195
|
+
return;
|
|
49196
|
+
}
|
|
49197
|
+
const store = this.draftStore;
|
|
49198
|
+
if (typeof store.readRecoverableBatchJournals !== 'function' ||
|
|
49199
|
+
typeof store.clearBatchJournal !== 'function' ||
|
|
49200
|
+
typeof store.writeBatchJournal !== 'function') {
|
|
49201
|
+
// Batching is ON but the store cannot journal. Dispatching would batch
|
|
49202
|
+
// keyless writes with no crash backstop, so refuse to start rather than
|
|
49203
|
+
// silently run unprotected (4065665821). Throwing keeps startQueue
|
|
49204
|
+
// stopped; a store misconfigured this way will keep failing, which is
|
|
49205
|
+
// the intended fail-closed outcome.
|
|
49206
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
49207
|
+
throw new Error('PerformanceDraftQueue: batching enabled but the DraftStore lacks batch-journal support');
|
|
49208
|
+
}
|
|
49209
|
+
// A read failure THROWS here (not swallowed): it propagates through
|
|
49210
|
+
// reconcileBatchJournalsOnce so startQueue stays stopped and retries,
|
|
49211
|
+
// rather than proceeding to dispatch a member of a batch that may have
|
|
49212
|
+
// already committed (4065665821). Only RECOVERABLE journals are read —
|
|
49213
|
+
// crash residue from a dead process. A journal stamped with the live
|
|
49214
|
+
// session's token is a batch still in flight (possibly on a sibling queue
|
|
49215
|
+
// sharing this store); reconciling it would quarantine live members out
|
|
49216
|
+
// from under their owner, so the store filters those out.
|
|
49217
|
+
const journals = await store.readRecoverableBatchJournals();
|
|
49218
|
+
if (journals.length === 0) {
|
|
49219
|
+
return;
|
|
49220
|
+
}
|
|
49221
|
+
for (const journal of journals) {
|
|
49222
|
+
// A journaled member that never completed was in-flight when the
|
|
49223
|
+
// process died: the batch MAY have committed, and the envelope carried
|
|
49224
|
+
// no server-visible idempotency key, so we do NOT re-batch or
|
|
49225
|
+
// re-dispatch it. Surface every still-Pending member as commit-ambiguous
|
|
49226
|
+
// for manual resolution (Option A), then clear the journal once resolved.
|
|
49227
|
+
// A resolve failure THROWS (via reconcileBatchJournalsOnce) so startup
|
|
49228
|
+
// stays stopped-but-retryable and the journal is retained.
|
|
49229
|
+
//
|
|
49230
|
+
// errorAmbiguousActions reads its own fresh draft snapshot per call
|
|
49231
|
+
// rather than sharing one across this loop: each iteration awaits
|
|
49232
|
+
// completeAction and listener callbacks, which can delete a member
|
|
49233
|
+
// (listener re-entrancy, or a concurrent removeDraftAction/deleteByTag),
|
|
49234
|
+
// and a snapshot held across the loop would let a later journal
|
|
49235
|
+
// resurrect that just-deleted member as an Error draft via the blind
|
|
49236
|
+
// Update in completeAction (4077120311). A fresh read per journal costs
|
|
49237
|
+
// O(journals × drafts), acceptable on this once-per-process
|
|
49238
|
+
// crash-recovery path.
|
|
49239
|
+
const resolved = await this.errorAmbiguousActions(journal.memberIds);
|
|
49240
|
+
if (resolved) {
|
|
49241
|
+
await this.clearBatchJournalIfSupported(journal.batchId);
|
|
48950
49242
|
}
|
|
48951
49243
|
else {
|
|
48952
|
-
//
|
|
48953
|
-
|
|
48954
|
-
const { draftStore } = this;
|
|
48955
|
-
const keys$1 = keys$4(draftStore);
|
|
48956
|
-
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
48957
|
-
actionArray.push(clone$1(draftStore[keys$1[i]]));
|
|
48958
|
-
}
|
|
49244
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
49245
|
+
throw new Error(`PerformanceDraftQueue: batch journal ${journal.batchId} could not be reconciled`);
|
|
48959
49246
|
}
|
|
48960
|
-
|
|
48961
|
-
});
|
|
49247
|
+
}
|
|
48962
49248
|
}
|
|
48963
|
-
|
|
48964
|
-
|
|
48965
|
-
|
|
48966
|
-
|
|
48967
|
-
|
|
48968
|
-
|
|
48969
|
-
|
|
48970
|
-
|
|
49249
|
+
// Overrides DurableDraftQueue.getQueueActions rather than reusing it: the
|
|
49250
|
+
// base marks the single in-flight action from its private `uploadingActionId`
|
|
49251
|
+
// (serial path), while the concurrent path tracks many in-flight ids in
|
|
49252
|
+
// `uploadingActionIds`. The base field is private, so the uploading check
|
|
49253
|
+
// cannot be shared without a base refactor; the rest of the body (sort) is
|
|
49254
|
+
// deliberately kept identical. If the base sort changes, mirror it here.
|
|
49255
|
+
async getQueueActions() {
|
|
49256
|
+
const drafts = (await this.draftStore.getAllDrafts());
|
|
49257
|
+
const queue = [];
|
|
49258
|
+
drafts.forEach((draft) => {
|
|
49259
|
+
if (this.uploadingActionIds.has(draft.id)) {
|
|
49260
|
+
draft.status = DraftActionStatus.Uploading;
|
|
49261
|
+
}
|
|
49262
|
+
queue.push(draft);
|
|
49263
|
+
});
|
|
49264
|
+
return queue.sort((a, b) => {
|
|
49265
|
+
const aTime = parseInt(a.id, 10);
|
|
49266
|
+
const bTime = parseInt(b.id, 10);
|
|
49267
|
+
// safety check
|
|
49268
|
+
if (isNaN(aTime)) {
|
|
49269
|
+
return 1;
|
|
48971
49270
|
}
|
|
49271
|
+
if (isNaN(bTime)) {
|
|
49272
|
+
return -1;
|
|
49273
|
+
}
|
|
49274
|
+
return aTime - bTime;
|
|
49275
|
+
});
|
|
49276
|
+
}
|
|
49277
|
+
async getOwnedQueueActions() {
|
|
49278
|
+
const all = await this.getQueueActions();
|
|
49279
|
+
return all.filter((a) => this.handlers[a.handler] !== undefined);
|
|
49280
|
+
}
|
|
49281
|
+
/** Draft ids `action` creates — the same set the dispatch-time dependency check uses. */
|
|
49282
|
+
getCreatedDraftIds(action) {
|
|
49283
|
+
const ids = new Set();
|
|
49284
|
+
this.collectDraftIds(action, ids);
|
|
49285
|
+
return Array.from(ids);
|
|
49286
|
+
}
|
|
49287
|
+
/** Owned actions whose basePath or body references any of `draftIds`. */
|
|
49288
|
+
async getOwnedActionsReferencing(draftIds) {
|
|
49289
|
+
const ids = new Set(draftIds);
|
|
49290
|
+
const owned = await this.getOwnedQueueActions();
|
|
49291
|
+
return owned.filter((action) => this.actionReferencesAny(action, ids));
|
|
49292
|
+
}
|
|
49293
|
+
async actionCompleted(action) {
|
|
49294
|
+
return this.workerPool.push({
|
|
49295
|
+
workFn: async () => {
|
|
49296
|
+
const handler = this.getHandler(action.handler);
|
|
49297
|
+
// Compute the redirects this completion produces BEFORE
|
|
49298
|
+
// completeAction resolves the POST out of the store, and retain
|
|
49299
|
+
// the draft ids they resolve. completeAction below removes the
|
|
49300
|
+
// completing POST from the store, so getInFlightRewritableDraftIds
|
|
49301
|
+
// would stop reporting its draft id — but the fire-and-forget
|
|
49302
|
+
// propagation to peers hasn't run yet, so a peer could dispatch a
|
|
49303
|
+
// queued action still carrying that stale draft id. Retaining the
|
|
49304
|
+
// ids (released only once the peer rewrites settle, below) keeps
|
|
49305
|
+
// peers deferring across that gap. Only bother when there are
|
|
49306
|
+
// peers to propagate to — with none, there is nothing to retain
|
|
49307
|
+
// for and no handler consultation to pay for.
|
|
49308
|
+
const peers = this.getPeerQueues().filter(isCrossQueueCoordination);
|
|
49309
|
+
const redirects = peers.length > 0 && handler.getRedirectMappings !== undefined
|
|
49310
|
+
? handler.getRedirectMappings(action)
|
|
49311
|
+
: undefined;
|
|
49312
|
+
const retainedDraftIds = this.retainRedirectingDraftIds(redirects);
|
|
49313
|
+
try {
|
|
49314
|
+
let queue = await this.getQueueActions();
|
|
49315
|
+
const queueOperations = handler.getQueueOperationsForCompletingDrafts(queue, action);
|
|
49316
|
+
// write the queue operations to the store prior to ingesting the result
|
|
49317
|
+
await this.draftStore.completeAction(queueOperations);
|
|
49318
|
+
await this.finalizeCompletedAction(action, handler, queueOperations);
|
|
49319
|
+
if (this.state === DraftQueueState.Started) {
|
|
49320
|
+
this.processNextAction();
|
|
49321
|
+
}
|
|
49322
|
+
// Propagate redirects to peer queues; fire-and-forget. Awaiting
|
|
49323
|
+
// peer.applyDraftIdRedirects from inside this worker callback
|
|
49324
|
+
// produces a cross-queue worker-pool deadlock when the peer is
|
|
49325
|
+
// also propagating back to this queue. Release the retained ids
|
|
49326
|
+
// only once every peer's rewrite settles.
|
|
49327
|
+
this.propagateRedirectsToPeers(redirects, () => this.releaseRedirectingDraftIds(retainedDraftIds));
|
|
49328
|
+
}
|
|
49329
|
+
catch (e) {
|
|
49330
|
+
// Any step above can throw before propagateRedirectsToPeers
|
|
49331
|
+
// schedules the release. Release the retained ids here so they
|
|
49332
|
+
// don't leak into redirectingDraftIds permanently (which would
|
|
49333
|
+
// make getInFlightRewritableDraftIds report them forever and
|
|
49334
|
+
// strand peers deferring on them). Must be a catch, not a
|
|
49335
|
+
// finally — on the success path release happens only after
|
|
49336
|
+
// peers apply the rewrites, so releasing here too early would
|
|
49337
|
+
// reopen the cross-queue stale-id race.
|
|
49338
|
+
this.releaseRedirectingDraftIds(retainedDraftIds);
|
|
49339
|
+
throw e;
|
|
49340
|
+
}
|
|
49341
|
+
},
|
|
49342
|
+
});
|
|
49343
|
+
}
|
|
49344
|
+
/**
|
|
49345
|
+
* Shared in-memory finalization for a completed action, run after its durable
|
|
49346
|
+
* removal: ingest the response and apply side effects (handleActionCompleted),
|
|
49347
|
+
* drop any retry backoff state, release the in-flight claim, and notify
|
|
49348
|
+
* listeners. Used by both the single-action completion path (actionCompleted)
|
|
49349
|
+
* and the atomic batch completion path (completeBatchAtomically), which each
|
|
49350
|
+
* wrap it in their own error handling.
|
|
49351
|
+
*/
|
|
49352
|
+
async finalizeCompletedAction(action, handler, queueOperations) {
|
|
49353
|
+
await handler.handleActionCompleted(action, queueOperations, values$2(this.handlers));
|
|
49354
|
+
// A completed action is no longer backing off: drop its timer, interval,
|
|
49355
|
+
// and parked flag.
|
|
49356
|
+
this.clearActionRetry(action.id);
|
|
49357
|
+
this.uploadingActionIds.delete(action.id);
|
|
49358
|
+
await this.notifyChangedListeners({
|
|
49359
|
+
type: DraftQueueEventType.ActionCompleted,
|
|
49360
|
+
action,
|
|
49361
|
+
draftCount: this.draftStore.getCount(),
|
|
49362
|
+
});
|
|
49363
|
+
}
|
|
49364
|
+
async actionFailed(action, retry, retryDelayInMs, actionDataChanged) {
|
|
49365
|
+
if (actionDataChanged === true) {
|
|
49366
|
+
await this.draftStore.writeAction({
|
|
49367
|
+
...action,
|
|
49368
|
+
status: DraftActionStatus.Pending,
|
|
49369
|
+
});
|
|
49370
|
+
}
|
|
49371
|
+
this.uploadingActionIds.delete(action.id);
|
|
49372
|
+
if (retry && this.state !== DraftQueueState.Stopped) {
|
|
49373
|
+
// Park the action so a re-entrant dispatch pass (peer kick / sibling
|
|
49374
|
+
// completion) can't re-select it while it's Pending in the store and
|
|
49375
|
+
// waiting on its backoff timer. Cleared when this action's own timer
|
|
49376
|
+
// fires.
|
|
49377
|
+
this.retryingActionIds.add(action.id);
|
|
49378
|
+
await this.scheduleActionRetry(action.id, retryDelayInMs);
|
|
49379
|
+
return;
|
|
49380
|
+
}
|
|
49381
|
+
// Permanent failure. Awaiting handleServerError (rather than returning
|
|
49382
|
+
// it) guarantees the errored action is persisted before this method
|
|
49383
|
+
// resolves, so any dispatch pass on the next tick observes the error
|
|
49384
|
+
// state instead of racing the write.
|
|
49385
|
+
if (isDraftError(action)) {
|
|
49386
|
+
this.clearActionRetry(action.id);
|
|
49387
|
+
await this.handleServerError(action, action.error);
|
|
49388
|
+
// A permanently-failed POST never resolves the draft ids it would
|
|
49389
|
+
// have created, so peers that deferred actions on those ids would
|
|
49390
|
+
// otherwise stall until an unrelated success on a later tick wakes
|
|
49391
|
+
// them. Wake them explicitly so they can re-evaluate (and surface
|
|
49392
|
+
// their own error, rather than hang).
|
|
49393
|
+
this.getPeerQueues()
|
|
49394
|
+
.filter(isCrossQueueCoordination)
|
|
49395
|
+
.forEach((peer) => peer.resumeDeferredActions());
|
|
49396
|
+
}
|
|
49397
|
+
}
|
|
49398
|
+
async handleBatch(actions) {
|
|
49399
|
+
const handler = this.getHandler(actions[0].handler);
|
|
49400
|
+
if (handler === undefined) {
|
|
49401
|
+
return Promise.reject(`No handler for ${actions[0].handler}.`);
|
|
49402
|
+
}
|
|
49403
|
+
if (handler.handleBatchActions === undefined) {
|
|
49404
|
+
return Promise.reject(`Handler ${actions[0].handler} does not support batching.`);
|
|
49405
|
+
}
|
|
49406
|
+
const batchIds = actions.map((a) => a.id);
|
|
49407
|
+
// B2: buffer the members the handler reports completed instead of
|
|
49408
|
+
// removing each from the store as its callback fires. On success they
|
|
49409
|
+
// are removed in ONE atomic durable write (completeBatchAtomically), so
|
|
49410
|
+
// there is no partial-removal window that a crash — or a completion
|
|
49411
|
+
// callback throwing mid-loop — could strand. The stranded-member bug it
|
|
49412
|
+
// prevents: a batch commits all-or-none on the server exactly once, but
|
|
49413
|
+
// members removed one-at-a-time can leave committed members Pending,
|
|
49414
|
+
// which restart (or the handler's broad retry) would re-batch WITHOUT
|
|
49415
|
+
// their per-action idempotency keys (the batch envelope drops them),
|
|
49416
|
+
// replaying a committed PATCH's automation or failing a committed DELETE.
|
|
49417
|
+
const completed = [];
|
|
49418
|
+
const batchActionCompleted = (action) => {
|
|
49419
|
+
completed.push(action);
|
|
48972
49420
|
return Promise.resolve();
|
|
48973
49421
|
};
|
|
48974
|
-
|
|
48975
|
-
|
|
48976
|
-
|
|
48977
|
-
|
|
48978
|
-
|
|
48979
|
-
|
|
48980
|
-
|
|
48981
|
-
|
|
48982
|
-
|
|
48983
|
-
|
|
48984
|
-
|
|
48985
|
-
|
|
48986
|
-
|
|
48987
|
-
|
|
49422
|
+
// Mint a unique id for this batch and journal its members BEFORE
|
|
49423
|
+
// dispatch (B2). If the process dies — or the atomic removal below fails
|
|
49424
|
+
// — between the server committing the request and the members being
|
|
49425
|
+
// removed, the next startQueue reconcile finds this journal and surfaces
|
|
49426
|
+
// any still-Pending member as commit-ambiguous (Option A: not auto-replayed,
|
|
49427
|
+
// because the batch envelope carries no server-visible idempotency key).
|
|
49428
|
+
// Journal support is optional; a store without it (never the gate-ON path)
|
|
49429
|
+
// skips this.
|
|
49430
|
+
const batchId = `${actions[0].id}:${(this.batchSequence += 1)}`;
|
|
49431
|
+
try {
|
|
49432
|
+
await this.writeBatchJournalIfSupported(batchId, batchIds);
|
|
49433
|
+
}
|
|
49434
|
+
catch (e) {
|
|
49435
|
+
// Fail CLOSED (4048061181): without a durable journal a mid-flight crash
|
|
49436
|
+
// would revive these members Pending with no record that they were
|
|
49437
|
+
// in-flight, so they would be re-batched — replaying a maybe-committed
|
|
49438
|
+
// write. Do NOT dispatch the batch. Release every member's in-flight
|
|
49439
|
+
// claim and dispatch the head alone via the single-action path, which
|
|
49440
|
+
// sends its idempotency key on the first attempt and so is replay-safe;
|
|
49441
|
+
// the remaining members re-arm and drain one-at-a-time single-action
|
|
49442
|
+
// while journaling is unavailable.
|
|
49443
|
+
batchLog$1(`journal write failed, single-action fallback batchId=${batchId} err=${errName(e)}`);
|
|
49444
|
+
for (const id of batchIds) {
|
|
49445
|
+
this.uploadingActionIds.delete(id);
|
|
49446
|
+
}
|
|
49447
|
+
this.uploadingActionIds.add(actions[0].id);
|
|
49448
|
+
return this.handle(actions[0]);
|
|
49449
|
+
}
|
|
49450
|
+
// Shared post-dispatch bookkeeping: release every batch member's uploading
|
|
49451
|
+
// claim, then handle the batch journal + still-Pending members per the
|
|
49452
|
+
// outcome, and issue the single trailing processNextAction. Two shapes:
|
|
49453
|
+
//
|
|
49454
|
+
// - Commit-AMBIGUOUS (result === NETWORK_ERROR, or a promise rejection):
|
|
49455
|
+
// the batch was dispatched but its outcome could not be confirmed
|
|
49456
|
+
// (network throw, empty-body 2xx, misaligned response, a retryable
|
|
49457
|
+
// 5xx/408/dropped-connection or generic-400 envelope that may have
|
|
49458
|
+
// committed with the 2xx lost — see executeBatch / classifyBatchFailure).
|
|
49459
|
+
// A rejection likewise implies a dispatch attempt whose cleanup then
|
|
49460
|
+
// threw, so it is treated as ambiguous too (it must NOT be read as
|
|
49461
|
+
// "nothing was sent"). The all-or-none request MAY have committed, and
|
|
49462
|
+
// the batch envelope carries no server-visible idempotency key, so a
|
|
49463
|
+
// re-send (batch OR serial) could replay a committed write. Do NOT
|
|
49464
|
+
// auto-replay (Option A): surface every still-Pending member as an error
|
|
49465
|
+
// for manual resolution, then clear the now-resolved journal (its job —
|
|
49466
|
+
// catching a mid-flight crash before we could record the ambiguity — is
|
|
49467
|
+
// done once the members carry the durable error).
|
|
49468
|
+
//
|
|
49469
|
+
// - Definitive non-success (any other non-success result): a terminal or
|
|
49470
|
+
// rolled-back envelope (server-generated 4xx, 429) or a partial
|
|
49471
|
+
// all-or-none failure — the server rolled the whole batch back or
|
|
49472
|
+
// rejected it before commit, so nothing committed and re-batching on
|
|
49473
|
+
// retry is replay-safe. The journal is stale: clear it. The handler
|
|
49474
|
+
// routed the offender (terminal, or parked in retryingActionIds) and
|
|
49475
|
+
// deliberately left innocent rolled-back siblings Pending/unparked (B5);
|
|
49476
|
+
// we only re-notify still-Pending members to clear the "uploading" UI
|
|
49477
|
+
// state.
|
|
49478
|
+
const finalize = async (result, rejected) => {
|
|
49479
|
+
for (const id of batchIds) {
|
|
49480
|
+
this.uploadingActionIds.delete(id);
|
|
49481
|
+
}
|
|
49482
|
+
if (rejected || result === ProcessActionResult.NETWORK_ERROR) {
|
|
49483
|
+
// Commit-ambiguous: surface members for manual resolution, then
|
|
49484
|
+
// clear the journal ONLY once the ambiguity is durably recorded on
|
|
49485
|
+
// every still-Pending member. If a member could not be moved to
|
|
49486
|
+
// Error, retain the journal so the next startQueue reconcile can
|
|
49487
|
+
// surface it — clearing now would strand it Pending with no backstop
|
|
49488
|
+
// and let a restart re-batch it (the replay Option A prevents).
|
|
49489
|
+
const resolved = await this.errorAmbiguousActions(batchIds);
|
|
49490
|
+
if (resolved) {
|
|
49491
|
+
await this.clearBatchJournalIfSupported(batchId);
|
|
49492
|
+
}
|
|
49493
|
+
else {
|
|
49494
|
+
// errorAmbiguousActions could not durably move the members to
|
|
49495
|
+
// Error (its bulk write failed); it stopped the queue and
|
|
49496
|
+
// quarantined them in-memory, and the journal is retained for
|
|
49497
|
+
// the next reconcile. Surface the failure as a bounded signal.
|
|
49498
|
+
await this.signalBatchRecoveryFailed('quarantine-failed');
|
|
48988
49499
|
}
|
|
48989
49500
|
}
|
|
48990
|
-
|
|
49501
|
+
else if (result !== ProcessActionResult.ACTION_SUCCEEDED) {
|
|
49502
|
+
// Definitively rolled back: journal is stale, clear it.
|
|
49503
|
+
await this.clearBatchJournalIfSupported(batchId);
|
|
49504
|
+
const queue = await this.getQueueActions();
|
|
49505
|
+
for (const id of batchIds) {
|
|
49506
|
+
const action = queue.find((a) => a.id === id);
|
|
49507
|
+
if (action && action.status === DraftActionStatus.Pending) {
|
|
49508
|
+
await this.notifyChangedListeners({
|
|
49509
|
+
type: DraftQueueEventType.ActionUpdated,
|
|
49510
|
+
action,
|
|
49511
|
+
draftCount: this.draftStore.getCount(),
|
|
49512
|
+
});
|
|
49513
|
+
}
|
|
49514
|
+
}
|
|
49515
|
+
}
|
|
49516
|
+
if (this.state === DraftQueueState.Started) {
|
|
49517
|
+
this.processNextAction();
|
|
49518
|
+
}
|
|
49519
|
+
return result;
|
|
48991
49520
|
};
|
|
48992
|
-
return
|
|
49521
|
+
return handler
|
|
49522
|
+
.handleBatchActions(actions, batchActionCompleted, this.actionFailed.bind(this))
|
|
49523
|
+
.then(async (result) => {
|
|
49524
|
+
batchLog$1(`dispatch done result=${result} size=${actions.length}`);
|
|
49525
|
+
if (result === ProcessActionResult.ACTION_SUCCEEDED) {
|
|
49526
|
+
// Atomically remove every committed member and clear the
|
|
49527
|
+
// journal. Never rejects — it protects members itself if
|
|
49528
|
+
// the durable write fails (see below).
|
|
49529
|
+
await this.completeBatchAtomically(batchId, completed);
|
|
49530
|
+
}
|
|
49531
|
+
return finalize(result, false);
|
|
49532
|
+
}, (e) => {
|
|
49533
|
+
batchLog$1(`dispatch rejected err=${errName(e)} size=${actions.length}`);
|
|
49534
|
+
return finalize(ProcessActionResult.ACTION_ERRORED, true);
|
|
49535
|
+
});
|
|
48993
49536
|
}
|
|
48994
|
-
|
|
48995
|
-
|
|
48996
|
-
|
|
48997
|
-
|
|
48998
|
-
|
|
48999
|
-
|
|
49000
|
-
|
|
49001
|
-
|
|
49002
|
-
|
|
49003
|
-
|
|
49004
|
-
|
|
49005
|
-
|
|
49006
|
-
|
|
49007
|
-
|
|
49008
|
-
|
|
49009
|
-
|
|
49537
|
+
/**
|
|
49538
|
+
* Atomically completes every successfully-uploaded member of a batch (B2).
|
|
49539
|
+
*
|
|
49540
|
+
* The server commits an all-or-none batch exactly once; the old path removed
|
|
49541
|
+
* members one-at-a-time, so a crash — or a completion callback throwing
|
|
49542
|
+
* mid-loop — could strand already-committed members Pending, which restart
|
|
49543
|
+
* (or the handler's broad retry) would re-batch WITHOUT their idempotency
|
|
49544
|
+
* keys and replay. This removes ALL members in ONE durable batchOperations
|
|
49545
|
+
* (the base completeAction applies the whole QueueOperation[] in a single
|
|
49546
|
+
* transaction), so there is no partial-removal state, then clears the
|
|
49547
|
+
* pre-dispatch journal. If any step throws after the server committed, this
|
|
49548
|
+
* fails CLOSED (4065665227): it quarantines every still-Pending member as a
|
|
49549
|
+
* commit-ambiguous error and retains the journal unless every member is
|
|
49550
|
+
* durably resolved, so the trailing finalize's claim release cannot let
|
|
49551
|
+
* collectBatch re-batch a keyless, maybe-committed member (Option A — the
|
|
49552
|
+
* batch envelope drops the idempotency key, so a re-send could replay a
|
|
49553
|
+
* committed write). Whatever the journal does not resolve this session is
|
|
49554
|
+
* surfaced by the next startQueue reconcile. The per-member cache ingest and
|
|
49555
|
+
* listener notification run afterward: they are best-effort cache updates, so
|
|
49556
|
+
* a throw there cannot corrupt the already-correct durable state.
|
|
49557
|
+
*
|
|
49558
|
+
* Batched members are UPDATE/DELETE only (collectBatch excludes POSTs), so
|
|
49559
|
+
* none mint server ids — getRedirectMappings returns undefined and each
|
|
49560
|
+
* member's queue ops reduce to a single self-delete, independent of the
|
|
49561
|
+
* others. That is why one queue snapshot yields correct ops for every member
|
|
49562
|
+
* and why the cross-queue redirect machinery on the single-action completion
|
|
49563
|
+
* path is not needed here. Never rejects.
|
|
49564
|
+
*/
|
|
49565
|
+
completeBatchAtomically(batchId, completed) {
|
|
49566
|
+
return this.workerPool.push({
|
|
49567
|
+
workFn: async () => {
|
|
49568
|
+
if (completed.length === 0) {
|
|
49569
|
+
await this.clearBatchJournalIfSupported(batchId);
|
|
49570
|
+
return;
|
|
49571
|
+
}
|
|
49572
|
+
const memberIds = completed.map((a) => a.id);
|
|
49573
|
+
let perMember;
|
|
49574
|
+
try {
|
|
49575
|
+
// The setup (snapshot, handler lookup, op construction) is
|
|
49576
|
+
// inside the try too (4065666785): it runs in the success
|
|
49577
|
+
// callback, so a throw here would otherwise reject this task,
|
|
49578
|
+
// bypass finalize, and strand every member marked Uploading for
|
|
49579
|
+
// the session. Every exit below either removes the members or
|
|
49580
|
+
// quarantines them, so this method still never rejects.
|
|
49581
|
+
const queue = await this.getQueueActions();
|
|
49582
|
+
perMember = [];
|
|
49583
|
+
const allOps = [];
|
|
49584
|
+
for (const action of completed) {
|
|
49585
|
+
const handler = this.getHandler(action.handler);
|
|
49586
|
+
const ops = handler.getQueueOperationsForCompletingDrafts(queue, action);
|
|
49587
|
+
perMember.push({ action, ops, handler });
|
|
49588
|
+
for (let i = 0, len = ops.length; i < len; i++) {
|
|
49589
|
+
allOps.push(ops[i]);
|
|
49590
|
+
}
|
|
49591
|
+
}
|
|
49592
|
+
// ONE transaction removes every committed member — no
|
|
49593
|
+
// partial-removal window (B2).
|
|
49594
|
+
await this.draftStore.completeAction(allOps);
|
|
49595
|
+
await this.clearBatchJournalIfSupported(batchId);
|
|
49596
|
+
}
|
|
49597
|
+
catch (e) {
|
|
49598
|
+
// The server committed the all-or-none request but the local
|
|
49599
|
+
// completion could not be applied (snapshot read, op
|
|
49600
|
+
// construction, or the durable write failed). Do NOT assume the
|
|
49601
|
+
// members were evicted from memory: completeAction is part of the
|
|
49602
|
+
// public DraftStore contract and an implementation may reject
|
|
49603
|
+
// without removing its Pending rows (4065665227). If we simply
|
|
49604
|
+
// returned, the trailing finalize would release the uploading
|
|
49605
|
+
// claims and re-arm dispatch, letting collectBatch re-batch a
|
|
49606
|
+
// keyless, maybe-committed member and replay it. Fail CLOSED:
|
|
49607
|
+
// quarantine every still-Pending member as commit-ambiguous
|
|
49608
|
+
// BEFORE finalize releases the claims. treatMissingAsResolved is
|
|
49609
|
+
// false because the base store evicts a member from memory before
|
|
49610
|
+
// its durable row settles, so an absent member's durable state is
|
|
49611
|
+
// unknown here — leave the journal so the next startQueue reconcile
|
|
49612
|
+
// (which reads freshly-synced durable state) is the authority
|
|
49613
|
+
// (4065666287). Only clear the journal once every member is durably
|
|
49614
|
+
// resolved.
|
|
49615
|
+
batchLog$1(`atomic completion failed batchId=${batchId} err=${errName(e)}`);
|
|
49616
|
+
let resolved = false;
|
|
49617
|
+
try {
|
|
49618
|
+
resolved = await this.errorAmbiguousActions(memberIds, {
|
|
49619
|
+
treatMissingAsResolved: false,
|
|
49010
49620
|
});
|
|
49011
49621
|
}
|
|
49622
|
+
catch (qe) {
|
|
49623
|
+
batchLog$1(`atomic completion quarantine failed batchId=${batchId} err=${errName(qe)}`);
|
|
49624
|
+
}
|
|
49625
|
+
if (resolved) {
|
|
49626
|
+
await this.clearBatchJournalIfSupported(batchId);
|
|
49627
|
+
}
|
|
49628
|
+
else {
|
|
49629
|
+
// Not resolved: errorAmbiguousActions either failed its
|
|
49630
|
+
// durable write (it already stopped the queue + quarantined)
|
|
49631
|
+
// or threw before quarantining (e.g. its snapshot read
|
|
49632
|
+
// failed). Handle both uniformly — force the queue
|
|
49633
|
+
// non-dispatchable and quarantine every member in-memory so
|
|
49634
|
+
// the trailing finalize's unconditional claim release cannot
|
|
49635
|
+
// let collectBatch re-batch a keyless, maybe-committed member
|
|
49636
|
+
// and replay it (Option A). The journal is deliberately NOT
|
|
49637
|
+
// cleared; the next process-restart reconcile is the backstop.
|
|
49638
|
+
this.state = DraftQueueState.Error;
|
|
49639
|
+
for (const id of memberIds) {
|
|
49640
|
+
this.commitAmbiguousUnresolvedIds.add(id);
|
|
49641
|
+
}
|
|
49642
|
+
// The server committed but the members could not be durably
|
|
49643
|
+
// quarantined this session — surface a bounded o11y signal.
|
|
49644
|
+
await this.signalBatchRecoveryFailed('quarantine-failed');
|
|
49645
|
+
}
|
|
49646
|
+
return;
|
|
49012
49647
|
}
|
|
49013
|
-
|
|
49014
|
-
|
|
49015
|
-
|
|
49016
|
-
|
|
49017
|
-
|
|
49018
|
-
|
|
49019
|
-
|
|
49020
|
-
|
|
49021
|
-
|
|
49022
|
-
|
|
49023
|
-
|
|
49024
|
-
|
|
49025
|
-
|
|
49648
|
+
// Durable state is now correct. Per-member in-memory
|
|
49649
|
+
// finalization (cache ingest, retry-state clear, listener
|
|
49650
|
+
// notify) is best-effort; isolate each so one failure can't skip
|
|
49651
|
+
// the others or reject this task (which would surface as an
|
|
49652
|
+
// unhandled dispatch rejection).
|
|
49653
|
+
let finalizeFailed = false;
|
|
49654
|
+
for (const { action, ops, handler } of perMember) {
|
|
49655
|
+
try {
|
|
49656
|
+
await this.finalizeCompletedAction(action, handler, ops);
|
|
49657
|
+
}
|
|
49658
|
+
catch (e) {
|
|
49659
|
+
// Still release the in-flight claim so the id isn't stuck
|
|
49660
|
+
// marked uploading.
|
|
49661
|
+
this.uploadingActionIds.delete(action.id);
|
|
49662
|
+
finalizeFailed = true;
|
|
49663
|
+
batchLog$1(`post-completion finalize failed id=${action.id} err=${errName(e)}`);
|
|
49664
|
+
}
|
|
49665
|
+
}
|
|
49666
|
+
if (finalizeFailed) {
|
|
49667
|
+
// The write committed and was durably removed exactly once — no
|
|
49668
|
+
// replay/duplication risk — but some member's local cache/notify
|
|
49669
|
+
// work was lost with no retry path. Emit one bounded structured
|
|
49670
|
+
// signal per batch (o11y counter) so this is monitorable beyond
|
|
49671
|
+
// the per-member device log (4077120522).
|
|
49672
|
+
await this.signalBatchRecoveryFailed('finalize-failed');
|
|
49673
|
+
}
|
|
49674
|
+
},
|
|
49675
|
+
});
|
|
49676
|
+
}
|
|
49677
|
+
/**
|
|
49678
|
+
* Surfaces the given still-Pending actions as commit-ambiguous errors (Option
|
|
49679
|
+
* A). A batch that MAY have committed cannot be safely re-sent because the batch
|
|
49680
|
+
* envelope drops the per-action idempotency keys, so instead of auto-replaying
|
|
49681
|
+
* (batch OR serial — both would present keys the server never saw on the
|
|
49682
|
+
* original batch) we move each member to Error for manual resolution. Used by
|
|
49683
|
+
* finalize on a commit-ambiguous dispatch outcome and by startQueue reconcile
|
|
49684
|
+
* for members a prior batch journaled but never completed (in-flight at a
|
|
49685
|
+
* crash). Reads the raw store status (not the uploading overlay); a member
|
|
49686
|
+
* already Error is left as-is (counts as resolved).
|
|
49687
|
+
*
|
|
49688
|
+
* All still-Pending members are moved to Error in ONE completeAction
|
|
49689
|
+
* transaction rather than one writeAction (and one getQueueActions snapshot)
|
|
49690
|
+
* per member (4065667688): a 25-member batch is a single durable
|
|
49691
|
+
* batchOperations, not 25 writes plus 25 snapshots. The in-memory effects
|
|
49692
|
+
* handleServerError applies per member (drop the uploading claim + retry
|
|
49693
|
+
* state, set queue Error state, notify listeners) are mirrored here once for
|
|
49694
|
+
* the whole set.
|
|
49695
|
+
*
|
|
49696
|
+
* A fresh draft snapshot (id → action) is read per call — never shared across
|
|
49697
|
+
* the reconcile loop — because each call awaits completeAction and listener
|
|
49698
|
+
* callbacks that can delete a member, and a stale shared snapshot would let a
|
|
49699
|
+
* later call resurrect a just-deleted member as an Error draft (4077120311).
|
|
49700
|
+
* `options.treatMissingAsResolved` controls how an id absent from the snapshot
|
|
49701
|
+
* is scored: true (default) when the snapshot authoritatively reflects durable
|
|
49702
|
+
* state (restart reconcile runs against a freshly-synced store, so absent means
|
|
49703
|
+
* genuinely gone); false in the live post-commit failure path, where the base
|
|
49704
|
+
* store evicts a member from memory BEFORE its durable row settles (4065666287),
|
|
49705
|
+
* so an absent member's durable state is unknown and must be treated as
|
|
49706
|
+
* unresolved to keep the journal. An id already in commitAmbiguousUnresolvedIds
|
|
49707
|
+
* is likewise treated as unresolved regardless of its (possibly stale) in-memory
|
|
49708
|
+
* status: its durable Error write failed earlier this session, so its durable
|
|
49709
|
+
* row is still Pending and the journal must be retained for the next
|
|
49710
|
+
* fresh-process reconcile (4077120459). Returns true only when every targeted id
|
|
49711
|
+
* is resolved.
|
|
49712
|
+
*/
|
|
49713
|
+
async errorAmbiguousActions(ids, options) {
|
|
49714
|
+
if (ids.length === 0) {
|
|
49715
|
+
return true;
|
|
49716
|
+
}
|
|
49717
|
+
const treatMissingAsResolved = options?.treatMissingAsResolved ?? true;
|
|
49718
|
+
// Read a fresh snapshot and index it by id so each targeted id is an O(1)
|
|
49719
|
+
// lookup instead of a full scan. Deliberately NOT shared across the
|
|
49720
|
+
// reconcile loop: a snapshot held across this call's awaited completeAction
|
|
49721
|
+
// and listener callbacks could go stale and let a later call resurrect a
|
|
49722
|
+
// just-deleted member as an Error draft (4077120311).
|
|
49723
|
+
const draftsById = new Map((await this.draftStore.getAllDrafts()).map((d) => [d.id, d]));
|
|
49724
|
+
const ops = [];
|
|
49725
|
+
const erroredActions = [];
|
|
49726
|
+
const seen = new Set();
|
|
49727
|
+
// An id whose durable Error write already failed this session (still in the
|
|
49728
|
+
// quarantine set) is NOT resolved even if its in-memory status now reads
|
|
49729
|
+
// Error: that in-memory Error is the write-through cache mutation that was
|
|
49730
|
+
// never rolled back after its durable batchOperations rejected, so the
|
|
49731
|
+
// durable row is still Pending. Retain the journal (return false) so the
|
|
49732
|
+
// next fresh-process reconcile — running against a re-synced store — is the
|
|
49733
|
+
// authority, rather than clearing the journal on unpersisted state and
|
|
49734
|
+
// re-batching the still-Pending row keyless after a later restart
|
|
49735
|
+
// (4077120459).
|
|
49736
|
+
let quarantinedUnresolved = false;
|
|
49737
|
+
for (const id of ids) {
|
|
49738
|
+
if (seen.has(id)) {
|
|
49739
|
+
continue;
|
|
49740
|
+
}
|
|
49741
|
+
if (this.commitAmbiguousUnresolvedIds.has(id)) {
|
|
49742
|
+
quarantinedUnresolved = true;
|
|
49743
|
+
continue;
|
|
49744
|
+
}
|
|
49745
|
+
const action = draftsById.get(id);
|
|
49746
|
+
if (action === undefined) {
|
|
49747
|
+
continue;
|
|
49748
|
+
}
|
|
49749
|
+
seen.add(id);
|
|
49750
|
+
if (action.status !== DraftActionStatus.Pending) {
|
|
49751
|
+
continue;
|
|
49752
|
+
}
|
|
49753
|
+
const errored = {
|
|
49754
|
+
...action,
|
|
49755
|
+
status: DraftActionStatus.Error,
|
|
49756
|
+
error: PerformanceDraftQueue.COMMIT_AMBIGUOUS_ERROR,
|
|
49757
|
+
metadata: action.metadata || {},
|
|
49758
|
+
};
|
|
49759
|
+
ops.push({ type: QueueOperationType.Update, id: action.id, action: errored });
|
|
49760
|
+
erroredActions.push(errored);
|
|
49761
|
+
}
|
|
49762
|
+
if (ops.length > 0) {
|
|
49763
|
+
try {
|
|
49764
|
+
// One durable transaction moves every still-Pending member to Error.
|
|
49765
|
+
await this.draftStore.completeAction(ops);
|
|
49766
|
+
}
|
|
49767
|
+
catch (e) {
|
|
49768
|
+
// Nothing was durably moved: the members are still Pending and MAY
|
|
49769
|
+
// have committed. Fail CLOSED — stop the queue and quarantine them
|
|
49770
|
+
// in-memory so no later dispatch (a re-arm, retryAllErrors, or a
|
|
49771
|
+
// reconnect-driven startQueue) re-batches them keyless and replays a
|
|
49772
|
+
// committed write (Option A). Report unresolved so the caller retains
|
|
49773
|
+
// the journal for the next reconcile.
|
|
49774
|
+
batchLog$1(`commit-ambiguous bulk error failed count=${ops.length} err=${errName(e)}`);
|
|
49775
|
+
this.state = DraftQueueState.Error;
|
|
49776
|
+
for (const errored of erroredActions) {
|
|
49777
|
+
this.commitAmbiguousUnresolvedIds.add(errored.id);
|
|
49778
|
+
}
|
|
49779
|
+
return false;
|
|
49780
|
+
}
|
|
49781
|
+
// Durable Error state is written; mirror handleServerError's in-memory
|
|
49782
|
+
// effects once for the whole set rather than per member. Batched members
|
|
49783
|
+
// are UPDATE/DELETE (never POST — collectBatch excludes POSTs), so no
|
|
49784
|
+
// peer deferred on a draft id these would have created; the peer-wake
|
|
49785
|
+
// that actionFailed does for permanently-failed POSTs is not needed.
|
|
49786
|
+
this.state = DraftQueueState.Error;
|
|
49787
|
+
for (const errored of erroredActions) {
|
|
49788
|
+
this.uploadingActionIds.delete(errored.id);
|
|
49789
|
+
// Durably Error now (already excluded from dispatch by status), so
|
|
49790
|
+
// drop any prior in-memory quarantine — otherwise a later
|
|
49791
|
+
// retryAllErrors that re-pends this action would find it wrongly
|
|
49792
|
+
// blocked from ever dispatching again.
|
|
49793
|
+
this.commitAmbiguousUnresolvedIds.delete(errored.id);
|
|
49794
|
+
this.clearActionRetry(errored.id);
|
|
49795
|
+
try {
|
|
49796
|
+
await this.notifyChangedListeners({
|
|
49797
|
+
type: DraftQueueEventType.ActionFailed,
|
|
49798
|
+
action: errored,
|
|
49799
|
+
draftCount: this.draftStore.getCount(),
|
|
49026
49800
|
});
|
|
49027
49801
|
}
|
|
49802
|
+
catch (e) {
|
|
49803
|
+
batchLog$1(`commit-ambiguous notify failed id=${errored.id} err=${errName(e)}`);
|
|
49804
|
+
}
|
|
49028
49805
|
}
|
|
49029
|
-
|
|
49030
|
-
|
|
49031
|
-
|
|
49032
|
-
|
|
49033
|
-
|
|
49034
|
-
|
|
49806
|
+
}
|
|
49807
|
+
if (quarantinedUnresolved) {
|
|
49808
|
+
return false;
|
|
49809
|
+
}
|
|
49810
|
+
if (treatMissingAsResolved) {
|
|
49811
|
+
return true;
|
|
49812
|
+
}
|
|
49813
|
+
for (let i = 0, len = ids.length; i < len; i++) {
|
|
49814
|
+
if (!seen.has(ids[i])) {
|
|
49815
|
+
return false;
|
|
49816
|
+
}
|
|
49817
|
+
}
|
|
49818
|
+
return true;
|
|
49035
49819
|
}
|
|
49036
49820
|
/**
|
|
49037
|
-
*
|
|
49038
|
-
*
|
|
49039
|
-
*
|
|
49040
|
-
*
|
|
49041
|
-
*
|
|
49821
|
+
* Feature-detecting wrapper: journals a batch's members if the store supports
|
|
49822
|
+
* it. A store without journal support (never the gate-ON path) resolves as a
|
|
49823
|
+
* no-op. When supported, the write REJECTION propagates so handleBatch can fail
|
|
49824
|
+
* CLOSED (4048061181) — the journal is the only record that these members were
|
|
49825
|
+
* in-flight, so a failed write must abort the batch dispatch rather than send
|
|
49826
|
+
* unrecorded.
|
|
49042
49827
|
*/
|
|
49043
|
-
|
|
49044
|
-
const
|
|
49045
|
-
|
|
49046
|
-
|
|
49047
|
-
return action();
|
|
49828
|
+
writeBatchJournalIfSupported(batchId, memberIds) {
|
|
49829
|
+
const store = this.draftStore;
|
|
49830
|
+
if (typeof store.writeBatchJournal !== 'function') {
|
|
49831
|
+
return Promise.resolve();
|
|
49048
49832
|
}
|
|
49049
|
-
|
|
49050
|
-
|
|
49051
|
-
|
|
49052
|
-
|
|
49053
|
-
|
|
49054
|
-
|
|
49055
|
-
|
|
49833
|
+
return store.writeBatchJournal(batchId, memberIds);
|
|
49834
|
+
}
|
|
49835
|
+
/**
|
|
49836
|
+
* Surfaces a batch-journal crash-recovery failure (B2) to listeners as a
|
|
49837
|
+
* bounded structured signal, so instrumentDraftQueue can map it to o11y —
|
|
49838
|
+
* these paths are otherwise visible only in device logs. Carries ONLY the
|
|
49839
|
+
* reason code and the queue depth (never ids or messages — device-log PII
|
|
49840
|
+
* policy). Best-effort and never rejects: a listener throw must not break the
|
|
49841
|
+
* recovery path, so it is swallowed to the batch log.
|
|
49842
|
+
*/
|
|
49843
|
+
async signalBatchRecoveryFailed(reason) {
|
|
49844
|
+
try {
|
|
49845
|
+
await this.notifyChangedListeners({
|
|
49846
|
+
type: DraftQueueEventType.BatchRecoveryFailed,
|
|
49847
|
+
reason,
|
|
49848
|
+
draftCount: this.draftStore.getCount(),
|
|
49056
49849
|
});
|
|
49850
|
+
}
|
|
49851
|
+
catch (e) {
|
|
49852
|
+
batchLog$1(`batch recovery signal failed reason=${reason} err=${errName(e)}`);
|
|
49853
|
+
}
|
|
49854
|
+
}
|
|
49855
|
+
/**
|
|
49856
|
+
* Feature-detecting wrapper: clears a batch journal entry if the store
|
|
49857
|
+
* supports it. A resolved batch's journal is stale, but a swallowed clear
|
|
49858
|
+
* failure would leave a recovery row behind on EVERY dispatch, so restart
|
|
49859
|
+
* reconcile grows unbounded (4065667688). Retry the clear a bounded number of
|
|
49860
|
+
* times, and if it still fails escalate the log line (leftover=true) so a
|
|
49861
|
+
* persistently-failing clear is greppable rather than silent. Never rejects:
|
|
49862
|
+
* callers (finalize, completeBatchAtomically) treat the clear as best-effort
|
|
49863
|
+
* cleanup of an already-resolved journal — a leftover row is caught by the
|
|
49864
|
+
* next reconcile, it does not risk a replay.
|
|
49865
|
+
*/
|
|
49866
|
+
async clearBatchJournalIfSupported(batchId) {
|
|
49867
|
+
const store = this.draftStore;
|
|
49868
|
+
if (typeof store.clearBatchJournal !== 'function') {
|
|
49869
|
+
return;
|
|
49870
|
+
}
|
|
49871
|
+
for (let attempt = 1; attempt <= CLEAR_JOURNAL_MAX_ATTEMPTS; attempt++) {
|
|
49872
|
+
try {
|
|
49873
|
+
await store.clearBatchJournal(batchId);
|
|
49874
|
+
return;
|
|
49875
|
+
}
|
|
49876
|
+
catch (e) {
|
|
49877
|
+
const leftover = attempt === CLEAR_JOURNAL_MAX_ATTEMPTS;
|
|
49878
|
+
batchLog$1(`journal clear failed batchId=${batchId} attempt=${attempt} leftover=${leftover} err=${errName(e)}`);
|
|
49879
|
+
}
|
|
49880
|
+
}
|
|
49881
|
+
// Every attempt failed (the loop returns on success): the resolved journal
|
|
49882
|
+
// row leaks, so the next reconcile keeps re-reading it (unbounded growth).
|
|
49883
|
+
// Surface a bounded o11y signal — the device log alone is not enough.
|
|
49884
|
+
// Still never rejects: signalBatchRecoveryFailed swallows its own errors.
|
|
49885
|
+
await this.signalBatchRecoveryFailed('clear-exhausted');
|
|
49886
|
+
}
|
|
49887
|
+
/**
|
|
49888
|
+
* A queued action is eligible to be selected or batched only when it is
|
|
49889
|
+
* Pending in the store and this queue is neither already uploading it nor
|
|
49890
|
+
* holding it in retry backoff. A retrying action stays Pending in the
|
|
49891
|
+
* store, so without the retryingActionIds guard collectBatch would pull a
|
|
49892
|
+
* parked action into a batch before its backoff timer fires (C5).
|
|
49893
|
+
*/
|
|
49894
|
+
isDispatchable(action) {
|
|
49895
|
+
return (action.status === DraftActionStatus.Pending &&
|
|
49896
|
+
!this.uploadingActionIds.has(action.id) &&
|
|
49897
|
+
!this.retryingActionIds.has(action.id) &&
|
|
49898
|
+
// A member left Pending because its commit-ambiguous quarantine could
|
|
49899
|
+
// not be durably written must never be re-batched keyless (Option A).
|
|
49900
|
+
!this.commitAmbiguousUnresolvedIds.has(action.id));
|
|
49901
|
+
}
|
|
49902
|
+
/**
|
|
49903
|
+
* Walks the queue from `headIndex` and returns the longest contiguous prefix
|
|
49904
|
+
* of actions that are eligible to batch together. Eligibility:
|
|
49905
|
+
* - same handler id as the head action
|
|
49906
|
+
* - dispatchable (Pending, not in-flight, not parked in retry backoff)
|
|
49907
|
+
* - not a POST/CREATE (POSTs are never batched — see BatchCutReason.PostBoundary)
|
|
49908
|
+
* - tag does not collide with any tag already in the batch
|
|
49909
|
+
* (UI-API Batch rejects two operations on the same record per request)
|
|
49910
|
+
* - body/basePath does not reference an unresolved draft id from a peer
|
|
49911
|
+
* queue's (or this queue's) pending POST (would dispatch with a draft id)
|
|
49912
|
+
* - targetId is not already being written by an in-flight request
|
|
49913
|
+
* (per-record serialization; the head is pre-checked by the caller,
|
|
49914
|
+
* but later members must be checked here too — see BatchCutReason.TargetInFlight)
|
|
49915
|
+
* - batch size capped at MAX_BATCH_SIZE
|
|
49916
|
+
*/
|
|
49917
|
+
collectBatch(queue, headIndex, peerPendingPostDraftIds, inFlightTargetIds) {
|
|
49918
|
+
if (queue.length === 0 || headIndex >= queue.length) {
|
|
49919
|
+
return [];
|
|
49920
|
+
}
|
|
49921
|
+
const head = queue[headIndex];
|
|
49922
|
+
const handler = this.handlers[head.handler];
|
|
49923
|
+
// CREATEs are never batched (C1): a POST head falls back to single-action
|
|
49924
|
+
// dispatch, which preserves its per-action idempotency key.
|
|
49925
|
+
const headData = head.data;
|
|
49926
|
+
if (headData !== undefined && headData.method === 'post') {
|
|
49927
|
+
batchLog$1('head is POST — not batchable, fall back to single-action');
|
|
49928
|
+
return [head];
|
|
49929
|
+
}
|
|
49930
|
+
// The head carries semantics the batch envelope cannot represent (B1):
|
|
49931
|
+
// fall back to single-action dispatch so those semantics (e.g. a
|
|
49932
|
+
// conditional-update header) are preserved.
|
|
49933
|
+
if (handler !== undefined &&
|
|
49934
|
+
typeof handler.isActionBatchable === 'function' &&
|
|
49935
|
+
!handler.isActionBatchable(head)) {
|
|
49936
|
+
batchLog$1('head not batchable — fall back to single-action');
|
|
49937
|
+
return [head];
|
|
49938
|
+
}
|
|
49939
|
+
const batch = [head];
|
|
49940
|
+
const seenTags = new Set();
|
|
49941
|
+
seenTags.add(head.tag);
|
|
49942
|
+
let reason = "END_OF_QUEUE" /* BatchCutReason.EndOfQueue */;
|
|
49943
|
+
let detail = '';
|
|
49944
|
+
for (let i = headIndex + 1; i < queue.length; i++) {
|
|
49945
|
+
if (batch.length >= MAX_BATCH_SIZE$1) {
|
|
49946
|
+
reason = "MAX_BATCH_SIZE" /* BatchCutReason.MaxBatchSize */;
|
|
49947
|
+
break;
|
|
49948
|
+
}
|
|
49949
|
+
const candidate = queue[i];
|
|
49950
|
+
if (candidate.handler !== head.handler) {
|
|
49951
|
+
reason = "HANDLER_CHANGE" /* BatchCutReason.HandlerChange */;
|
|
49952
|
+
detail = `nextHandler=${candidate.handler}`;
|
|
49953
|
+
break;
|
|
49954
|
+
}
|
|
49955
|
+
if (!this.isDispatchable(candidate)) {
|
|
49956
|
+
reason = "NOT_PENDING" /* BatchCutReason.NotPending */;
|
|
49957
|
+
detail = `nextStatus=${candidate.status}`;
|
|
49958
|
+
break;
|
|
49959
|
+
}
|
|
49960
|
+
// Stop at any POST: CREATEs are excluded from batching (C1). Only
|
|
49961
|
+
// naturally-idempotent UPDATE/DELETE actions batch, so there are no
|
|
49962
|
+
// in-batch POST draft ids to track — the blocking set below is the
|
|
49963
|
+
// caller's peer/owned pending-POST set alone.
|
|
49964
|
+
const candData = candidate.data;
|
|
49965
|
+
if (candData !== undefined && candData.method === 'post') {
|
|
49966
|
+
reason = "POST_BOUNDARY" /* BatchCutReason.PostBoundary */;
|
|
49967
|
+
break;
|
|
49968
|
+
}
|
|
49969
|
+
// Candidate carries semantics the batch envelope cannot represent
|
|
49970
|
+
// (B1): end the batch here so it is dispatched via single-action
|
|
49971
|
+
// handleAction on a later pass rather than folded in and stripped.
|
|
49972
|
+
if (handler !== undefined &&
|
|
49973
|
+
typeof handler.isActionBatchable === 'function' &&
|
|
49974
|
+
!handler.isActionBatchable(candidate)) {
|
|
49975
|
+
reason = "NOT_BATCHABLE" /* BatchCutReason.NotBatchable */;
|
|
49976
|
+
break;
|
|
49977
|
+
}
|
|
49978
|
+
// Per-record serialization (see BatchCutReason.TargetInFlight): the
|
|
49979
|
+
// caller pre-checks the head; later members must be checked here too.
|
|
49980
|
+
if (candidate.targetId && inFlightTargetIds.has(candidate.targetId)) {
|
|
49981
|
+
reason = "TARGET_IN_FLIGHT" /* BatchCutReason.TargetInFlight */;
|
|
49982
|
+
// Log the cut position, NOT the targetId: a targetId is a record id
|
|
49983
|
+
// and even a 24-char suffix preserves the full 18-char id, which the
|
|
49984
|
+
// device-log PII policy (metrics.ts) forbids (4065667273). The
|
|
49985
|
+
// BatchCutReason already says why; the offset says where.
|
|
49986
|
+
detail = `atOffset=${i - headIndex}`;
|
|
49987
|
+
break;
|
|
49988
|
+
}
|
|
49989
|
+
if (seenTags.has(candidate.tag)) {
|
|
49990
|
+
reason = "TAG_COLLISION" /* BatchCutReason.TagCollision */;
|
|
49991
|
+
// Log the cut position, NOT the tag: the tag embeds the record id
|
|
49992
|
+
// (same PII concern as targetId above).
|
|
49993
|
+
detail = `atOffset=${i - headIndex}`;
|
|
49994
|
+
break;
|
|
49995
|
+
}
|
|
49996
|
+
// Block a candidate that references a draft id still unresolved on a
|
|
49997
|
+
// peer queue's (or this queue's) in-flight POST, minus the
|
|
49998
|
+
// candidate's own draft ids (an action mustn't self-block on its own
|
|
49999
|
+
// targetId).
|
|
50000
|
+
const candidateOwnIds = new Set();
|
|
50001
|
+
this.collectDraftIds(candidate, candidateOwnIds);
|
|
50002
|
+
const blockingIds = new Set(peerPendingPostDraftIds);
|
|
50003
|
+
for (const id of candidateOwnIds) {
|
|
50004
|
+
blockingIds.delete(id);
|
|
50005
|
+
}
|
|
50006
|
+
if (this.actionReferencesAny(candidate, blockingIds)) {
|
|
50007
|
+
reason = "REF_BLOCKED" /* BatchCutReason.RefBlocked */;
|
|
50008
|
+
break;
|
|
50009
|
+
}
|
|
50010
|
+
batch.push(candidate);
|
|
50011
|
+
seenTags.add(candidate.tag);
|
|
50012
|
+
}
|
|
50013
|
+
const queuedAfterBatch = queue.length - headIndex - batch.length;
|
|
50014
|
+
batchLog$1(`collected size=${batch.length} reason=${reason}` +
|
|
50015
|
+
` queueRemaining=${queuedAfterBatch}` +
|
|
50016
|
+
(detail.length > 0 ? ` ${detail}` : ''));
|
|
50017
|
+
return batch;
|
|
50018
|
+
}
|
|
50019
|
+
/**
|
|
50020
|
+
* Returns all draft ids "created" by this action (i.e., ids that don't yet
|
|
50021
|
+
* exist on the server). Defaults to `[targetId]`; handlers may override
|
|
50022
|
+
* `getDraftIdsFromAction` to return additional ids (e.g., the CD composite
|
|
50023
|
+
* handler returns CD + CV + CDL).
|
|
50024
|
+
*/
|
|
50025
|
+
collectDraftIds(action, into) {
|
|
50026
|
+
const handler = this.handlers[action.handler];
|
|
50027
|
+
if (handler !== undefined && typeof handler.getDraftIdsFromAction === 'function') {
|
|
50028
|
+
for (const id of handler.getDraftIdsFromAction(action)) {
|
|
50029
|
+
if (id !== undefined && id !== null && id !== '') {
|
|
50030
|
+
into.add(id);
|
|
50031
|
+
}
|
|
50032
|
+
}
|
|
50033
|
+
return;
|
|
50034
|
+
}
|
|
50035
|
+
if (action.targetId) {
|
|
50036
|
+
into.add(action.targetId);
|
|
50037
|
+
}
|
|
50038
|
+
}
|
|
50039
|
+
/**
|
|
50040
|
+
* Returns true if any of the given draft ids appears in the action's
|
|
50041
|
+
* basePath or stringified body. Mirrors the substring scan used by
|
|
50042
|
+
* `getQueueOperationsForCompletingDrafts` so blocking and rewriting agree.
|
|
50043
|
+
*
|
|
50044
|
+
* TODO [W-23515682]: this re-scans basePath/body on every dispatch pass.
|
|
50045
|
+
* The referenced-id set could instead be computed once and cached on the
|
|
50046
|
+
* DraftAction, recomputed only when rewriteQueueActionsForRedirects mutates
|
|
50047
|
+
* basePath/body (the only place they change), so the hot path is a set
|
|
50048
|
+
* lookup rather than a stringify+substring scan.
|
|
50049
|
+
*/
|
|
50050
|
+
actionReferencesAny(action, draftIds) {
|
|
50051
|
+
if (draftIds.size === 0) {
|
|
50052
|
+
return false;
|
|
50053
|
+
}
|
|
50054
|
+
const data = action.data;
|
|
50055
|
+
if (data === undefined) {
|
|
50056
|
+
return false;
|
|
50057
|
+
}
|
|
50058
|
+
const body = typeof data.body === 'string' ? data.body : JSON.stringify(data.body || '');
|
|
50059
|
+
const haystack = `${data.basePath || ''}${body}`;
|
|
50060
|
+
return [...draftIds].some((id) => haystack.includes(id));
|
|
50061
|
+
}
|
|
50062
|
+
async getInFlightRewritableDraftIds() {
|
|
50063
|
+
const allDrafts = await this.draftStore.getAllDrafts();
|
|
50064
|
+
// Seed with draft ids retained across the completeAction →
|
|
50065
|
+
// propagateRedirectsToPeers gap so a peer keeps deferring even though the
|
|
50066
|
+
// completing POST has already left the store but its redirects have not
|
|
50067
|
+
// yet reached peers.
|
|
50068
|
+
const ids = new Set(this.redirectingDraftIds);
|
|
50069
|
+
for (const action of allDrafts) {
|
|
50070
|
+
if (this.handlers[action.handler] !== undefined &&
|
|
50071
|
+
(action.status === DraftActionStatus.Pending ||
|
|
50072
|
+
action.status === DraftActionStatus.Uploading) &&
|
|
50073
|
+
action.data &&
|
|
50074
|
+
action.data.method === 'post') {
|
|
50075
|
+
this.collectDraftIds(action, ids);
|
|
50076
|
+
}
|
|
50077
|
+
}
|
|
50078
|
+
return Array.from(ids);
|
|
50079
|
+
}
|
|
50080
|
+
resumeDeferredActions() {
|
|
50081
|
+
if (this.state === DraftQueueState.Started) {
|
|
50082
|
+
this.processNextAction();
|
|
50083
|
+
}
|
|
50084
|
+
}
|
|
50085
|
+
// Applies a set of draft → canonical id rewrites to every action queued
|
|
50086
|
+
// on this queue. Called by a peer queue after one of its POSTs completes
|
|
50087
|
+
// and produced redirects. Walks this queue, rewrites any action whose
|
|
50088
|
+
// basePath/body/tag references a draft id, persists via the shared
|
|
50089
|
+
// store, and kicks processNextAction so newly-resolvable actions dispatch.
|
|
50090
|
+
async applyDraftIdRedirects(redirects) {
|
|
50091
|
+
if (!redirects || redirects.length === 0) {
|
|
50092
|
+
return;
|
|
50093
|
+
}
|
|
50094
|
+
return this.workerPool.push({
|
|
50095
|
+
workFn: async () => {
|
|
50096
|
+
const queue = await this.getQueueActions();
|
|
50097
|
+
const queueOperations = rewriteQueueActionsForRedirects(queue, redirects);
|
|
50098
|
+
if (queueOperations.length > 0) {
|
|
50099
|
+
await this.draftStore.completeAction(queueOperations);
|
|
50100
|
+
}
|
|
50101
|
+
if (this.state === DraftQueueState.Started) {
|
|
50102
|
+
this.processNextAction();
|
|
50103
|
+
}
|
|
50104
|
+
},
|
|
49057
50105
|
});
|
|
49058
|
-
return deferred;
|
|
49059
50106
|
}
|
|
49060
50107
|
/**
|
|
49061
|
-
*
|
|
49062
|
-
*
|
|
49063
|
-
*
|
|
50108
|
+
* Retains the draft ids resolved by `redirects` in `redirectingDraftIds` so
|
|
50109
|
+
* getInFlightRewritableDraftIds keeps reporting them across the
|
|
50110
|
+
* completeAction → propagateRedirectsToPeers fire-and-forget gap. Returns
|
|
50111
|
+
* only the ids this call newly added (so a concurrent completion touching
|
|
50112
|
+
* the same id doesn't release it early). Called before completeAction on the
|
|
50113
|
+
* single-action completion path (actionCompleted).
|
|
49064
50114
|
*/
|
|
49065
|
-
|
|
49066
|
-
const
|
|
49067
|
-
|
|
49068
|
-
|
|
49069
|
-
.
|
|
49070
|
-
|
|
49071
|
-
|
|
49072
|
-
this.cleanSnapshot = {};
|
|
49073
|
-
return this.runQueuedOperations();
|
|
50115
|
+
retainRedirectingDraftIds(redirects) {
|
|
50116
|
+
const retainedDraftIds = [];
|
|
50117
|
+
if (redirects !== undefined) {
|
|
50118
|
+
for (const redirect of redirects) {
|
|
50119
|
+
if (redirect.draftId && !this.redirectingDraftIds.has(redirect.draftId)) {
|
|
50120
|
+
this.redirectingDraftIds.add(redirect.draftId);
|
|
50121
|
+
retainedDraftIds.push(redirect.draftId);
|
|
49074
50122
|
}
|
|
49075
|
-
|
|
49076
|
-
|
|
49077
|
-
|
|
49078
|
-
|
|
49079
|
-
|
|
49080
|
-
|
|
49081
|
-
|
|
50123
|
+
}
|
|
50124
|
+
}
|
|
50125
|
+
return retainedDraftIds;
|
|
50126
|
+
}
|
|
50127
|
+
/** Releases the ids retained by a prior retainRedirectingDraftIds call. */
|
|
50128
|
+
releaseRedirectingDraftIds(retainedDraftIds) {
|
|
50129
|
+
for (const id of retainedDraftIds) {
|
|
50130
|
+
this.redirectingDraftIds.delete(id);
|
|
50131
|
+
}
|
|
50132
|
+
}
|
|
50133
|
+
/**
|
|
50134
|
+
* After an action on this queue completes, propagates the draft → canonical
|
|
50135
|
+
* id rewrites it produced to the peer queues so their queued actions that
|
|
50136
|
+
* reference the resolved id are rewritten before dispatch. When there are no
|
|
50137
|
+
* redirects (or none apply), falls back to a bare wake of each peer so their
|
|
50138
|
+
* previously-deferred actions re-evaluate.
|
|
50139
|
+
*
|
|
50140
|
+
* `redirects` is precomputed by the caller (before completeAction resolves
|
|
50141
|
+
* the POST out of the store) so the redirecting draft ids can be retained
|
|
50142
|
+
* across the fire-and-forget gap. `onSettled` runs once every peer's rewrite
|
|
50143
|
+
* has settled, so the caller can release those retained ids.
|
|
50144
|
+
*/
|
|
50145
|
+
propagateRedirectsToPeers(redirects, onSettled) {
|
|
50146
|
+
// Peers are typed DraftQueue[]; only those that also implement
|
|
50147
|
+
// CrossQueueCoordination can participate. Narrow once up front so the
|
|
50148
|
+
// calls below need no per-method guards.
|
|
50149
|
+
const peers = this.getPeerQueues().filter(isCrossQueueCoordination);
|
|
50150
|
+
if (peers.length === 0) {
|
|
50151
|
+
onSettled();
|
|
50152
|
+
return;
|
|
50153
|
+
}
|
|
50154
|
+
if (redirects !== undefined && redirects.length > 0) {
|
|
50155
|
+
const settlements = peers.map((peer) => {
|
|
50156
|
+
// Fire-and-forget: peer.applyDraftIdRedirects schedules its own
|
|
50157
|
+
// work on the peer's worker pool. Don't await — that deadlocks
|
|
50158
|
+
// when the peer is propagating redirects back to this queue.
|
|
50159
|
+
const promise = peer.applyDraftIdRedirects(redirects);
|
|
50160
|
+
if (promise && typeof promise.catch === 'function') {
|
|
50161
|
+
return promise.catch((e) => {
|
|
50162
|
+
if (this.logger) {
|
|
50163
|
+
this.logger.logError(`PerformanceDraftQueue: cross-queue redirect propagation to peer failed: ${errName(e)}`);
|
|
50164
|
+
}
|
|
50165
|
+
});
|
|
49082
50166
|
}
|
|
49083
|
-
return
|
|
49084
|
-
})
|
|
49085
|
-
.finally(() => {
|
|
49086
|
-
this.syncPromise = undefined;
|
|
50167
|
+
return Promise.resolve();
|
|
49087
50168
|
});
|
|
49088
|
-
|
|
49089
|
-
|
|
49090
|
-
|
|
49091
|
-
|
|
49092
|
-
if (syncPromise === undefined) {
|
|
49093
|
-
return sync();
|
|
50169
|
+
// Release the retained draft ids only once every peer has applied
|
|
50170
|
+
// (or failed to apply) the rewrites, so getInFlightRewritableDraftIds
|
|
50171
|
+
// keeps reporting them until peers no longer reference the stale id.
|
|
50172
|
+
Promise.all(settlements).then(onSettled, onSettled);
|
|
49094
50173
|
}
|
|
49095
|
-
|
|
49096
|
-
|
|
50174
|
+
else {
|
|
50175
|
+
peers.forEach((peer) => peer.resumeDeferredActions());
|
|
50176
|
+
onSettled();
|
|
50177
|
+
}
|
|
50178
|
+
}
|
|
50179
|
+
async processNextAction() {
|
|
50180
|
+
// Serialize dispatch passes. A pass awaits peer state before
|
|
50181
|
+
// claiming a slot, so overlapping passes could double-dispatch. If a
|
|
50182
|
+
// pass is already running, record the wakeup and let it re-run.
|
|
50183
|
+
if (this.isDispatching) {
|
|
50184
|
+
this.redispatchRequested = true;
|
|
50185
|
+
return ProcessActionResult.ACTION_ALREADY_PROCESSING;
|
|
50186
|
+
}
|
|
50187
|
+
this.isDispatching = true;
|
|
50188
|
+
try {
|
|
50189
|
+
let result = ProcessActionResult.NO_ACTION_TO_PROCESS;
|
|
50190
|
+
do {
|
|
50191
|
+
this.redispatchRequested = false;
|
|
50192
|
+
result = await this.dispatchFreeSlots();
|
|
50193
|
+
} while (this.redispatchRequested);
|
|
50194
|
+
return result;
|
|
50195
|
+
}
|
|
50196
|
+
finally {
|
|
50197
|
+
this.isDispatching = false;
|
|
50198
|
+
}
|
|
50199
|
+
}
|
|
50200
|
+
async dispatchFreeSlots() {
|
|
50201
|
+
if (this.uploadingActionIds.size >= this.concurrency) {
|
|
50202
|
+
return ProcessActionResult.ACTION_ALREADY_PROCESSING;
|
|
50203
|
+
}
|
|
50204
|
+
// Gather the set of pending POST draft ids whose creators are still in
|
|
50205
|
+
// flight on any peer queue. A candidate action that references one of
|
|
50206
|
+
// those ids would otherwise dispatch with an unresolved draft id
|
|
50207
|
+
// (ContentDocument 069... etc.) instead of waiting for the upstream
|
|
50208
|
+
// POST to rewrite it to canonical. These peer reads each await, so they
|
|
50209
|
+
// run BEFORE the owned-queue snapshot below — otherwise an await would
|
|
50210
|
+
// sit between snapshotting the queue and claiming from it, letting the
|
|
50211
|
+
// store change underneath the selection (the candidate we picked could
|
|
50212
|
+
// already be gone or completed by the time we claim it).
|
|
50213
|
+
const blockingDraftIds = new Set();
|
|
50214
|
+
const peers = this.getPeerQueues().filter(isCrossQueueCoordination);
|
|
50215
|
+
for (const peer of peers) {
|
|
50216
|
+
try {
|
|
50217
|
+
const ids = await peer.getInFlightRewritableDraftIds();
|
|
50218
|
+
for (const id of ids) {
|
|
50219
|
+
blockingDraftIds.add(id);
|
|
50220
|
+
}
|
|
50221
|
+
}
|
|
50222
|
+
catch (e) {
|
|
50223
|
+
// Peers are already narrowed to CrossQueueCoordination, so this
|
|
50224
|
+
// is a genuine failure to read a peer's in-flight ids, not a
|
|
50225
|
+
// missing capability. Log it: on failure we fall through with an
|
|
50226
|
+
// incomplete blocking set, which can let a dependent action
|
|
50227
|
+
// dispatch against an unresolved draft id.
|
|
50228
|
+
if (this.logger) {
|
|
50229
|
+
this.logger.logError(`PerformanceDraftQueue: failed to read peer in-flight rewritable draft ids: ${errName(e)}`);
|
|
50230
|
+
}
|
|
50231
|
+
}
|
|
50232
|
+
}
|
|
50233
|
+
// Snapshot the owned queue AFTER the peer reads. From here to the claims
|
|
50234
|
+
// below there is no await, so the snapshot and the slot claims see a
|
|
50235
|
+
// consistent view of the store.
|
|
50236
|
+
const queue = await this.getQueueActions();
|
|
50237
|
+
// The shared DurableDraftStore returns actions for every queue, so we
|
|
50238
|
+
// restrict candidate selection to actions whose handler is registered
|
|
50239
|
+
// on this queue. This keeps a queue from claiming or being
|
|
50240
|
+
// error-blocked by its peer's actions.
|
|
50241
|
+
const ownedQueue = queue.filter((a) => this.handlers[a.handler] !== undefined);
|
|
50242
|
+
// Head-of-queue error block: only applies when strictOrdering is enabled.
|
|
50243
|
+
// With strictOrdering=false the candidate loop will skip errored items.
|
|
50244
|
+
if (this.strictOrdering) {
|
|
50245
|
+
const head = ownedQueue[0];
|
|
50246
|
+
if (head !== undefined && head.status === DraftActionStatus.Error) {
|
|
50247
|
+
this.state = DraftQueueState.Error;
|
|
50248
|
+
await this.notifyChangedListeners({
|
|
50249
|
+
type: DraftQueueEventType.ActionFailed,
|
|
50250
|
+
action: head,
|
|
50251
|
+
draftCount: this.draftStore.getCount(),
|
|
50252
|
+
});
|
|
50253
|
+
return ProcessActionResult.BLOCKED_ON_ERROR;
|
|
50254
|
+
}
|
|
50255
|
+
}
|
|
50256
|
+
// Single pass over ownedQueue derives everything the selection loop
|
|
50257
|
+
// needs: candidates (actions neither already claimed in-flight nor
|
|
50258
|
+
// parked in retry backoff), the targetIds of in-flight actions
|
|
50259
|
+
// (per-record serialization), and every owned pending/uploading POST's
|
|
50260
|
+
// draft ids (this queue's contribution to the blocking set).
|
|
50261
|
+
const candidates = [];
|
|
50262
|
+
const inFlightTargetIds = new Set();
|
|
50263
|
+
for (const a of ownedQueue) {
|
|
50264
|
+
if (this.uploadingActionIds.has(a.id)) {
|
|
50265
|
+
if (a.targetId) {
|
|
50266
|
+
inFlightTargetIds.add(a.targetId);
|
|
50267
|
+
}
|
|
50268
|
+
}
|
|
50269
|
+
else if (!this.retryingActionIds.has(a.id) &&
|
|
50270
|
+
// Same guard as isDispatchable: a commit-ambiguous member whose
|
|
50271
|
+
// quarantine could not be durably written stays out of
|
|
50272
|
+
// single-action dispatch too, not just batching.
|
|
50273
|
+
!this.commitAmbiguousUnresolvedIds.has(a.id)) {
|
|
50274
|
+
candidates.push(a);
|
|
50275
|
+
}
|
|
50276
|
+
if ((a.status === DraftActionStatus.Pending ||
|
|
50277
|
+
a.status === DraftActionStatus.Uploading) &&
|
|
50278
|
+
a.data &&
|
|
50279
|
+
a.data.method === 'post') {
|
|
50280
|
+
this.collectDraftIds(a, blockingDraftIds);
|
|
50281
|
+
}
|
|
50282
|
+
}
|
|
50283
|
+
// Fill every free concurrency slot from this single snapshot rather than
|
|
50284
|
+
// claiming one and bouncing back through processNextAction — that would
|
|
50285
|
+
// re-run getQueueActions (and its deep clone) per slot. Claims are
|
|
50286
|
+
// synchronous (no await inside the loop), so inFlightTargetIds stays
|
|
50287
|
+
// authoritative for per-record serialization of later picks in the same
|
|
50288
|
+
// pass. Blocked candidates are skipped (strictOrdering=false) or halt
|
|
50289
|
+
// the scan (strictOrdering=true head-of-line semantics).
|
|
50290
|
+
const toDispatch = [];
|
|
50291
|
+
let blockedCount = 0;
|
|
50292
|
+
for (const candidate of candidates) {
|
|
50293
|
+
if (this.uploadingActionIds.size >= this.concurrency) {
|
|
50294
|
+
break;
|
|
50295
|
+
}
|
|
50296
|
+
if (candidate.status === DraftActionStatus.Error) {
|
|
50297
|
+
continue;
|
|
50298
|
+
}
|
|
50299
|
+
// Per-record serialization: skip if another action on the same record is in-flight.
|
|
50300
|
+
if (candidate.targetId && inFlightTargetIds.has(candidate.targetId)) {
|
|
50301
|
+
blockedCount++;
|
|
50302
|
+
if (this.strictOrdering) {
|
|
50303
|
+
break;
|
|
50304
|
+
}
|
|
50305
|
+
continue;
|
|
50306
|
+
}
|
|
50307
|
+
// Exclude the candidate's own draft IDs so a POST doesn't self-block
|
|
50308
|
+
// on its own targetId.
|
|
50309
|
+
const ownIds = new Set();
|
|
50310
|
+
this.collectDraftIds(candidate, ownIds);
|
|
50311
|
+
const effectiveBlockingIds = new Set(blockingDraftIds);
|
|
50312
|
+
for (const id of ownIds) {
|
|
50313
|
+
effectiveBlockingIds.delete(id);
|
|
50314
|
+
}
|
|
50315
|
+
if (this.actionReferencesAny(candidate, effectiveBlockingIds)) {
|
|
50316
|
+
blockedCount++;
|
|
50317
|
+
if (this.strictOrdering) {
|
|
50318
|
+
break;
|
|
50319
|
+
}
|
|
50320
|
+
continue;
|
|
50321
|
+
}
|
|
50322
|
+
// Batching path: only when enabled, on the first action dispatched
|
|
50323
|
+
// this pass, and only if its handler implements handleBatchActions
|
|
50324
|
+
// and the eligible batch is larger than 1. collectBatch enforces the
|
|
50325
|
+
// same ref-blocking rule applied above so a candidate referencing a
|
|
50326
|
+
// peer's pending POST id won't be batched alongside the head. When a
|
|
50327
|
+
// batch dispatches it consumes the pass (returns), matching the
|
|
50328
|
+
// pre-fill-all behavior. It is also passed inFlightTargetIds so a
|
|
50329
|
+
// batch member can't write a record that another in-flight request
|
|
50330
|
+
// is already writing (the head candidate is pre-checked above).
|
|
50331
|
+
if (this.queueBatching && toDispatch.length === 0) {
|
|
50332
|
+
const handler = this.getHandler(candidate.handler);
|
|
50333
|
+
if (handler.handleBatchActions !== undefined) {
|
|
50334
|
+
const actionIndex = ownedQueue.indexOf(candidate);
|
|
50335
|
+
const batch = this.collectBatch(ownedQueue, actionIndex, blockingDraftIds, inFlightTargetIds);
|
|
50336
|
+
if (batch.length > 1) {
|
|
50337
|
+
batchLog$1(`dispatching batch size=${batch.length}`);
|
|
50338
|
+
if (this.state === DraftQueueState.Waiting) {
|
|
50339
|
+
this.state = DraftQueueState.Started;
|
|
50340
|
+
}
|
|
50341
|
+
// Claim every batch member so a re-entrant
|
|
50342
|
+
// processNextAction (e.g. a peer kick during the await
|
|
50343
|
+
// below) doesn't re-claim any of them.
|
|
50344
|
+
for (const batched of batch) {
|
|
50345
|
+
this.uploadingActionIds.add(batched.id);
|
|
50346
|
+
}
|
|
50347
|
+
for (const batched of batch) {
|
|
50348
|
+
await this.notifyChangedListeners({
|
|
50349
|
+
type: DraftQueueEventType.ActionUploading,
|
|
50350
|
+
action: { ...batched, status: DraftActionStatus.Uploading },
|
|
50351
|
+
draftCount: this.draftStore.getCount(),
|
|
50352
|
+
});
|
|
50353
|
+
}
|
|
50354
|
+
return this.handleBatch(batch);
|
|
50355
|
+
}
|
|
50356
|
+
}
|
|
50357
|
+
}
|
|
50358
|
+
// Eligible — claim the slot now (synchronously) so the next
|
|
50359
|
+
// iteration serializes against it.
|
|
50360
|
+
this.uploadingActionIds.add(candidate.id);
|
|
50361
|
+
if (candidate.targetId) {
|
|
50362
|
+
inFlightTargetIds.add(candidate.targetId);
|
|
50363
|
+
}
|
|
50364
|
+
toDispatch.push(candidate);
|
|
50365
|
+
}
|
|
50366
|
+
if (toDispatch.length === 0) {
|
|
50367
|
+
if (blockedCount > 0 && this.logger) {
|
|
50368
|
+
this.logger.logInfo(`[DraftQueue c=${this.concurrency}] processNextAction: ${blockedCount} action(s) deferred waiting on upstream draft ids`);
|
|
50369
|
+
}
|
|
50370
|
+
if (this.uploadingActionIds.size === 0) {
|
|
50371
|
+
if (this.state === DraftQueueState.Waiting) {
|
|
50372
|
+
this.state = DraftQueueState.Started;
|
|
50373
|
+
}
|
|
50374
|
+
return ProcessActionResult.NO_ACTION_TO_PROCESS;
|
|
50375
|
+
}
|
|
50376
|
+
return ProcessActionResult.ACTION_ALREADY_PROCESSING;
|
|
50377
|
+
}
|
|
50378
|
+
if (this.state === DraftQueueState.Waiting) {
|
|
50379
|
+
this.state = DraftQueueState.Started;
|
|
50380
|
+
}
|
|
50381
|
+
for (const action of toDispatch) {
|
|
50382
|
+
await this.notifyChangedListeners({
|
|
50383
|
+
type: DraftQueueEventType.ActionUploading,
|
|
50384
|
+
action: { ...action, status: DraftActionStatus.Uploading },
|
|
50385
|
+
draftCount: this.draftStore.getCount(),
|
|
50386
|
+
});
|
|
50387
|
+
this.handle(action);
|
|
50388
|
+
}
|
|
50389
|
+
return ProcessActionResult.ACTION_ALREADY_PROCESSING;
|
|
50390
|
+
}
|
|
50391
|
+
// Overrides DurableDraftQueue.removeDraftAction rather than reusing it: the
|
|
50392
|
+
// uploading guard reads the concurrent `uploadingActionIds` set (the base
|
|
50393
|
+
// reads its private single `uploadingActionId`, which this path never sets,
|
|
50394
|
+
// so delegating would defeat the guard), and it additionally clears the
|
|
50395
|
+
// per-action retry timer — a concept that exists only on this subclass. The
|
|
50396
|
+
// rest of the body is kept identical to the base; keep them in sync.
|
|
50397
|
+
async removeDraftAction(actionId) {
|
|
50398
|
+
const queue = await this.getQueueActions();
|
|
50399
|
+
//Get the store key for the removed action
|
|
50400
|
+
const actions = queue.filter((action) => action.id === actionId);
|
|
50401
|
+
if (actions.length === 0) {
|
|
50402
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
50403
|
+
throw new Error(`No removable action with id ${actionId}`);
|
|
50404
|
+
}
|
|
50405
|
+
const action = actions[0];
|
|
50406
|
+
if (this.uploadingActionIds.has(action.id)) {
|
|
50407
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
50408
|
+
throw new Error(`Cannot remove an uploading draft action with ID ${actionId}`);
|
|
50409
|
+
}
|
|
50410
|
+
// A removed action must not leave a dangling backoff timer that would
|
|
50411
|
+
// later fire and re-drive the scheduler for an id that no longer exists.
|
|
50412
|
+
this.clearActionRetry(action.id);
|
|
50413
|
+
const handler = this.getHandler(action.handler);
|
|
50414
|
+
const shouldDeleteRelated = handler.shouldDeleteActionByTagOnRemoval(action);
|
|
50415
|
+
if (shouldDeleteRelated) {
|
|
50416
|
+
await this.draftStore.deleteByTag(action.tag);
|
|
50417
|
+
}
|
|
50418
|
+
else {
|
|
50419
|
+
await this.draftStore.deleteDraft(action.id);
|
|
50420
|
+
}
|
|
50421
|
+
await handler.handleActionRemoved(action, queue.filter((x) => x.id !== actionId));
|
|
50422
|
+
await this.notifyChangedListeners({
|
|
50423
|
+
type: DraftQueueEventType.ActionDeleted,
|
|
50424
|
+
action,
|
|
50425
|
+
draftCount: this.draftStore.getCount(),
|
|
49097
50426
|
});
|
|
50427
|
+
if (this.userState === DraftQueueState.Started &&
|
|
50428
|
+
this.state !== DraftQueueState.Started &&
|
|
50429
|
+
this.replacingAction === undefined) {
|
|
50430
|
+
await this.startQueue();
|
|
50431
|
+
}
|
|
49098
50432
|
}
|
|
49099
50433
|
/**
|
|
49100
|
-
*
|
|
49101
|
-
*
|
|
50434
|
+
* Concurrent-aware, per-action retry scheduler. The serial base parks the
|
|
50435
|
+
* whole queue under one shared timer + one shared exponential interval,
|
|
50436
|
+
* which conflate concurrent retries. This instead gives the failed action
|
|
50437
|
+
* its own timer and its own growing backoff, keyed by id, so it retries on
|
|
50438
|
+
* its own schedule without disturbing siblings still uploading in other
|
|
50439
|
+
* slots.
|
|
50440
|
+
*
|
|
50441
|
+
* Only broadcasts `Waiting` when the queue is genuinely idle (nothing in
|
|
50442
|
+
* flight): a concurrent queue with other slots live is still `Started`, and
|
|
50443
|
+
* on the serial path the failed action was already removed from
|
|
50444
|
+
* `uploadingActionIds` above (size 0), so the Waiting broadcast the regular
|
|
50445
|
+
* queue emits today is preserved.
|
|
49102
50446
|
*/
|
|
49103
|
-
|
|
49104
|
-
const
|
|
49105
|
-
|
|
49106
|
-
|
|
49107
|
-
|
|
49108
|
-
|
|
49109
|
-
|
|
49110
|
-
|
|
50447
|
+
async scheduleActionRetry(actionId, retryDelayInMs) {
|
|
50448
|
+
const delay = retryDelayInMs !== undefined ? retryDelayInMs : this.nextBackoffInterval(actionId);
|
|
50449
|
+
// Only move to Waiting when the queue is genuinely idle (nothing in
|
|
50450
|
+
// flight). A concurrent queue with other slots still uploading must stay
|
|
50451
|
+
// Started — otherwise sibling completions (which gate their
|
|
50452
|
+
// processNextAction on state === Started) would stop advancing the queue
|
|
50453
|
+
// and free slots would sit idle until this action's retry timer fired.
|
|
50454
|
+
// Only dispatchFreeSlots flips Waiting back to Started, and nothing
|
|
50455
|
+
// reaches it while state is Waiting. On the serial path the failed action
|
|
50456
|
+
// was already removed above (size 0), so the Waiting transition the
|
|
50457
|
+
// regular queue emits today is preserved.
|
|
50458
|
+
if (this.uploadingActionIds.size === 0) {
|
|
50459
|
+
this.state = DraftQueueState.Waiting;
|
|
50460
|
+
await this.notifyChangedListeners({
|
|
50461
|
+
type: DraftQueueEventType.QueueStateChanged,
|
|
50462
|
+
state: DraftQueueState.Waiting,
|
|
50463
|
+
draftCount: this.draftStore.getCount(),
|
|
50464
|
+
});
|
|
50465
|
+
}
|
|
50466
|
+
// Replace any existing timer for this action so a fresh failure resets
|
|
50467
|
+
// its own backoff window without leaking the prior timer.
|
|
50468
|
+
const existing = this.retryTimers.get(actionId);
|
|
50469
|
+
if (existing !== undefined) {
|
|
50470
|
+
clearTimeout(existing);
|
|
50471
|
+
}
|
|
50472
|
+
this.retryTimers.set(actionId, setTimeout(() => {
|
|
50473
|
+
// Only this action becomes eligible again; siblings keep their
|
|
50474
|
+
// own backoff windows.
|
|
50475
|
+
this.retryTimers.delete(actionId);
|
|
50476
|
+
this.retryingActionIds.delete(actionId);
|
|
50477
|
+
if (this.state !== DraftQueueState.Stopped) {
|
|
50478
|
+
this.processNextAction();
|
|
49111
50479
|
}
|
|
50480
|
+
}, delay));
|
|
50481
|
+
}
|
|
50482
|
+
/**
|
|
50483
|
+
* Per-action exponential backoff, mirroring the base's doubling within the
|
|
50484
|
+
* same [minimum, maximum] bounds but tracked per id so each retrying action
|
|
50485
|
+
* grows its own interval independently.
|
|
50486
|
+
*/
|
|
50487
|
+
nextBackoffInterval(actionId) {
|
|
50488
|
+
const current = this.retryIntervals.get(actionId) ?? 0;
|
|
50489
|
+
const next = Math.min(Math.max(current * 2, this.minimumRetryInterval), this.maximumRetryInterval);
|
|
50490
|
+
this.retryIntervals.set(actionId, next);
|
|
50491
|
+
return next;
|
|
50492
|
+
}
|
|
50493
|
+
/**
|
|
50494
|
+
* Drops all retry bookkeeping for an action: cancels its pending timer,
|
|
50495
|
+
* un-parks it, and forgets its backoff interval. Called on completion,
|
|
50496
|
+
* permanent failure, and removal.
|
|
50497
|
+
*/
|
|
50498
|
+
clearActionRetry(actionId) {
|
|
50499
|
+
const timer = this.retryTimers.get(actionId);
|
|
50500
|
+
if (timer !== undefined) {
|
|
50501
|
+
clearTimeout(timer);
|
|
50502
|
+
this.retryTimers.delete(actionId);
|
|
50503
|
+
}
|
|
50504
|
+
this.retryIntervals.delete(actionId);
|
|
50505
|
+
this.retryingActionIds.delete(actionId);
|
|
50506
|
+
}
|
|
50507
|
+
/**
|
|
50508
|
+
* Cancels every per-action retry timer. The base clears only its single
|
|
50509
|
+
* `timeoutHandler`; a concurrent queue may have many parked actions, so on
|
|
50510
|
+
* any full stop we must clear them all to avoid a dangling timer firing
|
|
50511
|
+
* against a stopped queue.
|
|
50512
|
+
*/
|
|
50513
|
+
clearAllRetryTimers() {
|
|
50514
|
+
for (const timer of this.retryTimers.values()) {
|
|
50515
|
+
clearTimeout(timer);
|
|
50516
|
+
}
|
|
50517
|
+
this.retryTimers.clear();
|
|
50518
|
+
this.retryIntervals.clear();
|
|
50519
|
+
this.retryingActionIds.clear();
|
|
50520
|
+
}
|
|
50521
|
+
stopQueueManually(internalReason) {
|
|
50522
|
+
this.clearAllRetryTimers();
|
|
50523
|
+
super.stopQueueManually(internalReason);
|
|
50524
|
+
}
|
|
50525
|
+
/**
|
|
50526
|
+
* Clears every per-action retry timer on (re)start for the same reason the
|
|
50527
|
+
* base clears its single `timeoutHandler`: a dangling backoff timer must not
|
|
50528
|
+
* fire against a freshly-started queue. Guarded by the same conditions under
|
|
50529
|
+
* which the base actually restarts (not already Started, not mid-replace) so
|
|
50530
|
+
* a `startQueue()` that is a no-op — or a replace-driven restart — doesn't
|
|
50531
|
+
* wipe legitimately-parked backoff windows out from under in-flight retries.
|
|
50532
|
+
*/
|
|
50533
|
+
async startQueue() {
|
|
50534
|
+
// Record the caller's intent up front, exactly as the base startQueue does
|
|
50535
|
+
// synchronously. The reconcile below awaits, so a stopQueue() can interleave
|
|
50536
|
+
// with it; capturing intent here lets that later stop flip userState and win
|
|
50537
|
+
// (see the revalidation after the await, R4-6).
|
|
50538
|
+
this.userState = DraftQueueState.Started;
|
|
50539
|
+
if (this.state !== DraftQueueState.Started && this.replacingAction === undefined) {
|
|
50540
|
+
this.clearAllRetryTimers();
|
|
50541
|
+
// Concurrent counterpart: the base scalar is never set here, so check the Set.
|
|
50542
|
+
// Stale ids in uploadingActionIds at restart strand the dispatcher's uploading guard.
|
|
50543
|
+
if (this.uploadingActionIds.size > 0 && this.logger) {
|
|
50544
|
+
this.logger.logWarning(`Draft queue restarting with ${this.uploadingActionIds.size} action(s) still marked uploading (stale uploadingActionIds=${[
|
|
50545
|
+
...this.uploadingActionIds,
|
|
50546
|
+
].join(',')}); possible abandoned-upload stuck queue`);
|
|
50547
|
+
}
|
|
50548
|
+
}
|
|
50549
|
+
// Recover from any batch journaled but never confirmed removed (B2)
|
|
50550
|
+
// before the base starts dispatching, so a committed-but-unremoved
|
|
50551
|
+
// member is surfaced as a commit-ambiguous error before collectBatch can
|
|
50552
|
+
// re-batch it (Option A). Guarded to run at most once per process; a no-op
|
|
50553
|
+
// when batching is disabled or the store keeps no journal.
|
|
50554
|
+
await this.reconcileBatchJournalsOnce();
|
|
50555
|
+
// The reconcile awaited above yields the event loop. If a stopQueue() ran
|
|
50556
|
+
// while it was pending, that stop already completed and set
|
|
50557
|
+
// userState=Stopped; honor it rather than clobbering the queue back to
|
|
50558
|
+
// Started via super.startQueue() and dispatching after the caller stopped
|
|
50559
|
+
// it (R4-6).
|
|
50560
|
+
if (this.userState !== DraftQueueState.Started) {
|
|
50561
|
+
return;
|
|
50562
|
+
}
|
|
50563
|
+
return super.startQueue();
|
|
50564
|
+
}
|
|
50565
|
+
async retryAllErrors() {
|
|
50566
|
+
const actions = await this.getOwnedQueueActions();
|
|
50567
|
+
const errored = actions.filter((a) => a.status === DraftActionStatus.Error);
|
|
50568
|
+
if (errored.length === 0) {
|
|
50569
|
+
return;
|
|
50570
|
+
}
|
|
50571
|
+
// Stop the queue before rewriting errored actions to Pending. With
|
|
50572
|
+
// strictOrdering=false the queue keeps running while errored items are
|
|
50573
|
+
// skipped, so a live pass could observe the store mid-rewrite. Stop,
|
|
50574
|
+
// rewrite, then restart safely.
|
|
50575
|
+
this.stopQueueManually(true);
|
|
50576
|
+
for (const action of errored) {
|
|
50577
|
+
// Drop the `error` field that's only present on ErrorDraftAction so
|
|
50578
|
+
// the resulting object structurally matches PendingDraftAction and
|
|
50579
|
+
// satisfies the discriminated-union narrower.
|
|
50580
|
+
const { error: _droppedError, ...rest } = action;
|
|
50581
|
+
const pendingAction = {
|
|
50582
|
+
...rest,
|
|
50583
|
+
status: DraftActionStatus.Pending,
|
|
50584
|
+
};
|
|
50585
|
+
await this.draftStore.writeAction(pendingAction);
|
|
50586
|
+
await this.notifyChangedListeners({
|
|
50587
|
+
type: DraftQueueEventType.ActionUpdated,
|
|
50588
|
+
action: pendingAction,
|
|
50589
|
+
draftCount: this.draftStore.getCount(),
|
|
50590
|
+
});
|
|
50591
|
+
}
|
|
50592
|
+
await this.startQueueSafe();
|
|
50593
|
+
}
|
|
50594
|
+
}
|
|
50595
|
+
/**
|
|
50596
|
+
* Error surfaced on a batch member whose commit status could not be confirmed:
|
|
50597
|
+
* the batch response was lost (network throw, empty/misaligned 2xx, a retryable
|
|
50598
|
+
* gateway envelope) or the process died mid-flight. Because the batch envelope
|
|
50599
|
+
* carries no server-visible idempotency key, such a write cannot be safely
|
|
50600
|
+
* re-sent without risking a duplicate, so the member is surfaced as an error for
|
|
50601
|
+
* manual resolution rather than auto-replayed (Option A). Shaped like a
|
|
50602
|
+
* FetchResponse so downstream consumers that read action.error.status/.body
|
|
50603
|
+
* behave consistently with a server error.
|
|
50604
|
+
*/
|
|
50605
|
+
PerformanceDraftQueue.COMMIT_AMBIGUOUS_ERROR = {
|
|
50606
|
+
status: 0,
|
|
50607
|
+
statusText: 'Batch commit ambiguous',
|
|
50608
|
+
ok: false,
|
|
50609
|
+
headers: {},
|
|
50610
|
+
body: {
|
|
50611
|
+
errorCode: 'BATCH_COMMIT_AMBIGUOUS',
|
|
50612
|
+
message: 'The batched change may or may not have been saved (the server response ' +
|
|
50613
|
+
'was not received). It was not retried automatically to avoid saving it ' +
|
|
50614
|
+
'twice; please verify the record and retry or discard the draft.',
|
|
50615
|
+
},
|
|
50616
|
+
};
|
|
50617
|
+
|
|
50618
|
+
class RoutingDraftQueue {
|
|
50619
|
+
/**
|
|
50620
|
+
* @param primaryLane the serial/fallback lane: it owns unregistered handlers and backs
|
|
50621
|
+
* every store read. Passed explicitly so routing never depends on array ordering.
|
|
50622
|
+
* @param secondaryLanes the remaining routed lanes (e.g. the concurrent binary lane).
|
|
50623
|
+
*/
|
|
50624
|
+
constructor(primaryLane, secondaryLanes = []) {
|
|
50625
|
+
this.primaryLane = primaryLane;
|
|
50626
|
+
this.lanes = [primaryLane, ...secondaryLanes];
|
|
50627
|
+
}
|
|
50628
|
+
get primary() {
|
|
50629
|
+
return this.primaryLane.queue;
|
|
50630
|
+
}
|
|
50631
|
+
/** Resolve the lane that owns `handlerId`, falling back to the primary lane. */
|
|
50632
|
+
laneForHandler(handlerId) {
|
|
50633
|
+
return (this.lanes.find(({ queue }) => queue.isHandlerRegistered(handlerId)) ?? this.primaryLane);
|
|
50634
|
+
}
|
|
50635
|
+
/** Resolve the sub-queue that owns `handlerId`, falling back to the primary queue. */
|
|
50636
|
+
routeByHandler(handlerId) {
|
|
50637
|
+
return this.laneForHandler(handlerId).queue;
|
|
50638
|
+
}
|
|
50639
|
+
/**
|
|
50640
|
+
* Resolve the sub-queue that owns the action identified by `actionId` by
|
|
50641
|
+
* reading its stamped handler from the shared store (visible via the primary
|
|
50642
|
+
* queue), then routing by handler ownership. Falls back to the primary queue
|
|
50643
|
+
* when the action can't be found.
|
|
50644
|
+
*/
|
|
50645
|
+
async routeByActionId(actionId) {
|
|
50646
|
+
const actions = await this.primary.getQueueActions();
|
|
50647
|
+
const action = actions.find((candidate) => candidate.id === actionId);
|
|
50648
|
+
return action !== undefined ? this.routeByHandler(action.handler) : this.primary;
|
|
50649
|
+
}
|
|
50650
|
+
enqueue(handlerId, data, observabilityContext) {
|
|
50651
|
+
return this.routeByHandler(handlerId).enqueue(handlerId, data, observabilityContext);
|
|
50652
|
+
}
|
|
50653
|
+
async getQueueActions() {
|
|
50654
|
+
const actions = await this.primary.getQueueActions();
|
|
50655
|
+
return actions.map((action) => ({
|
|
50656
|
+
...action,
|
|
50657
|
+
lane: this.laneForHandler(action.handler).lane,
|
|
50658
|
+
}));
|
|
50659
|
+
}
|
|
50660
|
+
addHandler(handler) {
|
|
50661
|
+
return this.primary.addHandler(handler);
|
|
50662
|
+
}
|
|
50663
|
+
addCustomHandler(id, executor) {
|
|
50664
|
+
return this.primary.addCustomHandler(id, executor);
|
|
50665
|
+
}
|
|
50666
|
+
removeHandler(id) {
|
|
50667
|
+
return this.primary.removeHandler(id);
|
|
50668
|
+
}
|
|
50669
|
+
async removeDraftAction(actionId) {
|
|
50670
|
+
const owner = await this.routeByActionId(actionId);
|
|
50671
|
+
return owner.removeDraftAction(actionId);
|
|
50672
|
+
}
|
|
50673
|
+
updateDraftAction(action) {
|
|
50674
|
+
return this.routeByHandler(action.handler).updateDraftAction(action);
|
|
50675
|
+
}
|
|
50676
|
+
async retryAction(actionId) {
|
|
50677
|
+
const owner = await this.routeByActionId(actionId);
|
|
50678
|
+
return owner.retryAction(actionId);
|
|
50679
|
+
}
|
|
50680
|
+
async setMetadata(actionId, metadata) {
|
|
50681
|
+
const owner = await this.routeByActionId(actionId);
|
|
50682
|
+
return owner.setMetadata(actionId, metadata);
|
|
50683
|
+
}
|
|
50684
|
+
async replaceAction(targetActionId, sourceActionId) {
|
|
50685
|
+
const owner = await this.routeByActionId(targetActionId);
|
|
50686
|
+
return owner.replaceAction(targetActionId, sourceActionId);
|
|
50687
|
+
}
|
|
50688
|
+
async mergeActions(targetActionId, sourceActionId) {
|
|
50689
|
+
const owner = await this.routeByActionId(targetActionId);
|
|
50690
|
+
return owner.mergeActions(targetActionId, sourceActionId);
|
|
50691
|
+
}
|
|
50692
|
+
registerOnChangedListener(listener) {
|
|
50693
|
+
const unsubscribes = this.lanes.map(({ queue }) => queue.registerOnChangedListener(listener));
|
|
50694
|
+
return () => Promise.all(unsubscribes.map((unsubscribe) => unsubscribe())).then(() => undefined);
|
|
50695
|
+
}
|
|
50696
|
+
async processNextAction() {
|
|
50697
|
+
const [primaryResult] = await Promise.all(this.lanes.map(({ queue }) => queue.processNextAction()));
|
|
50698
|
+
return primaryResult;
|
|
50699
|
+
}
|
|
50700
|
+
/**
|
|
50701
|
+
* Conservative aggregate of the sub-queue states, most-attention-needed first:
|
|
50702
|
+
* Error (blocked, needs intervention) > Stopped (explicitly halted) > Waiting
|
|
50703
|
+
* (transient network backoff) > Started. Not bridged — DraftManager reports
|
|
50704
|
+
* per-queue state separately; this exists only to satisfy the interface.
|
|
50705
|
+
*/
|
|
50706
|
+
getQueueState() {
|
|
50707
|
+
const states = this.lanes.map(({ queue }) => queue.getQueueState());
|
|
50708
|
+
if (states.includes(DraftQueueState.Error)) {
|
|
50709
|
+
return DraftQueueState.Error;
|
|
50710
|
+
}
|
|
50711
|
+
if (states.includes(DraftQueueState.Stopped)) {
|
|
50712
|
+
return DraftQueueState.Stopped;
|
|
50713
|
+
}
|
|
50714
|
+
if (states.includes(DraftQueueState.Waiting)) {
|
|
50715
|
+
return DraftQueueState.Waiting;
|
|
50716
|
+
}
|
|
50717
|
+
return DraftQueueState.Started;
|
|
50718
|
+
}
|
|
50719
|
+
async startQueue() {
|
|
50720
|
+
await Promise.all(this.lanes.map(({ queue }) => queue.startQueue()));
|
|
50721
|
+
}
|
|
50722
|
+
async stopQueue() {
|
|
50723
|
+
await Promise.all(this.lanes.map(({ queue }) => queue.stopQueue()));
|
|
50724
|
+
}
|
|
50725
|
+
/**
|
|
50726
|
+
* Stops every sub-queue, runs `action` once, then restarts every sub-queue.
|
|
50727
|
+
* Not bridged; the per-queue variant preserves userState via an internal stop,
|
|
50728
|
+
* which isn't reachable from here, so this uses the public stop/start pair.
|
|
50729
|
+
*/
|
|
50730
|
+
async stopQueueWhileRunning(action) {
|
|
50731
|
+
await Promise.all(this.lanes.map(({ queue }) => queue.stopQueue()));
|
|
50732
|
+
try {
|
|
50733
|
+
await action();
|
|
50734
|
+
}
|
|
50735
|
+
finally {
|
|
50736
|
+
await Promise.all(this.lanes.map(({ queue }) => queue.startQueue()));
|
|
49112
50737
|
}
|
|
49113
|
-
return Promise.resolve();
|
|
49114
50738
|
}
|
|
49115
50739
|
}
|
|
49116
50740
|
|
|
@@ -49178,6 +50802,16 @@ class DraftManager {
|
|
|
49178
50802
|
shouldEmitEvent(event) {
|
|
49179
50803
|
return this.draftEventsShouldBeEmitted.includes(event.type);
|
|
49180
50804
|
}
|
|
50805
|
+
/**
|
|
50806
|
+
* Routes a sub-queue change event into the listener pipeline. Exposed to subclasses so they
|
|
50807
|
+
* can register it on an additional queue without duplicating the emit-gating logic.
|
|
50808
|
+
*/
|
|
50809
|
+
handleQueueChange(event) {
|
|
50810
|
+
if (this.shouldEmitEvent(event)) {
|
|
50811
|
+
return this.callListeners(event);
|
|
50812
|
+
}
|
|
50813
|
+
return Promise.resolve();
|
|
50814
|
+
}
|
|
49181
50815
|
constructor(draftQueue) {
|
|
49182
50816
|
this.listeners = [];
|
|
49183
50817
|
this.draftEventsShouldBeEmitted = [
|
|
@@ -49190,12 +50824,7 @@ class DraftManager {
|
|
|
49190
50824
|
DraftQueueEventType.QueueStateChanged,
|
|
49191
50825
|
];
|
|
49192
50826
|
this.draftQueue = draftQueue;
|
|
49193
|
-
draftQueue.registerOnChangedListener((event) =>
|
|
49194
|
-
if (this.shouldEmitEvent(event)) {
|
|
49195
|
-
return this.callListeners(event);
|
|
49196
|
-
}
|
|
49197
|
-
return Promise.resolve();
|
|
49198
|
-
});
|
|
50827
|
+
draftQueue.registerOnChangedListener((event) => this.handleQueueChange(event));
|
|
49199
50828
|
}
|
|
49200
50829
|
draftQueueEventTypeToOperationType(type) {
|
|
49201
50830
|
switch (type) {
|
|
@@ -49395,6 +51024,11 @@ class DraftManager {
|
|
|
49395
51024
|
if (isDraftQueueStateChangeEvent(event)) {
|
|
49396
51025
|
operationType = DraftQueueOperationType.QueueStateChanged;
|
|
49397
51026
|
}
|
|
51027
|
+
else if (event.type === DraftQueueEventType.BatchRecoveryFailed) {
|
|
51028
|
+
// Telemetry-only signal (batch-recovery failure): it carries no
|
|
51029
|
+
// draft item, so DraftManager has nothing to surface to its listeners.
|
|
51030
|
+
return;
|
|
51031
|
+
}
|
|
49398
51032
|
else {
|
|
49399
51033
|
const { action, type } = event;
|
|
49400
51034
|
item = this.buildDraftQueueItem(action);
|
|
@@ -49473,6 +51107,560 @@ function isResourceRequestAction$1(action) {
|
|
|
49473
51107
|
return (dataAsAny !== undefined && dataAsAny.method !== undefined && dataAsAny.body !== undefined);
|
|
49474
51108
|
}
|
|
49475
51109
|
|
|
51110
|
+
function hasGetOwnedQueueActions(queue) {
|
|
51111
|
+
return typeof queue.getOwnedQueueActions === 'function';
|
|
51112
|
+
}
|
|
51113
|
+
function hasGetCreatedDraftIds(queue) {
|
|
51114
|
+
return typeof queue.getCreatedDraftIds === 'function';
|
|
51115
|
+
}
|
|
51116
|
+
function hasGetOwnedActionsReferencing(queue) {
|
|
51117
|
+
return typeof queue.getOwnedActionsReferencing === 'function';
|
|
51118
|
+
}
|
|
51119
|
+
function hasRetryAllErrors(queue) {
|
|
51120
|
+
return typeof queue.retryAllErrors === 'function';
|
|
51121
|
+
}
|
|
51122
|
+
/**
|
|
51123
|
+
* Gate-ON DraftManager: the concurrent-binary upload variant, instantiated only when the
|
|
51124
|
+
* `concurrentBinaryQueue` feature flag is on. It wraps a strict-ordering serial draft queue
|
|
51125
|
+
* plus a parallel binary queue over one shared store, stamps each draft's routing `lane`, and
|
|
51126
|
+
* drives the binary-queue lifecycle/read methods the native Pending Images UI consumes.
|
|
51127
|
+
*
|
|
51128
|
+
* The base `DraftManager` (gate-OFF) keeps every one of these methods as an inert stub so the
|
|
51129
|
+
* native bridge surface is identical regardless of the flag; this subclass supplies the real
|
|
51130
|
+
* behavior. Current (non-SCE) users never instantiate this class — they run the unchanged base.
|
|
51131
|
+
*/
|
|
51132
|
+
class RoutingDraftManager extends DraftManager {
|
|
51133
|
+
constructor(draftQueue, binaryQueue, laneLabels) {
|
|
51134
|
+
super(draftQueue);
|
|
51135
|
+
this.logger = typeof __nimbus !== 'undefined' &&
|
|
51136
|
+
__nimbus.plugins !== undefined &&
|
|
51137
|
+
__nimbus.plugins.JSLoggerPlugin !== undefined
|
|
51138
|
+
? __nimbus.plugins.JSLoggerPlugin
|
|
51139
|
+
: undefined;
|
|
51140
|
+
this.binaryQueue = binaryQueue;
|
|
51141
|
+
this.laneLabels = laneLabels;
|
|
51142
|
+
// Surface binary-queue change events through the same listener pipeline as the serial
|
|
51143
|
+
// queue (super already registered the serial queue; reuse the inherited handler here).
|
|
51144
|
+
this.binaryQueue.registerOnChangedListener((event) => this.handleQueueChange(event));
|
|
51145
|
+
}
|
|
51146
|
+
getQueue() {
|
|
51147
|
+
return Promise.all([this.draftQueue.getQueueActions(), this.binaryActionsById()])
|
|
51148
|
+
.then(([queueActions, binaryById]) => queueActions.map((action) => {
|
|
51149
|
+
const binaryAction = binaryById.get(action.id);
|
|
51150
|
+
return this.buildDraftQueueItem(this.stampLane(binaryAction ?? action, binaryAction !== undefined));
|
|
51151
|
+
}))
|
|
51152
|
+
.then(toQueueState(this.draftQueue));
|
|
51153
|
+
}
|
|
51154
|
+
/**
|
|
51155
|
+
* Get the current state of each of the DraftActions in the binary DraftQueue
|
|
51156
|
+
* (ContentDocument/Version/Link uploads).
|
|
51157
|
+
*/
|
|
51158
|
+
getBinaryQueue() {
|
|
51159
|
+
const queue = this.binaryQueue;
|
|
51160
|
+
const getActions = hasGetOwnedQueueActions(queue)
|
|
51161
|
+
? queue.getOwnedQueueActions()
|
|
51162
|
+
: queue.getQueueActions();
|
|
51163
|
+
return getActions
|
|
51164
|
+
.then((queueActions) => {
|
|
51165
|
+
return queueActions.map(this.buildDraftQueueItem);
|
|
51166
|
+
})
|
|
51167
|
+
.then(toQueueState(queue));
|
|
51168
|
+
}
|
|
51169
|
+
/**
|
|
51170
|
+
* Starts both the serial and binary draft queues and begins processing their items.
|
|
51171
|
+
*/
|
|
51172
|
+
startQueue() {
|
|
51173
|
+
return Promise.all([this.draftQueue.startQueue(), this.binaryQueue.startQueue()]).then(() => undefined);
|
|
51174
|
+
}
|
|
51175
|
+
/**
|
|
51176
|
+
* Stops both the serial and binary draft queues from processing more draft items after
|
|
51177
|
+
* any in-progress items are finished.
|
|
51178
|
+
*/
|
|
51179
|
+
stopQueue() {
|
|
51180
|
+
return Promise.all([this.draftQueue.stopQueue(), this.binaryQueue.stopQueue()]).then(() => undefined);
|
|
51181
|
+
}
|
|
51182
|
+
/**
|
|
51183
|
+
* Starts the binary draft queue (for ContentDocument/Version/Link uploads). Separate from
|
|
51184
|
+
* startQueue() so the binary queue can be paused/resumed independently from the regular
|
|
51185
|
+
* draft queue and from stopQueueWhileRunning() interruptions.
|
|
51186
|
+
*/
|
|
51187
|
+
startBinaryQueue() {
|
|
51188
|
+
return this.binaryQueue.startQueue();
|
|
51189
|
+
}
|
|
51190
|
+
/** Stops the binary draft queue; pairs with startBinaryQueue(). */
|
|
51191
|
+
stopBinaryQueue() {
|
|
51192
|
+
return this.binaryQueue.stopQueue();
|
|
51193
|
+
}
|
|
51194
|
+
async retryAllErrors() {
|
|
51195
|
+
if (hasRetryAllErrors(this.draftQueue)) {
|
|
51196
|
+
await this.draftQueue.retryAllErrors();
|
|
51197
|
+
}
|
|
51198
|
+
if (hasRetryAllErrors(this.binaryQueue)) {
|
|
51199
|
+
await this.binaryQueue.retryAllErrors();
|
|
51200
|
+
}
|
|
51201
|
+
}
|
|
51202
|
+
/**
|
|
51203
|
+
* Routes the remove to the queue that owns the action. The serial queue reads the shared
|
|
51204
|
+
* store, so it sees binary-owned actions too, but has no handler registered for them and
|
|
51205
|
+
* would throw.
|
|
51206
|
+
*/
|
|
51207
|
+
removeDraftAction(actionId) {
|
|
51208
|
+
return this.binaryActionsById()
|
|
51209
|
+
.then((binaryById) => {
|
|
51210
|
+
const binaryAction = binaryById.get(actionId);
|
|
51211
|
+
return binaryAction !== undefined
|
|
51212
|
+
? this.removeBinaryActionAndDependents(binaryAction)
|
|
51213
|
+
: this.draftQueue.removeDraftAction(actionId);
|
|
51214
|
+
})
|
|
51215
|
+
.then(() => this.getQueue());
|
|
51216
|
+
}
|
|
51217
|
+
/**
|
|
51218
|
+
* Removes a binary action plus the serial actions that reference a draft id it creates
|
|
51219
|
+
* (e.g. the ContentDocumentLink native enqueues to link an uploaded image to a record).
|
|
51220
|
+
* Nothing resolves those ids once the binary action is gone, so a dependent left behind
|
|
51221
|
+
* would dispatch with an unresolved draft id, fail, and block the serial queue.
|
|
51222
|
+
*
|
|
51223
|
+
* Rejects up front when a dependent is uploading: an errored binary action no longer holds
|
|
51224
|
+
* its dependents back, so one can be mid-dispatch. Otherwise the binary action goes first
|
|
51225
|
+
* so a rejected remove (e.g. it is uploading) leaves its dependents untouched.
|
|
51226
|
+
*/
|
|
51227
|
+
async removeBinaryActionAndDependents(action) {
|
|
51228
|
+
const dependents = await this.serialActionsDependingOn(action);
|
|
51229
|
+
const uploading = dependents.find((d) => d.status === DraftActionStatus.Uploading);
|
|
51230
|
+
if (uploading !== undefined) {
|
|
51231
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
51232
|
+
throw new Error(`Cannot remove draft action ${action.id} while its dependent draft action ${uploading.id} is uploading`);
|
|
51233
|
+
}
|
|
51234
|
+
await this.binaryQueue.removeDraftAction(action.id);
|
|
51235
|
+
for (const dependent of dependents) {
|
|
51236
|
+
const current = (await this.draftQueue.getQueueActions()).find((a) => a.id === dependent.id);
|
|
51237
|
+
// Removing an earlier dependent may already have deleted this one by shared tag.
|
|
51238
|
+
if (current === undefined) {
|
|
51239
|
+
continue;
|
|
51240
|
+
}
|
|
51241
|
+
// It started uploading after the check above. The binary action is already gone,
|
|
51242
|
+
// so keep going rather than report a failed remove; this one will error and can be
|
|
51243
|
+
// removed on its own.
|
|
51244
|
+
if (current.status === DraftActionStatus.Uploading) {
|
|
51245
|
+
if (this.logger) {
|
|
51246
|
+
this.logger.logWarning(`RoutingDraftManager: dependent draft action ${dependent.id} of removed draft action ${action.id} started uploading and was not removed`);
|
|
51247
|
+
}
|
|
51248
|
+
continue;
|
|
51249
|
+
}
|
|
51250
|
+
await this.draftQueue.removeDraftAction(dependent.id);
|
|
51251
|
+
}
|
|
51252
|
+
}
|
|
51253
|
+
serialActionsDependingOn(action) {
|
|
51254
|
+
const binary = this.binaryQueue;
|
|
51255
|
+
const serial = this.draftQueue;
|
|
51256
|
+
if (!hasGetCreatedDraftIds(binary) || !hasGetOwnedActionsReferencing(serial)) {
|
|
51257
|
+
return Promise.resolve([]);
|
|
51258
|
+
}
|
|
51259
|
+
const draftIds = binary.getCreatedDraftIds(action);
|
|
51260
|
+
return draftIds.length === 0
|
|
51261
|
+
? Promise.resolve([])
|
|
51262
|
+
: serial.getOwnedActionsReferencing(draftIds);
|
|
51263
|
+
}
|
|
51264
|
+
/**
|
|
51265
|
+
* Binary-lane actions keyed by id, carrying the lane's transient `Uploading` status.
|
|
51266
|
+
* `getQueue` uses it to stamp the `concurrentBinary` lane and source the correct status.
|
|
51267
|
+
* An empty map means "treat every action as serial" — returned both when the binary queue
|
|
51268
|
+
* can't report ownership and (harmlessly) when it is simply empty; the caller doesn't need
|
|
51269
|
+
* to tell those apart.
|
|
51270
|
+
*/
|
|
51271
|
+
binaryActionsById() {
|
|
51272
|
+
const binary = this.binaryQueue;
|
|
51273
|
+
if (!hasGetOwnedQueueActions(binary)) {
|
|
51274
|
+
return Promise.resolve(new Map());
|
|
51275
|
+
}
|
|
51276
|
+
return binary
|
|
51277
|
+
.getOwnedQueueActions()
|
|
51278
|
+
.then((actions) => new Map(actions.map((action) => [action.id, action])));
|
|
51279
|
+
}
|
|
51280
|
+
/** Stamp `action`'s routing `lane` from lane membership. */
|
|
51281
|
+
stampLane(action, isBinaryOwned) {
|
|
51282
|
+
const lane = isBinaryOwned ? this.laneLabels.concurrentBinary : this.laneLabels.serial;
|
|
51283
|
+
return { ...action, lane };
|
|
51284
|
+
}
|
|
51285
|
+
/**
|
|
51286
|
+
* Extends the base projection with the routing `lane` stamped onto the action. Only the
|
|
51287
|
+
* gate-ON path carries a lane; the base DraftManager never emits the field, so the gate-OFF
|
|
51288
|
+
* projection stays identical to before the concurrent-binary queue existed.
|
|
51289
|
+
*/
|
|
51290
|
+
buildDraftQueueItem(action) {
|
|
51291
|
+
const item = super.buildDraftQueueItem(action);
|
|
51292
|
+
if (action.lane !== undefined) {
|
|
51293
|
+
item.lane = action.lane;
|
|
51294
|
+
}
|
|
51295
|
+
return item;
|
|
51296
|
+
}
|
|
51297
|
+
}
|
|
51298
|
+
|
|
51299
|
+
function clone$1(obj) {
|
|
51300
|
+
return parse$6(stringify$6(obj));
|
|
51301
|
+
}
|
|
51302
|
+
|
|
51303
|
+
const DRAFT_ACTION_KEY_JUNCTION = '__DraftAction__';
|
|
51304
|
+
function buildDraftDurableStoreKey(recordKey, draftActionId) {
|
|
51305
|
+
return `${recordKey}${DRAFT_ACTION_KEY_JUNCTION}${draftActionId}`;
|
|
51306
|
+
}
|
|
51307
|
+
/**
|
|
51308
|
+
* Implements a write-through InMemoryStore for Drafts, storing all drafts in a
|
|
51309
|
+
* in-memory store with a write through to the DurableStore.
|
|
51310
|
+
*
|
|
51311
|
+
* Before any reads or writes come in from the draft queue, we need to revive the draft
|
|
51312
|
+
* queue into memory. During this initial revive, any writes are queued up and operated on the
|
|
51313
|
+
* queue once it's in memory. Similarly any reads are delayed until the queue is in memory.
|
|
51314
|
+
*
|
|
51315
|
+
*/
|
|
51316
|
+
class DurableDraftStore {
|
|
51317
|
+
constructor(durableStore, instrumentation, useSnapshot = true) {
|
|
51318
|
+
this.draftStore = {};
|
|
51319
|
+
// Snapshot: holds deep clones of each draft, refreshed at write time.
|
|
51320
|
+
// getAllDrafts serves from this snapshot with zero deep clones per read.
|
|
51321
|
+
this.cleanSnapshot = {};
|
|
51322
|
+
// queue of writes that were made during the initial sync
|
|
51323
|
+
this.writeQueue = [];
|
|
51324
|
+
this.durableStore = durableStore;
|
|
51325
|
+
this.instrumentation = instrumentation;
|
|
51326
|
+
this.useSnapshot = useSnapshot;
|
|
51327
|
+
this.resyncDraftStore();
|
|
51328
|
+
}
|
|
51329
|
+
// When no instrumentation is injected the promise is returned as-is (zero overhead).
|
|
51330
|
+
timeDurableOp(op, segment, run) {
|
|
51331
|
+
const onDurableOperation = this.instrumentation?.onDurableOperation;
|
|
51332
|
+
if (onDurableOperation === undefined) {
|
|
51333
|
+
return run();
|
|
51334
|
+
}
|
|
51335
|
+
const start = Date.now();
|
|
51336
|
+
return run().finally(() => {
|
|
51337
|
+
onDurableOperation(op, segment, Date.now() - start);
|
|
51338
|
+
});
|
|
51339
|
+
}
|
|
51340
|
+
writeAction(action) {
|
|
51341
|
+
const addAction = () => {
|
|
51342
|
+
const { id, tag } = action;
|
|
51343
|
+
this.draftStore[id] = action;
|
|
51344
|
+
this.cleanSnapshot[id] = clone$1(action);
|
|
51345
|
+
const durableEntryKey = buildDraftDurableStoreKey(tag, id);
|
|
51346
|
+
const entry = {
|
|
51347
|
+
data: action,
|
|
51348
|
+
};
|
|
51349
|
+
const entries = { [durableEntryKey]: entry };
|
|
51350
|
+
return this.timeDurableOp('setEntries', DRAFT_SEGMENT, () => this.durableStore.setEntries(entries, DRAFT_SEGMENT));
|
|
51351
|
+
};
|
|
51352
|
+
return this.enqueueAction(addAction);
|
|
51353
|
+
}
|
|
51354
|
+
getAllDrafts() {
|
|
51355
|
+
const waitForOngoingSync = this.syncPromise || Promise.resolve();
|
|
51356
|
+
return waitForOngoingSync.then(() => {
|
|
51357
|
+
const actionArray = [];
|
|
51358
|
+
if (this.useSnapshot) {
|
|
51359
|
+
const { cleanSnapshot } = this;
|
|
51360
|
+
const keys$1 = keys$4(cleanSnapshot);
|
|
51361
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
51362
|
+
const key = keys$1[i];
|
|
51363
|
+
// Return shallow spread of snapshot entries so top-level caller mutations
|
|
51364
|
+
// (e.g., .status =) don't persist into the snapshot. The snapshot itself
|
|
51365
|
+
// is a deep clone of draftStore, so no nested mutation reaches draftStore.
|
|
51366
|
+
actionArray.push({ ...cleanSnapshot[key] });
|
|
51367
|
+
}
|
|
51368
|
+
}
|
|
51369
|
+
else {
|
|
51370
|
+
// Killswitch (W-23515682): original pre-fix behavior — deep-clone every draft on
|
|
51371
|
+
// every read so the internal draft store is never exposed. O(N) clones per read.
|
|
51372
|
+
const { draftStore } = this;
|
|
51373
|
+
const keys$1 = keys$4(draftStore);
|
|
51374
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
51375
|
+
actionArray.push(clone$1(draftStore[keys$1[i]]));
|
|
51376
|
+
}
|
|
51377
|
+
}
|
|
51378
|
+
return actionArray;
|
|
51379
|
+
});
|
|
51380
|
+
}
|
|
51381
|
+
deleteDraft(id) {
|
|
51382
|
+
const deleteAction = () => {
|
|
51383
|
+
const draft = this.draftStore[id];
|
|
51384
|
+
if (draft !== undefined) {
|
|
51385
|
+
delete this.draftStore[id];
|
|
51386
|
+
delete this.cleanSnapshot[id];
|
|
51387
|
+
const durableKey = buildDraftDurableStoreKey(draft.tag, draft.id);
|
|
51388
|
+
return this.timeDurableOp('evictEntries', DRAFT_SEGMENT, () => this.durableStore.evictEntries([durableKey], DRAFT_SEGMENT));
|
|
51389
|
+
}
|
|
51390
|
+
return Promise.resolve();
|
|
51391
|
+
};
|
|
51392
|
+
return this.enqueueAction(deleteAction);
|
|
51393
|
+
}
|
|
51394
|
+
deleteByTag(tag) {
|
|
51395
|
+
const deleteAction = () => {
|
|
51396
|
+
const { draftStore } = this;
|
|
51397
|
+
const keys$1 = keys$4(draftStore);
|
|
51398
|
+
const durableKeys = [];
|
|
51399
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
51400
|
+
const key = keys$1[i];
|
|
51401
|
+
const action = draftStore[key];
|
|
51402
|
+
if (action.tag === tag) {
|
|
51403
|
+
delete draftStore[action.id];
|
|
51404
|
+
delete this.cleanSnapshot[action.id];
|
|
51405
|
+
durableKeys.push(buildDraftDurableStoreKey(action.tag, action.id));
|
|
51406
|
+
}
|
|
51407
|
+
}
|
|
51408
|
+
return this.timeDurableOp('evictEntries', DRAFT_SEGMENT, () => this.durableStore.evictEntries(durableKeys, DRAFT_SEGMENT));
|
|
51409
|
+
};
|
|
51410
|
+
return this.enqueueAction(deleteAction);
|
|
51411
|
+
}
|
|
51412
|
+
completeAction(queueOperations) {
|
|
51413
|
+
const action = () => {
|
|
51414
|
+
const durableStoreOperations = [];
|
|
51415
|
+
const { draftStore } = this;
|
|
51416
|
+
for (let i = 0, len = queueOperations.length; i < len; i++) {
|
|
51417
|
+
const operation = queueOperations[i];
|
|
51418
|
+
if (operation.type === QueueOperationType.Delete) {
|
|
51419
|
+
const action = draftStore[operation.id];
|
|
51420
|
+
if (action !== undefined) {
|
|
51421
|
+
delete draftStore[operation.id];
|
|
51422
|
+
delete this.cleanSnapshot[operation.id];
|
|
51423
|
+
const key = buildDraftDurableStoreKey(action.tag, action.id);
|
|
51424
|
+
durableStoreOperations.push({
|
|
51425
|
+
ids: [key],
|
|
51426
|
+
type: 'evictEntries',
|
|
51427
|
+
segment: DRAFT_SEGMENT,
|
|
51428
|
+
});
|
|
51429
|
+
}
|
|
51430
|
+
}
|
|
51431
|
+
else {
|
|
51432
|
+
const { action } = operation;
|
|
51433
|
+
const key = buildDraftDurableStoreKey(action.tag, action.id);
|
|
51434
|
+
draftStore[action.id] = action;
|
|
51435
|
+
this.cleanSnapshot[action.id] = clone$1(action);
|
|
51436
|
+
durableStoreOperations.push({
|
|
51437
|
+
type: 'setEntries',
|
|
51438
|
+
segment: DRAFT_SEGMENT,
|
|
51439
|
+
entries: {
|
|
51440
|
+
[key]: {
|
|
51441
|
+
data: operation.action,
|
|
51442
|
+
},
|
|
51443
|
+
},
|
|
51444
|
+
});
|
|
51445
|
+
}
|
|
51446
|
+
}
|
|
51447
|
+
return this.timeDurableOp('batchOperations', DRAFT_SEGMENT, () => this.durableStore.batchOperations(durableStoreOperations));
|
|
51448
|
+
};
|
|
51449
|
+
return this.enqueueAction(action);
|
|
51450
|
+
}
|
|
51451
|
+
getCount() {
|
|
51452
|
+
return keys$4(this.draftStore).length;
|
|
51453
|
+
}
|
|
51454
|
+
/**
|
|
51455
|
+
* Runs a write operation against the draft store, if the initial
|
|
51456
|
+
* revive is still in progress, the action gets enqueued to run once the
|
|
51457
|
+
* initial revive is complete
|
|
51458
|
+
* @param action
|
|
51459
|
+
* @returns a promise that is resolved once the action has run
|
|
51460
|
+
*/
|
|
51461
|
+
enqueueAction(action) {
|
|
51462
|
+
const { syncPromise, writeQueue: pendingMerges } = this;
|
|
51463
|
+
// if the initial sync is done and existing operations have been run, no need to queue, just run
|
|
51464
|
+
if (syncPromise === undefined && pendingMerges.length === 0) {
|
|
51465
|
+
return action();
|
|
51466
|
+
}
|
|
51467
|
+
const deferred = new Promise((resolve, reject) => {
|
|
51468
|
+
this.writeQueue.push(() => {
|
|
51469
|
+
return action()
|
|
51470
|
+
.then((x) => {
|
|
51471
|
+
resolve(x);
|
|
51472
|
+
})
|
|
51473
|
+
.catch((err) => reject(err));
|
|
51474
|
+
});
|
|
51475
|
+
});
|
|
51476
|
+
return deferred;
|
|
51477
|
+
}
|
|
51478
|
+
/**
|
|
51479
|
+
* Revives the draft store from the durable store. Once the draft store is
|
|
51480
|
+
* revived, executes any queued up draft store operations that came in while
|
|
51481
|
+
* reviving
|
|
51482
|
+
*/
|
|
51483
|
+
resyncDraftStore() {
|
|
51484
|
+
const sync = () => {
|
|
51485
|
+
this.syncPromise = this.durableStore
|
|
51486
|
+
.getAllEntries(DRAFT_SEGMENT)
|
|
51487
|
+
.then((durableEntries) => {
|
|
51488
|
+
if (durableEntries === undefined) {
|
|
51489
|
+
this.draftStore = {};
|
|
51490
|
+
this.cleanSnapshot = {};
|
|
51491
|
+
return this.runQueuedOperations();
|
|
51492
|
+
}
|
|
51493
|
+
const { draftStore } = this;
|
|
51494
|
+
const keys$1 = keys$4(durableEntries);
|
|
51495
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
51496
|
+
const entry = durableEntries[keys$1[i]];
|
|
51497
|
+
const action = entry.data;
|
|
51498
|
+
draftStore[action.id] = action;
|
|
51499
|
+
this.cleanSnapshot[action.id] = clone$1(action);
|
|
51500
|
+
}
|
|
51501
|
+
return this.runQueuedOperations();
|
|
51502
|
+
})
|
|
51503
|
+
.finally(() => {
|
|
51504
|
+
this.syncPromise = undefined;
|
|
51505
|
+
});
|
|
51506
|
+
return this.syncPromise;
|
|
51507
|
+
};
|
|
51508
|
+
// if there's an ongoing sync populating the in memory store, wait for it to complete before re-syncing
|
|
51509
|
+
const { syncPromise } = this;
|
|
51510
|
+
if (syncPromise === undefined) {
|
|
51511
|
+
return sync();
|
|
51512
|
+
}
|
|
51513
|
+
return syncPromise.then(() => {
|
|
51514
|
+
return sync();
|
|
51515
|
+
});
|
|
51516
|
+
}
|
|
51517
|
+
/**
|
|
51518
|
+
* Runs the operations that were queued up while reviving the
|
|
51519
|
+
* draft store from the durable store
|
|
51520
|
+
*/
|
|
51521
|
+
runQueuedOperations() {
|
|
51522
|
+
const { writeQueue } = this;
|
|
51523
|
+
if (writeQueue.length > 0) {
|
|
51524
|
+
const queueItem = writeQueue.shift();
|
|
51525
|
+
if (queueItem !== undefined) {
|
|
51526
|
+
return queueItem().then(() => {
|
|
51527
|
+
return this.runQueuedOperations();
|
|
51528
|
+
});
|
|
51529
|
+
}
|
|
51530
|
+
}
|
|
51531
|
+
return Promise.resolve();
|
|
51532
|
+
}
|
|
51533
|
+
}
|
|
51534
|
+
|
|
51535
|
+
/**
|
|
51536
|
+
* Durable-store segment holding batch-completion journal entries (B2). Kept
|
|
51537
|
+
* separate from DRAFT_SEGMENT so journal rows are never mistaken for draft
|
|
51538
|
+
* actions by DurableDraftStore's resync (which parses every DRAFT_SEGMENT entry
|
|
51539
|
+
* as a DraftAction).
|
|
51540
|
+
*/
|
|
51541
|
+
const DRAFT_BATCH_JOURNAL_SEGMENT = 'DRAFT_BATCH_JOURNAL';
|
|
51542
|
+
/**
|
|
51543
|
+
* The Performance Upload draft store. Extends {@link DurableDraftStore} — whose
|
|
51544
|
+
* body is byte-identical to today's shipped store — and serializes concurrent
|
|
51545
|
+
* writes through a `writeChain` so that when two queues share a single store
|
|
51546
|
+
* their snapshot-then-mutate sequences cannot interleave (which would otherwise
|
|
51547
|
+
* let one queue's queue-operation rewrites clobber another's). Only constructed
|
|
51548
|
+
* on the gate-ON code path; gate-off customers use a plain DurableDraftStore.
|
|
51549
|
+
*/
|
|
51550
|
+
class PerformanceDraftStore extends DurableDraftStore {
|
|
51551
|
+
constructor(durableStore) {
|
|
51552
|
+
super(durableStore);
|
|
51553
|
+
// Serializes writes so two queues sharing this store can't interleave; see
|
|
51554
|
+
// class doc.
|
|
51555
|
+
this.writeChain = Promise.resolve();
|
|
51556
|
+
// Identifies THIS store session (process launch). Minted fresh per
|
|
51557
|
+
// construction and never persisted, so a journal entry stamped with a
|
|
51558
|
+
// different token is crash residue from a dead process (recoverable), while
|
|
51559
|
+
// one stamped with this token is a batch still in flight this session. Sibling
|
|
51560
|
+
// queues share one store instance (the Performance Upload model) and therefore
|
|
51561
|
+
// one token; see readRecoverableBatchJournals.
|
|
51562
|
+
this.sessionToken = uuidv4();
|
|
51563
|
+
this.durableStoreRef = durableStore;
|
|
51564
|
+
}
|
|
51565
|
+
/**
|
|
51566
|
+
* Records, BEFORE a batch is dispatched, the ids of the members about to be
|
|
51567
|
+
* sent as one all-or-none request (B2). If the app dies — or the durable
|
|
51568
|
+
* removal fails — between the server commit and the client removing the
|
|
51569
|
+
* committed members, the next startQueue reconcile reads this journal and
|
|
51570
|
+
* pins any still-Pending member to the key-preserving serial path instead
|
|
51571
|
+
* of re-batching it (which would drop its idempotency key and replay a
|
|
51572
|
+
* committed write). Journal rows live in their own segment and never flow
|
|
51573
|
+
* through the draft writeChain — they are keyed by batchId, independent of
|
|
51574
|
+
* the draft snapshot-then-mutate sequence the chain serializes.
|
|
51575
|
+
*/
|
|
51576
|
+
writeBatchJournal(batchId, memberIds) {
|
|
51577
|
+
const entry = {
|
|
51578
|
+
// Stamp the live session token so a later reconcile can tell this
|
|
51579
|
+
// in-flight batch apart from crash residue (see sessionToken).
|
|
51580
|
+
data: { batchId, memberIds, owner: this.sessionToken },
|
|
51581
|
+
};
|
|
51582
|
+
return this.durableStoreRef.setEntries({ [batchId]: entry }, DRAFT_BATCH_JOURNAL_SEGMENT);
|
|
51583
|
+
}
|
|
51584
|
+
/** Removes a batch-completion journal entry once its members are durably resolved. */
|
|
51585
|
+
clearBatchJournal(batchId) {
|
|
51586
|
+
return this.durableStoreRef.evictEntries([batchId], DRAFT_BATCH_JOURNAL_SEGMENT);
|
|
51587
|
+
}
|
|
51588
|
+
/**
|
|
51589
|
+
* Returns only crash-residue journals — those written by a PRIOR store session
|
|
51590
|
+
* (owner token != this session's). Entries stamped with the live session's
|
|
51591
|
+
* token are batches still in flight this launch (possibly on a sibling queue
|
|
51592
|
+
* sharing this store) and are deliberately excluded, so a startup reconcile can
|
|
51593
|
+
* never quarantine a live peer batch's members out from under it. This is the
|
|
51594
|
+
* only read the queue reconcile uses; readBatchJournals (all entries) is kept
|
|
51595
|
+
* for inspection/tests.
|
|
51596
|
+
*
|
|
51597
|
+
* Relies on sibling queues sharing ONE store INSTANCE (the documented
|
|
51598
|
+
* Performance Upload model), so they share one session token. If two queues
|
|
51599
|
+
* were ever given separate store instances over the same durable store, the
|
|
51600
|
+
* token would have to move down to the durable store.
|
|
51601
|
+
*/
|
|
51602
|
+
readRecoverableBatchJournals() {
|
|
51603
|
+
return this.readBatchJournals().then((journals) => journals.filter((journal) => journal.owner !== this.sessionToken));
|
|
51604
|
+
}
|
|
51605
|
+
/** Returns every persisted batch-completion journal entry (inspection/tests). */
|
|
51606
|
+
readBatchJournals() {
|
|
51607
|
+
return this.durableStoreRef
|
|
51608
|
+
.getAllEntries(DRAFT_BATCH_JOURNAL_SEGMENT)
|
|
51609
|
+
.then((entries) => {
|
|
51610
|
+
if (entries === undefined) {
|
|
51611
|
+
return [];
|
|
51612
|
+
}
|
|
51613
|
+
const keys$1 = keys$4(entries);
|
|
51614
|
+
const journals = [];
|
|
51615
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
51616
|
+
journals.push(entries[keys$1[i]].data);
|
|
51617
|
+
}
|
|
51618
|
+
return journals;
|
|
51619
|
+
});
|
|
51620
|
+
}
|
|
51621
|
+
getAllDrafts() {
|
|
51622
|
+
const waitForOngoingSync = this.syncPromise || Promise.resolve();
|
|
51623
|
+
// Also wait for any outstanding writes serialized through the
|
|
51624
|
+
// write chain so callers observe writes initiated before this read.
|
|
51625
|
+
const waitForOutstandingWrites = this.writeChain;
|
|
51626
|
+
return Promise.all([waitForOngoingSync, waitForOutstandingWrites]).then(() => {
|
|
51627
|
+
const { draftStore } = this;
|
|
51628
|
+
const keys$1 = keys$4(draftStore);
|
|
51629
|
+
const actionArray = [];
|
|
51630
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
51631
|
+
const key = keys$1[i];
|
|
51632
|
+
// clone draft so we don't expose the internal draft store
|
|
51633
|
+
actionArray.push(clone$1(draftStore[key]));
|
|
51634
|
+
}
|
|
51635
|
+
return actionArray;
|
|
51636
|
+
});
|
|
51637
|
+
}
|
|
51638
|
+
enqueueAction(action) {
|
|
51639
|
+
const { syncPromise, writeQueue: pendingMerges } = this;
|
|
51640
|
+
// During initial sync, queue the write to run after sync completes.
|
|
51641
|
+
if (syncPromise !== undefined || pendingMerges.length > 0) {
|
|
51642
|
+
const deferred = new Promise((resolve, reject) => {
|
|
51643
|
+
this.writeQueue.push(() => {
|
|
51644
|
+
return action()
|
|
51645
|
+
.then((x) => {
|
|
51646
|
+
resolve(x);
|
|
51647
|
+
})
|
|
51648
|
+
.catch((err) => reject(err));
|
|
51649
|
+
});
|
|
51650
|
+
});
|
|
51651
|
+
return deferred;
|
|
51652
|
+
}
|
|
51653
|
+
// Sync done. Serialize concurrent writes through writeChain so that
|
|
51654
|
+
// when two queues share this store, their snapshot-then-mutate
|
|
51655
|
+
// sequences cannot interleave.
|
|
51656
|
+
const next = this.writeChain.then(action, action);
|
|
51657
|
+
// Don't propagate failures into the chain — every caller awaits its own
|
|
51658
|
+
// result and a single rejection must not poison subsequent writes.
|
|
51659
|
+
this.writeChain = next.catch(() => undefined);
|
|
51660
|
+
return next;
|
|
51661
|
+
}
|
|
51662
|
+
}
|
|
51663
|
+
|
|
49476
51664
|
const { hasOwnProperty: ObjectPrototypeHasOwnProperty$1$1 } = Object.prototype;
|
|
49477
51665
|
const { keys: ObjectKeys$1$2, create: ObjectCreate$1$2 } = Object;
|
|
49478
51666
|
const { stringify: JSONStringify$1 } = JSON;
|
|
@@ -80298,6 +82486,7 @@ const DRAFT_QUEUE_ACTION_DELETED = 'draft-queue-action-deleted';
|
|
|
80298
82486
|
const DRAFT_QUEUE_ACTION_UPDATED = 'draft-queue-action-updated';
|
|
80299
82487
|
const DRAFT_QUEUE_ACTION_FAILED = 'draft-queue-action-failed';
|
|
80300
82488
|
const DRAFT_QUEUE_ACTION_UPLOAD_ERROR = 'draft-queue-action-upload-error';
|
|
82489
|
+
const DRAFT_QUEUE_BATCH_RECOVERY_FAILED = 'draft-queue-batch-recovery-failed';
|
|
80301
82490
|
const DRAFT_QUEUE_TOTAL_MERGE_ACTIONS_CALLS = 'draft-queue-total-mergeActions-calls';
|
|
80302
82491
|
// Emitted every time a draft's Idempotency-Key is rotated (replaced with a fresh uuid).
|
|
80303
82492
|
// Used to triage the WOLI duplicate-record investigation in Splunk — correlate by draftId
|
|
@@ -80348,6 +82537,11 @@ const nimbusLogger = typeof __nimbus !== 'undefined' &&
|
|
|
80348
82537
|
__nimbus.plugins.JSLoggerPlugin !== undefined
|
|
80349
82538
|
? __nimbus.plugins.JSLoggerPlugin
|
|
80350
82539
|
: undefined;
|
|
82540
|
+
function logInfo(message) {
|
|
82541
|
+
if (nimbusLogger) {
|
|
82542
|
+
nimbusLogger.logInfo(message);
|
|
82543
|
+
}
|
|
82544
|
+
}
|
|
80351
82545
|
function reportGraphqlQueryParseError(err) {
|
|
80352
82546
|
const error = normalizeError$2(err);
|
|
80353
82547
|
const errorCode = GRAPHQL_QUERY_PARSE_ERROR;
|
|
@@ -80393,9 +82587,7 @@ function incrementGraphQLRefreshUndfined() {
|
|
|
80393
82587
|
ldsMobileInstrumentation$2.incrementCounter(GRAPHQL_SNAPSHOT_REFRESH_UNDEFINED);
|
|
80394
82588
|
}
|
|
80395
82589
|
function reportDraftActionEvent(state, draftCount, message) {
|
|
80396
|
-
|
|
80397
|
-
nimbusLogger.logInfo(`Draft action event: ${state}, depth: ${draftCount}${message ? `, message: ${message}` : ''}`);
|
|
80398
|
-
}
|
|
82590
|
+
logInfo(`Draft action event: ${state}, depth: ${draftCount}${message ? `, message: ${message}` : ''}`);
|
|
80399
82591
|
switch (state) {
|
|
80400
82592
|
case 'added':
|
|
80401
82593
|
ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_ACTION_ADDED);
|
|
@@ -80595,6 +82787,26 @@ function reportDraftActionUploadError(error, handlerId, attempt) {
|
|
|
80595
82787
|
});
|
|
80596
82788
|
ldsMobileInstrumentation$2.error(normalized, DRAFT_QUEUE_ACTION_UPLOAD_ERROR);
|
|
80597
82789
|
}
|
|
82790
|
+
/**
|
|
82791
|
+
* Reports a batch-journal crash-recovery failure on the Performance Upload
|
|
82792
|
+
* batching path (B2, currently feature-gated and dormant): a maybe-committed
|
|
82793
|
+
* all-or-none batch's members could not be durably quarantined this session, a
|
|
82794
|
+
* startup reconcile could not surface a prior crash's batch, or a resolved journal
|
|
82795
|
+
* row could not be cleared. Each is otherwise visible only in device logs; this
|
|
82796
|
+
* surfaces it to o11y (greppable log line + reason-tagged counter) so the batching
|
|
82797
|
+
* path can be monitored when it ships.
|
|
82798
|
+
*
|
|
82799
|
+
* PII: carries ONLY the bounded reason code and the queue depth — never a record
|
|
82800
|
+
* id, draft id, tag, or free-text error message (device-log PII policy).
|
|
82801
|
+
*/
|
|
82802
|
+
function reportBatchRecoveryFailure(reason, draftCount) {
|
|
82803
|
+
if (nimbusLogger) {
|
|
82804
|
+
nimbusLogger.logInfo(`Batch recovery failed: reason=${reason}, depth=${draftCount}`);
|
|
82805
|
+
}
|
|
82806
|
+
ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_BATCH_RECOVERY_FAILED, 1, true, {
|
|
82807
|
+
reason,
|
|
82808
|
+
});
|
|
82809
|
+
}
|
|
80598
82810
|
/**
|
|
80599
82811
|
* Emits a single greppable Splunk log line (and an o11y counter) every time a draft's
|
|
80600
82812
|
* Idempotency-Key is rotated — i.e. replaced with a fresh uuid rather than reused on
|
|
@@ -80624,9 +82836,7 @@ function reportIdempotencyKeyRotated(draftId, reason) {
|
|
|
80624
82836
|
});
|
|
80625
82837
|
}
|
|
80626
82838
|
function reportDraftQueueState(state, draftCount) {
|
|
80627
|
-
|
|
80628
|
-
nimbusLogger.logInfo(`Draft state changed: ${state}, depth: ${draftCount}`);
|
|
80629
|
-
}
|
|
82839
|
+
logInfo(`Draft state changed: ${state}, depth: ${draftCount}`);
|
|
80630
82840
|
switch (state) {
|
|
80631
82841
|
case 'started':
|
|
80632
82842
|
ldsMobileInstrumentation$2.incrementCounter(DRAFT_QUEUE_STATE_STARTED);
|
|
@@ -80739,6 +82949,26 @@ function getRetryAfterInMs(headers) {
|
|
|
80739
82949
|
|
|
80740
82950
|
const DEFAULT_FIELD_LAST_MODIFIED_DATE$1 = 'LastModifiedDate';
|
|
80741
82951
|
const DEFAULT_FIELD_CREATED_DATE$1 = 'CreatedDate';
|
|
82952
|
+
// UI-API Records Batch endpoint. Reuses the same baseUri pattern as the
|
|
82953
|
+
// generated record adapters (see e.g. getUiApiRecordsByRecordId.ts).
|
|
82954
|
+
const BATCH_BASE_URI = '/services/data/v68.0';
|
|
82955
|
+
const BATCH_BASE_PATH = '/ui-api/records/batch';
|
|
82956
|
+
// Non-attributive error codes returned by an allOrNone=true batch. When one
|
|
82957
|
+
// operation fails, UI-API rolls the whole batch back and marks the *innocent*
|
|
82958
|
+
// siblings with one of these codes — they are not themselves the offender:
|
|
82959
|
+
// - ALL_OR_NONE_OPERATION_ROLLED_BACK: the canonical all-or-none rollback
|
|
82960
|
+
// marker applied to every sibling that would otherwise have succeeded.
|
|
82961
|
+
// - PROCESSING_HALTED: applied to operations skipped because processing was
|
|
82962
|
+
// halted (e.g. a dependency/composite-ordering stop) before they ran.
|
|
82963
|
+
// The true offender is any member whose errorCode is neither of these.
|
|
82964
|
+
const PROCESSING_HALTED_ERROR_CODE = 'PROCESSING_HALTED';
|
|
82965
|
+
const ALL_OR_NONE_ROLLED_BACK_ERROR_CODE = 'ALL_OR_NONE_OPERATION_ROLLED_BACK';
|
|
82966
|
+
// Codes that mark a batch member as a rolled-back innocent sibling rather than
|
|
82967
|
+
// the operation that actually caused the batch to fail.
|
|
82968
|
+
const ROLLBACK_SIBLING_ERROR_CODES = new Set([
|
|
82969
|
+
PROCESSING_HALTED_ERROR_CODE,
|
|
82970
|
+
ALL_OR_NONE_ROLLED_BACK_ERROR_CODE,
|
|
82971
|
+
]);
|
|
80742
82972
|
class AbstractResourceRequestActionHandler {
|
|
80743
82973
|
constructor(draftQueue, networkAdapter, getLuvio, recordService) {
|
|
80744
82974
|
this.draftQueue = draftQueue;
|
|
@@ -80901,23 +83131,7 @@ class AbstractResourceRequestActionHandler {
|
|
|
80901
83131
|
actionDataChanged = true;
|
|
80902
83132
|
}
|
|
80903
83133
|
}
|
|
80904
|
-
if (response
|
|
80905
|
-
this.isBackdatingError(response.body, action)) {
|
|
80906
|
-
updatedAction.timestamp = Date.now();
|
|
80907
|
-
updatedAction.data.body.fields = {
|
|
80908
|
-
...updatedAction.data.body.fields,
|
|
80909
|
-
LastModifiedDate: new Date(updatedAction.timestamp).toISOString(),
|
|
80910
|
-
};
|
|
80911
|
-
// The record content changed (LastModifiedDate is bumped), so this is a NEW
|
|
80912
|
-
// logical write. Rotate the idempotency key so the server treats it as such
|
|
80913
|
-
// rather than deduping it against the stale, pre-backdate request. This is a
|
|
80914
|
-
// legitimate rotation — the request body is modified — unlike the generic-400
|
|
80915
|
-
// fallback above which must preserve the key. @W-23445154
|
|
80916
|
-
if (this.hasIdempotencySupport() &&
|
|
80917
|
-
updatedAction.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] !== undefined) {
|
|
80918
|
-
updatedAction.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
|
|
80919
|
-
reportIdempotencyKeyRotated(updatedAction.targetId, 'backdating-collision');
|
|
80920
|
-
}
|
|
83134
|
+
if (this.applyBackdatingRetryIfNeeded(response, updatedAction)) {
|
|
80921
83135
|
shouldRetry = true;
|
|
80922
83136
|
actionDataChanged = true;
|
|
80923
83137
|
}
|
|
@@ -81015,6 +83229,113 @@ class AbstractResourceRequestActionHandler {
|
|
|
81015
83229
|
};
|
|
81016
83230
|
await actionErrored(retryingAction, true, retryDelayInMs, true);
|
|
81017
83231
|
}
|
|
83232
|
+
/**
|
|
83233
|
+
* If `response` is a LastModifiedDate backdating collision (@W-23445154) for
|
|
83234
|
+
* `action`, mutate the action so it is treated as a NEW logical write — bump
|
|
83235
|
+
* its timestamp + LastModifiedDate, and rotate the idempotency key when one
|
|
83236
|
+
* is present (the body changed, so the server must not dedupe it against the
|
|
83237
|
+
* stale pre-backdate request) — and return true. Otherwise return false and
|
|
83238
|
+
* leave the action untouched.
|
|
83239
|
+
*
|
|
83240
|
+
* Extracted from the single-action handleAction path so the batch upload
|
|
83241
|
+
* path (UiApiActionHandler.classifyBatchFailure) treats a backdating
|
|
83242
|
+
* collision identically instead of diverging (B4).
|
|
83243
|
+
*/
|
|
83244
|
+
applyBackdatingRetryIfNeeded(response, action) {
|
|
83245
|
+
if (response.status !== HttpStatusCode$2.BadRequest ||
|
|
83246
|
+
!this.isBackdatingError(response.body, action)) {
|
|
83247
|
+
return false;
|
|
83248
|
+
}
|
|
83249
|
+
action.timestamp = Date.now();
|
|
83250
|
+
action.data.body.fields = {
|
|
83251
|
+
...action.data.body.fields,
|
|
83252
|
+
LastModifiedDate: new Date(action.timestamp).toISOString(),
|
|
83253
|
+
};
|
|
83254
|
+
if (this.hasIdempotencySupport() &&
|
|
83255
|
+
action.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] !== undefined) {
|
|
83256
|
+
action.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
|
|
83257
|
+
reportIdempotencyKeyRotated(action.targetId, 'backdating-collision');
|
|
83258
|
+
}
|
|
83259
|
+
return true;
|
|
83260
|
+
}
|
|
83261
|
+
/**
|
|
83262
|
+
* Classifies a failure surfaced by the batch upload path — either a non-ok
|
|
83263
|
+
* batch envelope (whole request failed) or a single offending operation's
|
|
83264
|
+
* error result — into a retry-or-terminal decision, reusing the SAME policy
|
|
83265
|
+
* the single-action handleAction path applies to the same failure shapes so
|
|
83266
|
+
* the two do not diverge (B4). The caller routes the decision: a retry goes
|
|
83267
|
+
* through retryOrCap (so the durable attempt cap and backoff apply), a
|
|
83268
|
+
* terminal result errors the action with no retry.
|
|
83269
|
+
*
|
|
83270
|
+
* Note the idempotency-key state machine in handleAction (key rotation on
|
|
83271
|
+
* recognized idempotency error codes, isIdempotencySupported toggling) is
|
|
83272
|
+
* intentionally NOT shared here: those branches are keyed on the request's
|
|
83273
|
+
* per-action Idempotency-Key, which the batch envelope does not carry.
|
|
83274
|
+
*
|
|
83275
|
+
* commitAmbiguous distinguishes retryable failures that MAY have committed the
|
|
83276
|
+
* all-or-none batch at the origin (a 5xx/408/dropped-connection — or a generic
|
|
83277
|
+
* gateway 400 — returned after the write landed, with the success response lost
|
|
83278
|
+
* in transit) from retryable failures that definitively rolled back or never
|
|
83279
|
+
* reached the origin (a 429 rate-limit, a backdating collision the server
|
|
83280
|
+
* evaluated and rejected). The batch envelope carries no server-visible
|
|
83281
|
+
* idempotency key, so a commit-ambiguous failure cannot be safely re-sent; the
|
|
83282
|
+
* caller returns NETWORK_ERROR and the queue surfaces the members for manual
|
|
83283
|
+
* resolution instead of auto-replaying them (Option A). A definitively
|
|
83284
|
+
* rolled-back failure committed nothing, so a keyless re-batch is replay-safe.
|
|
83285
|
+
*/
|
|
83286
|
+
classifyBatchFailure(response, action) {
|
|
83287
|
+
const status = response.status ?? 0;
|
|
83288
|
+
// Transient / infrastructure failures — retry, honoring any server-sent
|
|
83289
|
+
// Retry-After (mirrors handleAction's 408/503 handling). 0 = no response
|
|
83290
|
+
// (connection dropped / suspended in flight); 408 Request Timeout; 429
|
|
83291
|
+
// Too Many Requests; 5xx server errors.
|
|
83292
|
+
if (status === 0 ||
|
|
83293
|
+
status === 408 ||
|
|
83294
|
+
status === 429 ||
|
|
83295
|
+
status >= HttpStatusCode$2.ServerError) {
|
|
83296
|
+
return {
|
|
83297
|
+
shouldRetry: true,
|
|
83298
|
+
retryDelayInMs: getRetryAfterInMs(response.headers),
|
|
83299
|
+
actionDataChanged: false,
|
|
83300
|
+
// 0 (dropped connection), 408 (timeout), and 5xx (gateway/LB/origin
|
|
83301
|
+
// server error) can each be returned AFTER the all-or-none batch
|
|
83302
|
+
// committed at the origin with the 2xx lost — commit-ambiguous, like
|
|
83303
|
+
// handleAction's NoResponse/ServiceUnavailable path. A 429 is a rate
|
|
83304
|
+
// limiter rejecting the request before it is processed, so nothing
|
|
83305
|
+
// committed and a keyless re-batch is replay-safe.
|
|
83306
|
+
commitAmbiguous: status !== 429,
|
|
83307
|
+
};
|
|
83308
|
+
}
|
|
83309
|
+
// LastModifiedDate backdating collision — bump the record + retry, same
|
|
83310
|
+
// as the single-action path. The server evaluated and rejected the write,
|
|
83311
|
+
// so the batch rolled back — not commit-ambiguous.
|
|
83312
|
+
if (this.applyBackdatingRetryIfNeeded(response, action)) {
|
|
83313
|
+
return { shouldRetry: true, actionDataChanged: true, commitAmbiguous: false };
|
|
83314
|
+
}
|
|
83315
|
+
// A 400 with no recognizable server-generated error body is an edge- or
|
|
83316
|
+
// gateway-injected rejection whose commit status is genuinely ambiguous: an
|
|
83317
|
+
// edge proxy or load balancer can return a bare 400 AFTER the origin has
|
|
83318
|
+
// already committed the all-or-none batch. The envelope carries no
|
|
83319
|
+
// server-visible idempotency key, so a keyless re-send could double-apply.
|
|
83320
|
+
// Treat it as commit-ambiguous (like status 0/408/5xx) so the queue does NOT
|
|
83321
|
+
// auto-replay it and surfaces the members for manual resolution instead.
|
|
83322
|
+
if (status === HttpStatusCode$2.BadRequest && !this.isServerGeneratedError(response.body)) {
|
|
83323
|
+
return { shouldRetry: true, actionDataChanged: false, commitAmbiguous: true };
|
|
83324
|
+
}
|
|
83325
|
+
// A real, server-generated client error (validation, permissions, a
|
|
83326
|
+
// deterministic 4xx) — terminal. Retrying it unchanged would only be
|
|
83327
|
+
// rejected again.
|
|
83328
|
+
return { shouldRetry: false, actionDataChanged: false, commitAmbiguous: false };
|
|
83329
|
+
}
|
|
83330
|
+
/**
|
|
83331
|
+
* Reports an upload failure to o11y using the same channel and attempt
|
|
83332
|
+
* numbering as the single-action handleAction catch. Exposed so the batch
|
|
83333
|
+
* upload path surfaces a thrown/deterministic failure to telemetry instead
|
|
83334
|
+
* of swallowing it (B4).
|
|
83335
|
+
*/
|
|
83336
|
+
reportUploadFailure(error, action) {
|
|
83337
|
+
reportDraftActionUploadError(error, this.handlerId, this.getUploadRetryCount(action) + 1);
|
|
83338
|
+
}
|
|
81018
83339
|
async handleActionEnqueued(action) {
|
|
81019
83340
|
const impactedKeys = await this.recordService.setSideEffectsForActions([action], 'enqueued');
|
|
81020
83341
|
await this.recordService.reapplyRecordSideEffects(impactedKeys, 'enqueued');
|
|
@@ -81477,6 +83798,23 @@ class MemoizingRecordReader {
|
|
|
81477
83798
|
|
|
81478
83799
|
const LDS_ACTION_HANDLER_ID = 'LDS_ACTION_HANDLER';
|
|
81479
83800
|
const LDS_ACTION_METADATA_API_NAME = 'LDS_ACTION_METADATA_API_NAME';
|
|
83801
|
+
// Request headers/query params that the UI-API records/batch envelope can omit
|
|
83802
|
+
// without changing the write, so their presence must not disqualify an action
|
|
83803
|
+
// from batching (see isActionBatchable, B1). Everything not listed here does
|
|
83804
|
+
// change server behavior and forces single-action dispatch.
|
|
83805
|
+
const BATCH_OMITTABLE_HEADERS = new Set([HTTP_HEADER_IDEMPOTENCY_KEY]);
|
|
83806
|
+
const BATCH_OMITTABLE_QUERY_PARAMS = new Set(['includeFieldsInBody']);
|
|
83807
|
+
// Diagnostic log line for the local-experimental batching path. Mirrors the
|
|
83808
|
+
// helper in PerformanceDraftQueue so handler-side events show up in the same
|
|
83809
|
+
// device-log stream with the same `Batch ` prefix.
|
|
83810
|
+
function batchLog(line) {
|
|
83811
|
+
try {
|
|
83812
|
+
globalThis.__nimbus?.plugins?.JSLoggerPlugin?.logInfo(`Batch ${line}`);
|
|
83813
|
+
}
|
|
83814
|
+
catch {
|
|
83815
|
+
// debug-only
|
|
83816
|
+
}
|
|
83817
|
+
}
|
|
81480
83818
|
class UiApiActionHandler extends AbstractResourceRequestActionHandler {
|
|
81481
83819
|
constructor(getLuvio, networkAdapter, draftQueue, getRecordAdapter, objectInfoService, isDraftId, recordService, sideEffectService, durableRecordStore) {
|
|
81482
83820
|
super(draftQueue, networkAdapter, getLuvio, recordService);
|
|
@@ -81659,6 +83997,500 @@ class UiApiActionHandler extends AbstractResourceRequestActionHandler {
|
|
|
81659
83997
|
getIdFromResponseBody(responseBody) {
|
|
81660
83998
|
return responseBody.id;
|
|
81661
83999
|
}
|
|
84000
|
+
/**
|
|
84001
|
+
* The UI-API records/batch envelope carries only { type, records:[{ apiName?,
|
|
84002
|
+
* fields }] } per operation — it has no place for per-operation headers or
|
|
84003
|
+
* query params (see buildBatchOperations / executeBatch, which send empty
|
|
84004
|
+
* headers and queryParams). An action whose request relies on a header or
|
|
84005
|
+
* query param that changes the *write* would silently lose that semantic if
|
|
84006
|
+
* folded into a batch, so it must be dispatched through single-action
|
|
84007
|
+
* handleAction (which sends the request verbatim) instead (B1).
|
|
84008
|
+
*
|
|
84009
|
+
* Two request members are present on essentially every update/delete draft
|
|
84010
|
+
* and are safe for the batch to omit, so they must NOT disqualify batching
|
|
84011
|
+
* (doing so would disable it wholesale):
|
|
84012
|
+
* - Idempotency-Key header: dropped from the batch envelope by design (the
|
|
84013
|
+
* batch has no per-op key). Its absence weakens replay protection, which
|
|
84014
|
+
* is addressed by journaled batch completion (B2) — it is not a per-op
|
|
84015
|
+
* write-semantic change, so it does not block batching here.
|
|
84016
|
+
* - includeFieldsInBody query param: only shapes the response body, not the
|
|
84017
|
+
* write; updateRecord adds it unconditionally.
|
|
84018
|
+
* Any OTHER header (e.g. If-Unmodified-Since — a conditional update that would
|
|
84019
|
+
* become an unconditional lost-update) or query param (useDefaultRule,
|
|
84020
|
+
* triggerUserEmail, triggerOtherEmail — assignment-rule / email side effects)
|
|
84021
|
+
* does change server behavior and makes the action non-batchable.
|
|
84022
|
+
*
|
|
84023
|
+
* The request BODY is checked too: buildBatchOperations copies ONLY `fields`
|
|
84024
|
+
* into the batch UPDATE/DELETE operation, so any other body member — e.g.
|
|
84025
|
+
* `allowSaveOnDuplicate`, which changes duplicate-rule handling — would be
|
|
84026
|
+
* silently dropped if the action were batched. An action whose body carries
|
|
84027
|
+
* anything beyond `fields` is therefore dispatched single-action (handleAction
|
|
84028
|
+
* sends the body verbatim), preserving those semantics.
|
|
84029
|
+
*
|
|
84030
|
+
* resolveResourceRequest only rewrites basePath/body/urlParams (never headers
|
|
84031
|
+
* or queryParams), and a body option like allowSaveOnDuplicate survives that
|
|
84032
|
+
* rewrite, so the raw action.data is authoritative for this check and we avoid
|
|
84033
|
+
* the cost of resolving here.
|
|
84034
|
+
*/
|
|
84035
|
+
isActionBatchable(action) {
|
|
84036
|
+
const request = action.data;
|
|
84037
|
+
if (request.headers !== undefined) {
|
|
84038
|
+
const headerNames = keys$3(request.headers);
|
|
84039
|
+
for (let i = 0; i < headerNames.length; i++) {
|
|
84040
|
+
if (!BATCH_OMITTABLE_HEADERS.has(headerNames[i])) {
|
|
84041
|
+
return false;
|
|
84042
|
+
}
|
|
84043
|
+
}
|
|
84044
|
+
}
|
|
84045
|
+
if (request.queryParams !== undefined) {
|
|
84046
|
+
const queryNames = keys$3(request.queryParams);
|
|
84047
|
+
for (let i = 0; i < queryNames.length; i++) {
|
|
84048
|
+
if (!BATCH_OMITTABLE_QUERY_PARAMS.has(queryNames[i])) {
|
|
84049
|
+
return false;
|
|
84050
|
+
}
|
|
84051
|
+
}
|
|
84052
|
+
}
|
|
84053
|
+
// Only `fields` is carried into the batch op (buildBatchOperations); any
|
|
84054
|
+
// other body member (e.g. allowSaveOnDuplicate) would be dropped, so it
|
|
84055
|
+
// disqualifies the action from batching.
|
|
84056
|
+
const body = request.body;
|
|
84057
|
+
if (body !== undefined && body !== null) {
|
|
84058
|
+
const bodyKeys = keys$3(body);
|
|
84059
|
+
for (let i = 0; i < bodyKeys.length; i++) {
|
|
84060
|
+
if (bodyKeys[i] !== 'fields') {
|
|
84061
|
+
return false;
|
|
84062
|
+
}
|
|
84063
|
+
}
|
|
84064
|
+
}
|
|
84065
|
+
return true;
|
|
84066
|
+
}
|
|
84067
|
+
/**
|
|
84068
|
+
* Sends a single UI-API records/batch request that combines all given actions.
|
|
84069
|
+
* Uses allOrNone=true so any failure rolls back the whole batch on the server,
|
|
84070
|
+
* preserving today's "one bad mutation blocks the queue" UX.
|
|
84071
|
+
*
|
|
84072
|
+
* Per-action result routing pairs each result with its action by index. The
|
|
84073
|
+
* endpoint exposes no per-result correlation key (no client-supplied
|
|
84074
|
+
* referenceId; DELETE/error results carry no id) and does not contractually
|
|
84075
|
+
* guarantee results order matches operations order, so index pairing is
|
|
84076
|
+
* guarded: the result count must equal the action count, and each UPDATE
|
|
84077
|
+
* result's returned record id must match the action's resolved recordId.
|
|
84078
|
+
* - Success (hasErrors === false): each result is paired with its action by
|
|
84079
|
+
* index and forwarded to actionCompleted in order.
|
|
84080
|
+
* - Server-reported failure (hasErrors === true): exactly one result carries
|
|
84081
|
+
* the real errorCode; the innocent siblings carry a non-attributive
|
|
84082
|
+
* rollback code (ALL_OR_NONE_OPERATION_ROLLED_BACK or PROCESSING_HALTED).
|
|
84083
|
+
* The batch rolled back, so a retryable offender parks EVERY member
|
|
84084
|
+
* together (C4/R4-3 — no self-driven prefix/suffix retry) charging a retry
|
|
84085
|
+
* attempt only to the offender, and a deterministic offender is errored
|
|
84086
|
+
* terminally while innocent siblings are left Pending (B5); returns
|
|
84087
|
+
* ACTION_ERRORED (journal cleared, replay-safe).
|
|
84088
|
+
* - Commit-ambiguous transport failure (network throw, empty-body or
|
|
84089
|
+
* misaligned 2xx, result count/id mismatch, or a retryable 5xx/408/dropped-
|
|
84090
|
+
* connection or generic-400 envelope that may have committed with the 2xx
|
|
84091
|
+
* lost): return NETWORK_ERROR WITHOUT retrying. The batch envelope carries
|
|
84092
|
+
* no server-visible idempotency key, so it cannot be safely re-sent; the
|
|
84093
|
+
* queue surfaces the members for manual resolution and retains/clears the
|
|
84094
|
+
* B2 journal instead of auto-replaying them (Option A / R4-4).
|
|
84095
|
+
*/
|
|
84096
|
+
async handleBatchActions(actions, actionCompleted, actionErrored) {
|
|
84097
|
+
return this.executeBatch(actions, actionCompleted, actionErrored);
|
|
84098
|
+
}
|
|
84099
|
+
async executeBatch(actions, actionCompleted, actionErrored) {
|
|
84100
|
+
if (actions.length === 0) {
|
|
84101
|
+
return ProcessActionResult.NO_ACTION_TO_PROCESS;
|
|
84102
|
+
}
|
|
84103
|
+
try {
|
|
84104
|
+
const operations = this.buildBatchOperations(actions);
|
|
84105
|
+
batchLog(`sending size=${actions.length}`);
|
|
84106
|
+
const batchRequest = {
|
|
84107
|
+
baseUri: BATCH_BASE_URI,
|
|
84108
|
+
basePath: BATCH_BASE_PATH,
|
|
84109
|
+
method: 'post',
|
|
84110
|
+
body: { allOrNone: true, operations },
|
|
84111
|
+
urlParams: {},
|
|
84112
|
+
queryParams: {},
|
|
84113
|
+
headers: {},
|
|
84114
|
+
priority: 'normal',
|
|
84115
|
+
};
|
|
84116
|
+
// Attribute the batch request to the head action's observability
|
|
84117
|
+
// context. A batch collapses N actions into one request, so there is
|
|
84118
|
+
// no per-member correlation; the head action's context is used as a
|
|
84119
|
+
// representative, mirroring the single-action dispatch path in
|
|
84120
|
+
// handleAction. (If members carried differing contexts, only the
|
|
84121
|
+
// head's is reported.)
|
|
84122
|
+
const dispatchOptions = actions[0].observabilityContext !== undefined
|
|
84123
|
+
? {
|
|
84124
|
+
requestCorrelator: {
|
|
84125
|
+
observabilityContext: actions[0].observabilityContext,
|
|
84126
|
+
},
|
|
84127
|
+
}
|
|
84128
|
+
: {};
|
|
84129
|
+
const response = await this.networkAdapter(batchRequest, dispatchOptions);
|
|
84130
|
+
if (!response.ok) {
|
|
84131
|
+
// Classify the envelope with the SAME body-aware policy the
|
|
84132
|
+
// single-action path uses (classifyBatchFailure) instead of a
|
|
84133
|
+
// status-only check. Three outcomes, routing EVERY member (not just
|
|
84134
|
+
// the head — C4):
|
|
84135
|
+
// - Commit-AMBIGUOUS (5xx/408/dropped connection, or a generic
|
|
84136
|
+
// gateway 400): may have committed at the origin with the 2xx
|
|
84137
|
+
// lost, and the batch carries no server-visible idempotency key,
|
|
84138
|
+
// so a keyless re-send could double-apply. Return NETWORK_ERROR
|
|
84139
|
+
// WITHOUT retrying; the queue surfaces the members for manual
|
|
84140
|
+
// resolution and retains/clears the journal instead of
|
|
84141
|
+
// auto-replaying them (Option A / R4-4).
|
|
84142
|
+
// - Definitively rolled back but retryable (429 rate-limit,
|
|
84143
|
+
// backdating collision the server evaluated): committed nothing,
|
|
84144
|
+
// so a keyless re-batch is replay-safe — park every member for a
|
|
84145
|
+
// bounded, backed-off retry (capped, honoring Retry-After).
|
|
84146
|
+
// - Deterministic server-generated client error: terminal, so a
|
|
84147
|
+
// rejected batch is not retried forever (C3/B4).
|
|
84148
|
+
const decision = this.classifyBatchFailure(response, actions[0]);
|
|
84149
|
+
batchLog(`http error status=${response.status ?? 0} retry=${decision.shouldRetry}` +
|
|
84150
|
+
` ambiguous=${decision.commitAmbiguous} size=${actions.length}`);
|
|
84151
|
+
if (decision.shouldRetry) {
|
|
84152
|
+
if (decision.commitAmbiguous) {
|
|
84153
|
+
return ProcessActionResult.NETWORK_ERROR;
|
|
84154
|
+
}
|
|
84155
|
+
await this.retryBatchMembers(actions, response, actionErrored, decision.retryDelayInMs);
|
|
84156
|
+
}
|
|
84157
|
+
else {
|
|
84158
|
+
await this.terminalBatchMembers(actions, response, actionErrored);
|
|
84159
|
+
}
|
|
84160
|
+
return ProcessActionResult.ACTION_ERRORED;
|
|
84161
|
+
}
|
|
84162
|
+
if (response.body === undefined || response.body === null) {
|
|
84163
|
+
// 2xx with no body — a misaligned/empty response. We got a success
|
|
84164
|
+
// status but cannot confirm what committed, so this is
|
|
84165
|
+
// commit-AMBIGUOUS: return NETWORK_ERROR so the queue surfaces the
|
|
84166
|
+
// members for manual resolution and retains/clears the journal,
|
|
84167
|
+
// rather than auto-replaying a keyless batch that may have committed
|
|
84168
|
+
// (Option A / R4-4).
|
|
84169
|
+
batchLog(`empty body ambiguous size=${actions.length}`);
|
|
84170
|
+
return ProcessActionResult.NETWORK_ERROR;
|
|
84171
|
+
}
|
|
84172
|
+
const body = response.body;
|
|
84173
|
+
// Both branches below pair body.results to actions positionally.
|
|
84174
|
+
// The UI-API batch endpoint supplies no per-result correlation key
|
|
84175
|
+
// (no client referenceId, and DELETE/error results carry no id) and
|
|
84176
|
+
// does not contractually guarantee results order matches operations
|
|
84177
|
+
// order, so a short/over-length response would silently mis-pair
|
|
84178
|
+
// completions with the wrong drafts. Refuse to pair positionally
|
|
84179
|
+
// unless the counts match exactly; on mismatch complete nothing. This is
|
|
84180
|
+
// commit-AMBIGUOUS (2xx received, response untrustworthy): return
|
|
84181
|
+
// NETWORK_ERROR so the queue surfaces the members for manual resolution
|
|
84182
|
+
// and retains/clears the journal (Option A / R4-4).
|
|
84183
|
+
if (!isArray$3$1(body.results) || body.results.length !== actions.length) {
|
|
84184
|
+
batchLog(`result count mismatch expected=${actions.length}` +
|
|
84185
|
+
` got=${isArray$3$1(body.results) ? body.results.length : 'none'}` +
|
|
84186
|
+
` ambiguous`);
|
|
84187
|
+
return ProcessActionResult.NETWORK_ERROR;
|
|
84188
|
+
}
|
|
84189
|
+
if (body.hasErrors === false) {
|
|
84190
|
+
// Strict success envelope (R4-4): the endpoint claims the batch
|
|
84191
|
+
// succeeded, but we only complete (and remove) a draft when the
|
|
84192
|
+
// member's OWN result confirms it committed — otherwise it is
|
|
84193
|
+
// commit-AMBIGUOUS: return NETWORK_ERROR so the queue surfaces the
|
|
84194
|
+
// members for manual resolution and retains/clears the journal,
|
|
84195
|
+
// rather than silently dropping a write that may never have landed.
|
|
84196
|
+
// (CREATE is never batched; DELETE carries no body, so only its 2xx
|
|
84197
|
+
// status is required — id echo is UPDATE-only.)
|
|
84198
|
+
for (let i = 0; i < actions.length; i++) {
|
|
84199
|
+
const action = actions[i];
|
|
84200
|
+
const method = action.data.method;
|
|
84201
|
+
const result = body.results[i];
|
|
84202
|
+
const childStatus = result?.statusCode;
|
|
84203
|
+
if (childStatus === undefined ||
|
|
84204
|
+
childStatus < HttpStatusCode$2.Ok ||
|
|
84205
|
+
childStatus >= 300) {
|
|
84206
|
+
batchLog(`success envelope unconfirmed child index=${i}` +
|
|
84207
|
+
` status=${childStatus ?? 'none'} ambiguous`);
|
|
84208
|
+
return ProcessActionResult.NETWORK_ERROR;
|
|
84209
|
+
}
|
|
84210
|
+
if (method === 'patch') {
|
|
84211
|
+
const resolved = this.resolveResourceRequest(action.data);
|
|
84212
|
+
const expectedId = resolved.urlParams['recordId'];
|
|
84213
|
+
const returnedId = !isArray$3$1(result.result)
|
|
84214
|
+
? result.result.id
|
|
84215
|
+
: undefined;
|
|
84216
|
+
// UPDATE must echo a matching record id; a missing or
|
|
84217
|
+
// mismatched id is commit-AMBIGUOUS.
|
|
84218
|
+
if (returnedId === undefined || returnedId !== expectedId) {
|
|
84219
|
+
// Log the reason code + whether an id came back, NOT the
|
|
84220
|
+
// ids themselves: expectedId/returnedId are record ids,
|
|
84221
|
+
// and the device-log PII policy (metrics.ts) forbids
|
|
84222
|
+
// record ids in device logs (4065667273).
|
|
84223
|
+
batchLog(`success envelope bad update id index=${i}` +
|
|
84224
|
+
` returned=${returnedId === undefined ? 'none' : 'mismatch'} ambiguous`);
|
|
84225
|
+
return ProcessActionResult.NETWORK_ERROR;
|
|
84226
|
+
}
|
|
84227
|
+
}
|
|
84228
|
+
}
|
|
84229
|
+
batchLog(`completed size=${actions.length}`);
|
|
84230
|
+
for (let i = 0; i < actions.length; i++) {
|
|
84231
|
+
const action = actions[i];
|
|
84232
|
+
const result = body.results[i];
|
|
84233
|
+
const recordBody = result !== undefined && !isArray$3$1(result.result)
|
|
84234
|
+
? result.result
|
|
84235
|
+
: undefined;
|
|
84236
|
+
const perActionResponse = {
|
|
84237
|
+
status: (result?.statusCode ?? HttpStatusCode$2.Ok),
|
|
84238
|
+
statusText: 'OK',
|
|
84239
|
+
ok: true,
|
|
84240
|
+
headers: {},
|
|
84241
|
+
body: recordBody,
|
|
84242
|
+
};
|
|
84243
|
+
await actionCompleted({
|
|
84244
|
+
...action,
|
|
84245
|
+
response: perActionResponse,
|
|
84246
|
+
status: DraftActionStatus.Completed,
|
|
84247
|
+
});
|
|
84248
|
+
}
|
|
84249
|
+
return ProcessActionResult.ACTION_SUCCEEDED;
|
|
84250
|
+
}
|
|
84251
|
+
// hasErrors is neither false (clean success, handled above) nor true
|
|
84252
|
+
// (attributable failure, handled below): the envelope is incomplete
|
|
84253
|
+
// or malformed — we can neither confirm a commit nor attribute a
|
|
84254
|
+
// failure, so treat it as commit-AMBIGUOUS (return NETWORK_ERROR, as
|
|
84255
|
+
// above).
|
|
84256
|
+
if (body.hasErrors !== true) {
|
|
84257
|
+
batchLog(`success envelope missing hasErrors flag ambiguous`);
|
|
84258
|
+
return ProcessActionResult.NETWORK_ERROR;
|
|
84259
|
+
}
|
|
84260
|
+
// Locate the action whose result is the real failure. Under
|
|
84261
|
+
// allOrNone=true the server rolls the whole batch back and marks the
|
|
84262
|
+
// innocent siblings with a non-attributive code (B3): the canonical
|
|
84263
|
+
// all-or-none marker ALL_OR_NONE_OPERATION_ROLLED_BACK, or
|
|
84264
|
+
// PROCESSING_HALTED for operations skipped before they ran. The true
|
|
84265
|
+
// offender is the member whose errorCode is neither — matching only
|
|
84266
|
+
// PROCESSING_HALTED would mis-pick an innocent rolled-back sibling as
|
|
84267
|
+
// the offender and leave the real invalid action Pending forever.
|
|
84268
|
+
let offendingIndex = -1;
|
|
84269
|
+
let offendingResult;
|
|
84270
|
+
for (let i = 0; i < body.results.length; i++) {
|
|
84271
|
+
const r = body.results[i];
|
|
84272
|
+
if (r === undefined)
|
|
84273
|
+
continue;
|
|
84274
|
+
if (!isArray$3$1(r.result))
|
|
84275
|
+
continue;
|
|
84276
|
+
const code = r.result[0]?.errorCode;
|
|
84277
|
+
if (code !== undefined && !ROLLBACK_SIBLING_ERROR_CODES.has(code)) {
|
|
84278
|
+
offendingIndex = i;
|
|
84279
|
+
offendingResult = r;
|
|
84280
|
+
break;
|
|
84281
|
+
}
|
|
84282
|
+
}
|
|
84283
|
+
// Unattributed: every failing result carries only a non-attributive
|
|
84284
|
+
// rollback-sibling code with no clear offender. Treat as a transient
|
|
84285
|
+
// server error — retry the whole batch.
|
|
84286
|
+
if (offendingIndex < 0 || offendingResult === undefined) {
|
|
84287
|
+
batchLog(`hasErrors size=${actions.length} unattributed retry=true`);
|
|
84288
|
+
await this.retryBatchMembers(actions, response, actionErrored);
|
|
84289
|
+
return ProcessActionResult.ACTION_ERRORED;
|
|
84290
|
+
}
|
|
84291
|
+
const offendingAction = actions[offendingIndex];
|
|
84292
|
+
const errorBody = offendingResult?.result ?? [];
|
|
84293
|
+
const errorResponse = {
|
|
84294
|
+
status: (offendingResult?.statusCode ??
|
|
84295
|
+
HttpStatusCode$2.BadRequest),
|
|
84296
|
+
statusText: 'Bad Request',
|
|
84297
|
+
ok: false,
|
|
84298
|
+
headers: {},
|
|
84299
|
+
body: errorBody,
|
|
84300
|
+
};
|
|
84301
|
+
const offendingErrorCode = isArray$3$1(offendingResult?.result)
|
|
84302
|
+
? offendingResult.result[0]?.errorCode ?? 'unknown'
|
|
84303
|
+
: 'unknown';
|
|
84304
|
+
// Classify the offender, reusing the single-action retry/terminal
|
|
84305
|
+
// policy (B4): a transient/ambiguous failure retries through the
|
|
84306
|
+
// durable cap, a deterministic server-generated client error is
|
|
84307
|
+
// terminal so the user can fix or remove it. classifyBatchFailure may
|
|
84308
|
+
// mutate offendingAction (e.g. a backdating bump) and reports
|
|
84309
|
+
// actionDataChanged so the mutation is persisted before the retry.
|
|
84310
|
+
// The batch endpoint returns each member's failure as a UiApiError[]
|
|
84311
|
+
// (the offender loop above keys on r.result[0].errorCode). The shared
|
|
84312
|
+
// classifiers reused by classifyBatchFailure — isBackdatingError /
|
|
84313
|
+
// isServerGeneratedError — and the single-action handleAction path
|
|
84314
|
+
// expect the origin's structured enhancedErrorType envelope, where a
|
|
84315
|
+
// backdating collision surfaces as
|
|
84316
|
+
// { enhancedErrorType: 'RecordError', output: { errors: [...] } }.
|
|
84317
|
+
// Left as a bare array, isBackdatingError never matches, so a batched
|
|
84318
|
+
// LastModifiedDate CollisionDetectedException would be errored
|
|
84319
|
+
// terminally instead of getting the single-action backdating bump +
|
|
84320
|
+
// key-rotate + retry (B4). Wrap the member's error array in that
|
|
84321
|
+
// envelope for CLASSIFICATION ONLY. This is behavior-preserving for
|
|
84322
|
+
// every non-collision offender: isServerGeneratedError is already
|
|
84323
|
+
// true for a UiApiError[] offender, so the terminal decision is
|
|
84324
|
+
// unchanged; only a genuine collision (which also requires the action
|
|
84325
|
+
// to carry a LastModifiedDate field) now recovers. The error surfaced
|
|
84326
|
+
// to the draft (errorResponse) keeps the raw array — unchanged.
|
|
84327
|
+
const classificationResponse = {
|
|
84328
|
+
...errorResponse,
|
|
84329
|
+
body: {
|
|
84330
|
+
enhancedErrorType: 'RecordError',
|
|
84331
|
+
output: {
|
|
84332
|
+
errors: isArray$3$1(offendingResult?.result) ? offendingResult.result : [],
|
|
84333
|
+
},
|
|
84334
|
+
},
|
|
84335
|
+
};
|
|
84336
|
+
const decision = this.classifyBatchFailure(classificationResponse, offendingAction);
|
|
84337
|
+
batchLog(`failed size=${actions.length} offendingIndex=${offendingIndex}` +
|
|
84338
|
+
` errorCode=${offendingErrorCode} retry=${decision.shouldRetry}`);
|
|
84339
|
+
if (decision.shouldRetry) {
|
|
84340
|
+
// Transient/ambiguous offender: the whole batch rolled back and
|
|
84341
|
+
// NONE committed. Parking only the offender and leaving the innocent
|
|
84342
|
+
// siblings Pending would let the queue immediately re-collect them
|
|
84343
|
+
// into a fresh, backoff-free batch — and if that batch's new head
|
|
84344
|
+
// draws the same transient response, [A,B,C] retries as [B,C] then
|
|
84345
|
+
// [C], up to 25 + 24 + ... + 1 = 325 operations before the first
|
|
84346
|
+
// backoff expires (R4-3). Park EVERY member together so the whole
|
|
84347
|
+
// batch backs off — but charge a durable retry attempt ONLY to the
|
|
84348
|
+
// offender (via retryOrCap). The innocent rolled-back siblings
|
|
84349
|
+
// produced no error of their own, so they are parked for the same
|
|
84350
|
+
// backoff WITHOUT consuming their retry budget; charging them could
|
|
84351
|
+
// push an innocent later action to its cap for another action's
|
|
84352
|
+
// transient failure (#9, matching the terminal branch's B5
|
|
84353
|
+
// treatment). The offender's own cap still bounds the shared-batch
|
|
84354
|
+
// retry loop. Only the offender carries any classifier mutation (a
|
|
84355
|
+
// backdating bump), so it alone reports actionDataChanged to persist
|
|
84356
|
+
// that mutation before its retry.
|
|
84357
|
+
for (const member of actions) {
|
|
84358
|
+
if (member === offendingAction) {
|
|
84359
|
+
await this.retryOrCap(member, errorResponse, actionErrored, decision.retryDelayInMs, decision.actionDataChanged);
|
|
84360
|
+
}
|
|
84361
|
+
else {
|
|
84362
|
+
await actionErrored(member, true, decision.retryDelayInMs);
|
|
84363
|
+
}
|
|
84364
|
+
}
|
|
84365
|
+
}
|
|
84366
|
+
else {
|
|
84367
|
+
// Deterministic, server-generated offender: terminal. Route ONLY the
|
|
84368
|
+
// offender and leave the innocent rolled-back siblings Pending and
|
|
84369
|
+
// unparked — reporting nothing for them (B5). Charging a sibling a
|
|
84370
|
+
// durable retry attempt for another action's terminal failure could
|
|
84371
|
+
// push an innocent later action to its cap without it ever producing
|
|
84372
|
+
// its own error. The queue re-collects the siblings on the next pass
|
|
84373
|
+
// (bounded: each pass terminally resolves one offender, so there is
|
|
84374
|
+
// no zero-delay storm), and it owns stop state / backoff / retry
|
|
84375
|
+
// budgets — the handler no longer self-drives prefix/suffix retries
|
|
84376
|
+
// (that recursion bypassed stopQueue and could launch a new request
|
|
84377
|
+
// while the original was in flight).
|
|
84378
|
+
await actionErrored({
|
|
84379
|
+
...offendingAction,
|
|
84380
|
+
error: errorResponse,
|
|
84381
|
+
status: DraftActionStatus.Error,
|
|
84382
|
+
}, false);
|
|
84383
|
+
}
|
|
84384
|
+
return ProcessActionResult.ACTION_ERRORED;
|
|
84385
|
+
}
|
|
84386
|
+
catch (e) {
|
|
84387
|
+
batchLog(`network error ambiguous size=${actions.length}`);
|
|
84388
|
+
const failure = normalizeError$1(e);
|
|
84389
|
+
// A throw here means the request was dispatched but no response was
|
|
84390
|
+
// received — the all-or-none batch may have committed at the origin
|
|
84391
|
+
// with the response lost, and the envelope carries no server-visible
|
|
84392
|
+
// idempotency key. This is commit-AMBIGUOUS: surface the failure to o11y
|
|
84393
|
+
// (the single-action handleAction catch reports every upload error, and
|
|
84394
|
+
// the batch path swallowed them until now — B4; attributed to the head
|
|
84395
|
+
// as a representative, matching the dispatch correlation above), then
|
|
84396
|
+
// return NETWORK_ERROR so the queue surfaces the members for manual
|
|
84397
|
+
// resolution instead of auto-replaying them (Option A).
|
|
84398
|
+
this.reportUploadFailure(failure, actions[0]);
|
|
84399
|
+
return ProcessActionResult.NETWORK_ERROR;
|
|
84400
|
+
}
|
|
84401
|
+
}
|
|
84402
|
+
/**
|
|
84403
|
+
* Retries every given batch member through the durable retry cap
|
|
84404
|
+
* (retryOrCap). Used for whole-batch transient failures (network throw,
|
|
84405
|
+
* misaligned/short/empty response, unattributed rollback, retryable envelope
|
|
84406
|
+
* status) and for the surviving members of a partial failure, so that (a)
|
|
84407
|
+
* every member is parked with backoff before the queue re-arms — not just
|
|
84408
|
+
* the head (C4) — and (b) the durable attempt cap bounds the retries instead
|
|
84409
|
+
* of looping forever (C3). When the cap gate is closed, retryOrCap is a
|
|
84410
|
+
* passthrough that parks each member exactly as before (retry=true).
|
|
84411
|
+
*/
|
|
84412
|
+
async retryBatchMembers(members, lastFailure, actionErrored, retryDelayInMs) {
|
|
84413
|
+
for (const member of members) {
|
|
84414
|
+
await this.retryOrCap(member, lastFailure, actionErrored, retryDelayInMs);
|
|
84415
|
+
}
|
|
84416
|
+
}
|
|
84417
|
+
/**
|
|
84418
|
+
* Terminally errors every given batch member (no retry). Used when the batch
|
|
84419
|
+
* envelope is rejected with a deterministic client error (4xx other than
|
|
84420
|
+
* 408/429): retrying an unchanged, server-rejected batch would only be
|
|
84421
|
+
* rejected again, so the drafts are errored for the user to fix or discard —
|
|
84422
|
+
* consistent with the single-action response policy — instead of being
|
|
84423
|
+
* retried forever (C3).
|
|
84424
|
+
*/
|
|
84425
|
+
async terminalBatchMembers(members, error, actionErrored) {
|
|
84426
|
+
for (const member of members) {
|
|
84427
|
+
await actionErrored({ ...member, error, status: DraftActionStatus.Error }, false);
|
|
84428
|
+
}
|
|
84429
|
+
}
|
|
84430
|
+
/**
|
|
84431
|
+
* Maps each draft action to a UI-API records/batch operation. Each op has
|
|
84432
|
+
* exactly one record entry; the order of returned ops matches the order of
|
|
84433
|
+
* input actions so per-action result routing works on index alone.
|
|
84434
|
+
*
|
|
84435
|
+
* - method='post' -> CREATE with apiName + fields
|
|
84436
|
+
* - method='patch' -> UPDATE with fields including Id (Id pulled from
|
|
84437
|
+
* urlParams.recordId)
|
|
84438
|
+
* - method='delete' -> DELETE with fields containing only Id
|
|
84439
|
+
*
|
|
84440
|
+
* Each request is re-resolved through resolveResourceRequest first. The
|
|
84441
|
+
* draft-id redirect rewrite that runs when an upstream POST completes only
|
|
84442
|
+
* updates a queued action's basePath/body — NOT its urlParams — so a stored
|
|
84443
|
+
* PATCH/DELETE can still carry a stale draft id in urlParams.recordId at
|
|
84444
|
+
* drain time. resolveResourceRequest re-canonicalizes urlParams.recordId
|
|
84445
|
+
* (and body fields) against the store, and is idempotent when the id is
|
|
84446
|
+
* already canonical, so it is safe to call unconditionally here.
|
|
84447
|
+
*/
|
|
84448
|
+
buildBatchOperations(actions) {
|
|
84449
|
+
const operations = [];
|
|
84450
|
+
for (const action of actions) {
|
|
84451
|
+
const request = this.resolveResourceRequest(action.data);
|
|
84452
|
+
const method = request.method;
|
|
84453
|
+
if (method === 'post') {
|
|
84454
|
+
const body = request.body;
|
|
84455
|
+
operations.push({
|
|
84456
|
+
type: 'CREATE',
|
|
84457
|
+
records: [
|
|
84458
|
+
{
|
|
84459
|
+
apiName: body.apiName,
|
|
84460
|
+
fields: { ...body.fields },
|
|
84461
|
+
},
|
|
84462
|
+
],
|
|
84463
|
+
});
|
|
84464
|
+
}
|
|
84465
|
+
else if (method === 'patch') {
|
|
84466
|
+
const recordId = request.urlParams['recordId'];
|
|
84467
|
+
const body = request.body;
|
|
84468
|
+
operations.push({
|
|
84469
|
+
type: 'UPDATE',
|
|
84470
|
+
records: [
|
|
84471
|
+
{
|
|
84472
|
+
fields: { Id: recordId, ...body.fields },
|
|
84473
|
+
},
|
|
84474
|
+
],
|
|
84475
|
+
});
|
|
84476
|
+
}
|
|
84477
|
+
else if (method === 'delete') {
|
|
84478
|
+
const recordId = request.urlParams['recordId'];
|
|
84479
|
+
operations.push({
|
|
84480
|
+
type: 'DELETE',
|
|
84481
|
+
records: [
|
|
84482
|
+
{
|
|
84483
|
+
fields: { Id: recordId },
|
|
84484
|
+
},
|
|
84485
|
+
],
|
|
84486
|
+
});
|
|
84487
|
+
}
|
|
84488
|
+
else {
|
|
84489
|
+
throw Error(`Unsupported method '${method}' in UiApiActionHandler.buildBatchOperations`);
|
|
84490
|
+
}
|
|
84491
|
+
}
|
|
84492
|
+
return operations;
|
|
84493
|
+
}
|
|
81662
84494
|
async draftActionToSideEffects(action) {
|
|
81663
84495
|
const method = action.data.method;
|
|
81664
84496
|
const apiName = action.metadata[LDS_ACTION_METADATA_API_NAME];
|
|
@@ -82141,6 +84973,12 @@ class UpdateRecordQuickActionExecutionRepresentationHandler extends AbstractQuic
|
|
|
82141
84973
|
const CONTENT_DOCUMENT_DRAFT_ID_KEY = 'CONTENT_DOCUMENT_DRAFT_ID';
|
|
82142
84974
|
const CONTENT_VERSION_DRAFT_ID_KEY = 'CONTENT_VERSION_DRAFT_ID';
|
|
82143
84975
|
const CONTENT_DOCUMENT_LINK_DRAFT_ID_KEY = 'CONTENT_DOCUMENT_LINK_DRAFT_ID';
|
|
84976
|
+
/**
|
|
84977
|
+
* Metadata key carrying the LDS binary store URI for the upload's local file.
|
|
84978
|
+
* Set in `buildPendingAction` so the Swift "Pending Images" UI can render
|
|
84979
|
+
* a thumbnail from the local bytes without a JSC round-trip.
|
|
84980
|
+
*/
|
|
84981
|
+
const LDS_BINARY_HANDLE_KEY = 'LDS_BINARY_HANDLE';
|
|
82144
84982
|
const RESPONSE_CONTENT_DOCUMENT_FIELDS = [
|
|
82145
84983
|
'ContentSize',
|
|
82146
84984
|
'CreatedDate',
|
|
@@ -82392,6 +85230,15 @@ class ContentDocumentCompositeRepresentationActionHandler extends AbstractResour
|
|
|
82392
85230
|
return Promise.reject(new DraftSynthesisError('Cannot generate local ContentDocumentLink synthetic id', 'CANNOT_GENERATE_ID'));
|
|
82393
85231
|
});
|
|
82394
85232
|
pendingAction.metadata[CONTENT_DOCUMENT_LINK_DRAFT_ID_KEY] = contentDocumentLinkId;
|
|
85233
|
+
// Stash the LDS binary store URI so the Swift "Pending Images" section
|
|
85234
|
+
// can render a thumbnail directly from the local file bytes.
|
|
85235
|
+
try {
|
|
85236
|
+
const fileRef = getFileReferenceFromResourceRequest(request);
|
|
85237
|
+
pendingAction.metadata[LDS_BINARY_HANDLE_KEY] = fileRef.handle;
|
|
85238
|
+
}
|
|
85239
|
+
catch {
|
|
85240
|
+
// No file data — leave metadata unset; UI falls back to placeholder.
|
|
85241
|
+
}
|
|
82395
85242
|
const objectInfo = await this.objectInfoService.getObjectInfo(CONTENT_VERSION_API_NAME);
|
|
82396
85243
|
if (objectInfo === undefined) {
|
|
82397
85244
|
throw Error(`Could not generate draft. Object info is missing`);
|
|
@@ -82994,7 +85841,18 @@ function recordLoaderFactory(query) {
|
|
|
82994
85841
|
return ids.map((id) => recordMap.get(id) || null);
|
|
82995
85842
|
}
|
|
82996
85843
|
}
|
|
82997
|
-
|
|
85844
|
+
const loader = new DataLoader(batchRecordQuery);
|
|
85845
|
+
const loads = new Map();
|
|
85846
|
+
return Object.assign(loader, {
|
|
85847
|
+
prefetch(id) {
|
|
85848
|
+
const existing = loads.get(id);
|
|
85849
|
+
if (existing)
|
|
85850
|
+
return existing;
|
|
85851
|
+
const load = loader.load(id).then((record) => ({ record }), (error) => ({ error }));
|
|
85852
|
+
loads.set(id, load);
|
|
85853
|
+
return load;
|
|
85854
|
+
},
|
|
85855
|
+
});
|
|
82998
85856
|
}
|
|
82999
85857
|
|
|
83000
85858
|
function createContext(store, objectInfos, eventEmitter, settings, snapshot, mappedCursors = new Map(), draftFunctions) {
|
|
@@ -83028,6 +85886,8 @@ function createContext(store, objectInfos, eventEmitter, settings, snapshot, map
|
|
|
83028
85886
|
draftFunctions,
|
|
83029
85887
|
mappedCursors,
|
|
83030
85888
|
missingFieldsMap: new Map(),
|
|
85889
|
+
parentPrefetchEnabled: !parentRelationshipPrefetchKillSwitch.isOpen({ fallback: false }),
|
|
85890
|
+
parentResults: new Map(),
|
|
83031
85891
|
};
|
|
83032
85892
|
}
|
|
83033
85893
|
|
|
@@ -83075,47 +85935,41 @@ var DateLiteral;
|
|
|
83075
85935
|
DateLiteral["YESTERDAY"] = "YESTERDAY";
|
|
83076
85936
|
DateLiteral["NEXT_WEEK"] = "NEXT_WEEK";
|
|
83077
85937
|
})(DateLiteral || (DateLiteral = {}));
|
|
83078
|
-
// ranged adjusted to be between now and the end of the day,
|
|
83079
|
-
// you cannot do a straight = comparison in SQL
|
|
83080
85938
|
const TODAY_RANGE = {
|
|
83081
85939
|
lowerBound: `date('now')`,
|
|
83082
|
-
upperBound: `
|
|
85940
|
+
upperBound: `datetime('now', 'start of day', '+1 day', '-1 seconds')`,
|
|
83083
85941
|
};
|
|
83084
|
-
// ranged adjusted to be between today +1 day and +2 days -.001 seconds to be the very end of tomorrows date
|
|
83085
|
-
// you cannot do a straight = comparison in SQL
|
|
83086
85942
|
const TOMORROW_RANGE = {
|
|
83087
85943
|
lowerBound: `date('now', '+1 day')`,
|
|
83088
|
-
upperBound: `
|
|
85944
|
+
upperBound: `datetime('now', 'start of day', '+2 days', '-1 seconds')`,
|
|
83089
85945
|
};
|
|
83090
85946
|
const YESTERDAY_RANGE = {
|
|
83091
85947
|
lowerBound: `date('now', '-1 day')`,
|
|
83092
|
-
upperBound: `
|
|
85948
|
+
upperBound: `datetime('now', 'start of day', '-1 seconds')`,
|
|
83093
85949
|
};
|
|
83094
85950
|
const THIS_WEEK_RANGE = {
|
|
83095
|
-
lowerBound: `date('now', '
|
|
83096
|
-
upperBound: `
|
|
85951
|
+
lowerBound: `date('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}')`,
|
|
85952
|
+
upperBound: `datetime('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', '+7 days', '-1 seconds')`,
|
|
83097
85953
|
};
|
|
83098
85954
|
const LAST_WEEK_RANGE = {
|
|
83099
|
-
lowerBound: `date('now', 'weekday
|
|
83100
|
-
upperBound: `
|
|
85955
|
+
lowerBound: `date('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', '-7 days')`,
|
|
85956
|
+
upperBound: `datetime('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', '-1 seconds')`,
|
|
83101
85957
|
};
|
|
83102
85958
|
const NEXT_WEEK_RANGE = {
|
|
83103
|
-
lowerBound: `date('now', 'weekday
|
|
83104
|
-
upperBound: `
|
|
85959
|
+
lowerBound: `date('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', '+7 days')`,
|
|
85960
|
+
upperBound: `datetime('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', '+14 days', '-1 seconds')`,
|
|
83105
85961
|
};
|
|
83106
85962
|
const THIS_MONTH_RANGE = {
|
|
83107
85963
|
lowerBound: `date('now', 'start of month')`,
|
|
83108
|
-
upperBound: `
|
|
85964
|
+
upperBound: `datetime('now', 'start of month', '+1 month', '-1 seconds')`,
|
|
83109
85965
|
};
|
|
83110
85966
|
const LAST_MONTH_RANGE = {
|
|
83111
85967
|
lowerBound: `date('now', 'start of month', '-1 month')`,
|
|
83112
|
-
upperBound: `
|
|
85968
|
+
upperBound: `datetime('now', 'start of month', '-1 seconds')`,
|
|
83113
85969
|
};
|
|
83114
85970
|
const NEXT_MONTH_RANGE = {
|
|
83115
85971
|
lowerBound: `date('now', 'start of month', '+1 month')`,
|
|
83116
|
-
|
|
83117
|
-
// this needs to be 0 day
|
|
83118
|
-
upperBound: `date('now', 'start of month', '+2 month', '0 day')`,
|
|
85972
|
+
upperBound: `datetime('now', 'start of month', '+2 months', '-1 seconds')`,
|
|
83119
85973
|
};
|
|
83120
85974
|
const THIS_QUARTER_RANGE = () => {
|
|
83121
85975
|
return computeQuarterDateRange(DateLiteral.THIS_QUARTER);
|
|
@@ -83128,23 +85982,23 @@ const NEXT_QUARTER_RANGE = () => {
|
|
|
83128
85982
|
};
|
|
83129
85983
|
const THIS_YEAR_RANGE = {
|
|
83130
85984
|
lowerBound: `date('now', 'start of year')`,
|
|
83131
|
-
upperBound: `
|
|
85985
|
+
upperBound: `datetime('now', 'start of year', '+1 year', '-1 seconds')`,
|
|
83132
85986
|
};
|
|
83133
85987
|
const LAST_YEAR_RANGE = {
|
|
83134
85988
|
lowerBound: `date('now', 'start of year', '-1 year')`,
|
|
83135
|
-
upperBound: `
|
|
85989
|
+
upperBound: `datetime('now', 'start of year', '-1 seconds')`,
|
|
83136
85990
|
};
|
|
83137
85991
|
const NEXT_YEAR_RANGE = {
|
|
83138
85992
|
lowerBound: `date('now', 'start of year', '+1 year')`,
|
|
83139
|
-
upperBound: `
|
|
85993
|
+
upperBound: `datetime('now', 'start of year', '+2 years', '-1 seconds')`,
|
|
83140
85994
|
};
|
|
83141
85995
|
const LAST_90_DAYS_RANGE = {
|
|
83142
85996
|
lowerBound: `date('now', '-90 days')`,
|
|
83143
|
-
upperBound: `
|
|
85997
|
+
upperBound: `datetime('now')`,
|
|
83144
85998
|
};
|
|
83145
85999
|
const NEXT_90_DAYS_RANGE = {
|
|
83146
86000
|
lowerBound: `date('now', '+1 day')`,
|
|
83147
|
-
upperBound: `
|
|
86001
|
+
upperBound: `datetime('now', 'start of day', '+91 days', '-1 seconds')`,
|
|
83148
86002
|
};
|
|
83149
86003
|
// querter start date in UTC
|
|
83150
86004
|
function quarterStart(date) {
|
|
@@ -83158,23 +86012,17 @@ function computeQuarterDateRange(enumType) {
|
|
|
83158
86012
|
case DateLiteral.LAST_QUARTER:
|
|
83159
86013
|
return {
|
|
83160
86014
|
lowerBound: `date('${curQuarterStart}', '-3 months')`,
|
|
83161
|
-
|
|
83162
|
-
// 0 days sets it to the end of the month
|
|
83163
|
-
upperBound: `date('${curQuarterStart}', '0 days')`,
|
|
86015
|
+
upperBound: `datetime('${curQuarterStart}', '-1 seconds')`,
|
|
83164
86016
|
};
|
|
83165
86017
|
case DateLiteral.THIS_QUARTER:
|
|
83166
86018
|
return {
|
|
83167
86019
|
lowerBound: `date('${curQuarterStart}')`,
|
|
83168
|
-
|
|
83169
|
-
// 0 days sets it to the end of the month
|
|
83170
|
-
upperBound: `date('${curQuarterStart}', '3 months', '0 days')`,
|
|
86020
|
+
upperBound: `datetime('${curQuarterStart}', '+3 months', '-1 seconds')`,
|
|
83171
86021
|
};
|
|
83172
86022
|
case DateLiteral.NEXT_QUARTER:
|
|
83173
86023
|
return {
|
|
83174
86024
|
lowerBound: `date('${curQuarterStart}', '+3 months')`,
|
|
83175
|
-
|
|
83176
|
-
// 0 days sets it to the end of the month
|
|
83177
|
-
upperBound: `date('${curQuarterStart}', '+6 months', '0 days')`,
|
|
86025
|
+
upperBound: `datetime('${curQuarterStart}', '+6 months', '-1 seconds')`,
|
|
83178
86026
|
};
|
|
83179
86027
|
}
|
|
83180
86028
|
}
|
|
@@ -83393,11 +86241,11 @@ function dateRangesFrom(dateRange, input, dateFunction) {
|
|
|
83393
86241
|
case DateRange.next_n_days: {
|
|
83394
86242
|
return {
|
|
83395
86243
|
binding: {
|
|
83396
|
-
upper: `${input} days`,
|
|
86244
|
+
upper: `${input + 1} days`,
|
|
83397
86245
|
lower: '1 days',
|
|
83398
86246
|
},
|
|
83399
86247
|
range: {
|
|
83400
|
-
upper: `${dateFunction}('now', 'start of day',
|
|
86248
|
+
upper: `${dateFunction}('now', 'start of day', ?, '-1 seconds')`,
|
|
83401
86249
|
lower: `${dateFunction}('now', 'start of day', ?)`,
|
|
83402
86250
|
},
|
|
83403
86251
|
};
|
|
@@ -83434,19 +86282,14 @@ function dateRangesFrom(dateRange, input, dateFunction) {
|
|
|
83434
86282
|
// starts at 00:00:00 the first day of the week n weeks after the current week
|
|
83435
86283
|
// ends 23:59:59 of the last day of week before the current
|
|
83436
86284
|
case DateRange.next_n_weeks: {
|
|
83437
|
-
const isStartOfWeek = isTodayStartOfWeek();
|
|
83438
86285
|
return {
|
|
83439
86286
|
binding: {
|
|
83440
|
-
|
|
83441
|
-
|
|
83442
|
-
// so for the upper bound if today is the start of the week we need to add 7 more days to the value to get next week as the start
|
|
83443
|
-
upper: `${input * 7 + (isStartOfWeek ? 7 : 0)} days`,
|
|
83444
|
-
//lower bound starts out 00:00:00 of next week
|
|
83445
|
-
lower: `${isStartOfWeek ? 7 : 0} days`,
|
|
86287
|
+
upper: `${(input + 1) * 7} days`,
|
|
86288
|
+
lower: '7 days',
|
|
83446
86289
|
},
|
|
83447
86290
|
range: {
|
|
83448
|
-
upper: `${dateFunction}('now', '
|
|
83449
|
-
lower: `${dateFunction}('now', '
|
|
86291
|
+
upper: `${dateFunction}('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', ?, '-1 seconds')`,
|
|
86292
|
+
lower: `${dateFunction}('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', ?)`,
|
|
83450
86293
|
},
|
|
83451
86294
|
};
|
|
83452
86295
|
}
|
|
@@ -83454,19 +86297,14 @@ function dateRangesFrom(dateRange, input, dateFunction) {
|
|
|
83454
86297
|
// starts at 00:00:00 of day 0 n weeks before the current week
|
|
83455
86298
|
// ends 23:59:59 at last day of the week before the current week
|
|
83456
86299
|
case DateRange.last_n_weeks: {
|
|
83457
|
-
const isStartOfWeek = isTodayStartOfWeek();
|
|
83458
86300
|
return {
|
|
83459
86301
|
binding: {
|
|
83460
|
-
|
|
83461
|
-
|
|
83462
|
-
upper: `${-(isStartOfWeek ? 0 : 7)} days`,
|
|
83463
|
-
// starting on 00:00:00 of n * weeks before current week
|
|
83464
|
-
// -7 more days if today not the start of the week because weekday FIRST_DAY_OF_WEEK puts us next week day 0
|
|
83465
|
-
lower: `${-input * 7 - (isStartOfWeek ? 0 : 7)} days`,
|
|
86302
|
+
upper: '0 days',
|
|
86303
|
+
lower: `${-input * 7} days`,
|
|
83466
86304
|
},
|
|
83467
86305
|
range: {
|
|
83468
|
-
upper: `${dateFunction}('now', '
|
|
83469
|
-
lower: `${dateFunction}('now', '
|
|
86306
|
+
upper: `${dateFunction}('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', ?, '-1 seconds')`,
|
|
86307
|
+
lower: `${dateFunction}('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', ?)`,
|
|
83470
86308
|
},
|
|
83471
86309
|
};
|
|
83472
86310
|
}
|
|
@@ -83474,21 +86312,14 @@ function dateRangesFrom(dateRange, input, dateFunction) {
|
|
|
83474
86312
|
// starts 00:00:00 on day 0 of n weeks before the current week
|
|
83475
86313
|
// ends 7 days after the start
|
|
83476
86314
|
case DateRange.n_weeks_ago: {
|
|
83477
|
-
const isStartOfWeek = isTodayStartOfWeek();
|
|
83478
86315
|
return {
|
|
83479
86316
|
binding: {
|
|
83480
|
-
|
|
83481
|
-
|
|
83482
|
-
// so add 7 to remove a week from -(n)
|
|
83483
|
-
upper: `${-input * 7 + (isStartOfWeek ? 7 : 0)} days`,
|
|
83484
|
-
// start of the week n weeks from the current week
|
|
83485
|
-
// if today is the start of the week then -(n) will do to be the previous week, but
|
|
83486
|
-
// if today is not the start of the week we need to go back 1 more week since weekday FIRST_DAY_OF_WEEK will put us next week
|
|
83487
|
-
lower: `${-input * 7 - (isStartOfWeek ? 0 : 7)} days`,
|
|
86317
|
+
upper: `${-input * 7 + 7} days`,
|
|
86318
|
+
lower: `${-input * 7} days`,
|
|
83488
86319
|
},
|
|
83489
86320
|
range: {
|
|
83490
|
-
upper: `${dateFunction}('now', '
|
|
83491
|
-
lower: `${dateFunction}('now', '
|
|
86321
|
+
upper: `${dateFunction}('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', ?, '-1 seconds')`,
|
|
86322
|
+
lower: `${dateFunction}('now', 'start of day', '-6 days', 'weekday ${FIRST_DAY_OF_WEEK}', ?)`,
|
|
83492
86323
|
},
|
|
83493
86324
|
};
|
|
83494
86325
|
}
|
|
@@ -83638,9 +86469,6 @@ function dateRangesFrom(dateRange, input, dateFunction) {
|
|
|
83638
86469
|
throw new Error(`DateRangeInput ${dateRange} is not supported in local evalutation`);
|
|
83639
86470
|
}
|
|
83640
86471
|
}
|
|
83641
|
-
function isTodayStartOfWeek() {
|
|
83642
|
-
return new Date().getDay() === FIRST_DAY_OF_WEEK;
|
|
83643
|
-
}
|
|
83644
86472
|
|
|
83645
86473
|
const JSON_EXTRACT_PATH_INGESTION_TIMESTAMP = '$.ingestionTimestamp';
|
|
83646
86474
|
const JSON_EXTRACT_PATH_INGESTION_APINAME = '$.apiName';
|
|
@@ -86207,6 +89035,207 @@ async function readPaginationMetadataForKeys(keys, query) {
|
|
|
86207
89035
|
return metadataMap;
|
|
86208
89036
|
}
|
|
86209
89037
|
|
|
89038
|
+
function getDirectiveBooleanValue(argument, variables) {
|
|
89039
|
+
if (!argument)
|
|
89040
|
+
return undefined;
|
|
89041
|
+
if (argument.value.kind === Kind$2.BOOLEAN)
|
|
89042
|
+
return argument.value.value;
|
|
89043
|
+
if (argument.value.kind === Kind$2.VARIABLE) {
|
|
89044
|
+
const value = variables[argument.value.name.value];
|
|
89045
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
89046
|
+
}
|
|
89047
|
+
return undefined;
|
|
89048
|
+
}
|
|
89049
|
+
function isSelectionIncluded(selection, variables) {
|
|
89050
|
+
for (const directive of selection.directives || []) {
|
|
89051
|
+
if (directive.name.value !== 'skip' && directive.name.value !== 'include')
|
|
89052
|
+
continue;
|
|
89053
|
+
const argument = (directive.arguments || []).find((argument) => argument.name.value === 'if');
|
|
89054
|
+
const value = getDirectiveBooleanValue(argument, variables);
|
|
89055
|
+
if (value === undefined)
|
|
89056
|
+
return false;
|
|
89057
|
+
if (directive.name.value === 'skip' && value === true)
|
|
89058
|
+
return false;
|
|
89059
|
+
if (directive.name.value === 'include' && value === false)
|
|
89060
|
+
return false;
|
|
89061
|
+
}
|
|
89062
|
+
return true;
|
|
89063
|
+
}
|
|
89064
|
+
function matchesTypeCondition(typeCondition, runtimeType, schema) {
|
|
89065
|
+
if (!typeCondition)
|
|
89066
|
+
return true;
|
|
89067
|
+
const conditionType = schema.getType(typeCondition.name.value);
|
|
89068
|
+
if (!conditionType || conditionType === runtimeType)
|
|
89069
|
+
return conditionType === runtimeType;
|
|
89070
|
+
const kind = conditionType.astNode?.kind;
|
|
89071
|
+
if (kind !== Kind$2.INTERFACE_TYPE_DEFINITION && kind !== Kind$2.UNION_TYPE_DEFINITION)
|
|
89072
|
+
return false;
|
|
89073
|
+
return schema.isSubType(conditionType, runtimeType);
|
|
89074
|
+
}
|
|
89075
|
+
function relationshipId(fieldName, record) {
|
|
89076
|
+
let id;
|
|
89077
|
+
if (fieldName === 'RecordType')
|
|
89078
|
+
id = record.recordTypeId;
|
|
89079
|
+
else if (fieldName.endsWith('__r')) {
|
|
89080
|
+
const relationshipField = record.fields[fieldName.replace('__r', '__c')];
|
|
89081
|
+
id = relationshipField && relationshipField.value;
|
|
89082
|
+
}
|
|
89083
|
+
else {
|
|
89084
|
+
const relationshipField = record.fields[fieldName + 'Id'];
|
|
89085
|
+
id = relationshipField && relationshipField.value;
|
|
89086
|
+
}
|
|
89087
|
+
return typeof id === 'string' && id ? id : null;
|
|
89088
|
+
}
|
|
89089
|
+
function getObjectFields(type) {
|
|
89090
|
+
return isObjectType(type) ? type.getFields() : {};
|
|
89091
|
+
}
|
|
89092
|
+
function getSelectedParentFields(selectionSet, runtimeType, info, visitedFragments) {
|
|
89093
|
+
if (!selectionSet)
|
|
89094
|
+
return [];
|
|
89095
|
+
const selectedParentFields = [];
|
|
89096
|
+
const variables = info.variableValues;
|
|
89097
|
+
for (const selection of selectionSet.selections) {
|
|
89098
|
+
if (!isSelectionIncluded(selection, variables))
|
|
89099
|
+
continue;
|
|
89100
|
+
if (selection.kind === Kind$2.FIELD) {
|
|
89101
|
+
const field = getObjectFields(runtimeType)[selection.name.value];
|
|
89102
|
+
const fieldType = field && getNamedType(field.type);
|
|
89103
|
+
const isParent = fieldType &&
|
|
89104
|
+
((isObjectType(fieldType) &&
|
|
89105
|
+
fieldType.getInterfaces().some((iface) => iface.name === 'Record')) ||
|
|
89106
|
+
fieldType.name === 'Record');
|
|
89107
|
+
if (isParent &&
|
|
89108
|
+
selection.selectionSet &&
|
|
89109
|
+
!(isObjectType(fieldType) && fieldType.name.endsWith('Connection'))) {
|
|
89110
|
+
selectedParentFields.push({
|
|
89111
|
+
name: selection.name.value,
|
|
89112
|
+
selectionSet: selection.selectionSet,
|
|
89113
|
+
});
|
|
89114
|
+
}
|
|
89115
|
+
}
|
|
89116
|
+
else if (selection.kind === Kind$2.INLINE_FRAGMENT &&
|
|
89117
|
+
matchesTypeCondition(selection.typeCondition, runtimeType, info.schema)) {
|
|
89118
|
+
selectedParentFields.push(...getSelectedParentFields(selection.selectionSet, runtimeType, info, visitedFragments));
|
|
89119
|
+
}
|
|
89120
|
+
else if (selection.kind === Kind$2.FRAGMENT_SPREAD) {
|
|
89121
|
+
const fragment = info.fragments[selection.name.value];
|
|
89122
|
+
if (!fragment ||
|
|
89123
|
+
visitedFragments.has(fragment.name.value) ||
|
|
89124
|
+
!matchesTypeCondition(fragment.typeCondition, runtimeType, info.schema))
|
|
89125
|
+
continue;
|
|
89126
|
+
visitedFragments.add(fragment.name.value);
|
|
89127
|
+
selectedParentFields.push(...getSelectedParentFields(fragment.selectionSet, runtimeType, info, visitedFragments));
|
|
89128
|
+
}
|
|
89129
|
+
}
|
|
89130
|
+
return selectedParentFields;
|
|
89131
|
+
}
|
|
89132
|
+
function getActiveFieldSelectionSets(selectionSet, fieldName, runtimeType, info, visitedFragments) {
|
|
89133
|
+
if (!selectionSet)
|
|
89134
|
+
return [];
|
|
89135
|
+
const fieldSelectionSets = [];
|
|
89136
|
+
const variables = info.variableValues;
|
|
89137
|
+
for (const selection of selectionSet.selections) {
|
|
89138
|
+
if (!isSelectionIncluded(selection, variables))
|
|
89139
|
+
continue;
|
|
89140
|
+
if (selection.kind === Kind$2.FIELD &&
|
|
89141
|
+
selection.name.value === fieldName &&
|
|
89142
|
+
selection.selectionSet)
|
|
89143
|
+
fieldSelectionSets.push(selection.selectionSet);
|
|
89144
|
+
else if (selection.kind === Kind$2.INLINE_FRAGMENT &&
|
|
89145
|
+
matchesTypeCondition(selection.typeCondition, runtimeType, info.schema)) {
|
|
89146
|
+
fieldSelectionSets.push(...getActiveFieldSelectionSets(selection.selectionSet, fieldName, runtimeType, info, visitedFragments));
|
|
89147
|
+
}
|
|
89148
|
+
else if (selection.kind === Kind$2.FRAGMENT_SPREAD) {
|
|
89149
|
+
const fragment = info.fragments[selection.name.value];
|
|
89150
|
+
if (!fragment ||
|
|
89151
|
+
visitedFragments.has(fragment.name.value) ||
|
|
89152
|
+
!matchesTypeCondition(fragment.typeCondition, runtimeType, info.schema))
|
|
89153
|
+
continue;
|
|
89154
|
+
visitedFragments.add(fragment.name.value);
|
|
89155
|
+
fieldSelectionSets.push(...getActiveFieldSelectionSets(fragment.selectionSet, fieldName, runtimeType, info, visitedFragments));
|
|
89156
|
+
}
|
|
89157
|
+
}
|
|
89158
|
+
return fieldSelectionSets;
|
|
89159
|
+
}
|
|
89160
|
+
async function prefetchParentBatch(parents, state) {
|
|
89161
|
+
const { info, context, parentOutcomes, visitedSelections } = state;
|
|
89162
|
+
const nextParents = [];
|
|
89163
|
+
const parentLoads = [];
|
|
89164
|
+
for (const parentWork of parents) {
|
|
89165
|
+
let selectionsByType = visitedSelections.get(parentWork.record.recordRepresentation.id);
|
|
89166
|
+
if (!selectionsByType)
|
|
89167
|
+
visitedSelections.set(parentWork.record.recordRepresentation.id, (selectionsByType = new Map()));
|
|
89168
|
+
let visitedSelectionSets = selectionsByType.get(parentWork.runtimeType);
|
|
89169
|
+
if (!visitedSelectionSets)
|
|
89170
|
+
selectionsByType.set(parentWork.runtimeType, (visitedSelectionSets = new Set()));
|
|
89171
|
+
if (visitedSelectionSets.has(parentWork.selectionSet))
|
|
89172
|
+
continue;
|
|
89173
|
+
visitedSelectionSets.add(parentWork.selectionSet);
|
|
89174
|
+
for (const parentField of getSelectedParentFields(parentWork.selectionSet, parentWork.runtimeType, info, new Set())) {
|
|
89175
|
+
const parentId = relationshipId(parentField.name, parentWork.record.recordRepresentation);
|
|
89176
|
+
if (!parentId)
|
|
89177
|
+
continue;
|
|
89178
|
+
const knownOutcome = parentOutcomes.get(parentId) || context.parentResults.get(parentId);
|
|
89179
|
+
if (knownOutcome && 'error' in knownOutcome)
|
|
89180
|
+
continue;
|
|
89181
|
+
const parentLoad = context.Record.prefetch(parentId);
|
|
89182
|
+
parentLoads.push(parentLoad.then((outcome) => {
|
|
89183
|
+
parentOutcomes.set(parentId, outcome);
|
|
89184
|
+
if ('error' in outcome || outcome.record === null)
|
|
89185
|
+
return;
|
|
89186
|
+
const parentRecord = outcome.record;
|
|
89187
|
+
const declaredParentType = getNamedType(getObjectFields(parentWork.runtimeType)[parentField.name].type);
|
|
89188
|
+
const recordInterface = declaredParentType.name === 'Record' &&
|
|
89189
|
+
declaredParentType.astNode?.kind === Kind$2.INTERFACE_TYPE_DEFINITION
|
|
89190
|
+
? declaredParentType
|
|
89191
|
+
: undefined;
|
|
89192
|
+
const resolveRecordType = recordInterface?.resolveType;
|
|
89193
|
+
const resolvedParentType = resolveRecordType
|
|
89194
|
+
? resolveRecordType({
|
|
89195
|
+
recordRepresentation: parentRecord,
|
|
89196
|
+
ingestionTimestamp: parentWork.record.ingestionTimestamp,
|
|
89197
|
+
}, context, info)
|
|
89198
|
+
: declaredParentType;
|
|
89199
|
+
const parentRuntimeType = typeof resolvedParentType === 'string'
|
|
89200
|
+
? info.schema.getType(resolvedParentType)
|
|
89201
|
+
: resolvedParentType;
|
|
89202
|
+
if (!isObjectType(parentRuntimeType))
|
|
89203
|
+
return;
|
|
89204
|
+
nextParents.push({
|
|
89205
|
+
record: {
|
|
89206
|
+
recordRepresentation: parentRecord,
|
|
89207
|
+
ingestionTimestamp: parentWork.record.ingestionTimestamp,
|
|
89208
|
+
},
|
|
89209
|
+
selectionSet: parentField.selectionSet,
|
|
89210
|
+
runtimeType: parentRuntimeType,
|
|
89211
|
+
});
|
|
89212
|
+
}));
|
|
89213
|
+
}
|
|
89214
|
+
}
|
|
89215
|
+
await Promise.all(parentLoads);
|
|
89216
|
+
if (nextParents.length > 0)
|
|
89217
|
+
await prefetchParentBatch(nextParents, state);
|
|
89218
|
+
}
|
|
89219
|
+
async function prefetchSelectedParents(records, info, context) {
|
|
89220
|
+
const parentOutcomes = new Map();
|
|
89221
|
+
const connectionType = getNamedType(info.returnType);
|
|
89222
|
+
const edgeType = getNamedType(getObjectFields(connectionType).edges?.type);
|
|
89223
|
+
const nodeType = getNamedType(getObjectFields(edgeType).node?.type);
|
|
89224
|
+
if (!isObjectType(connectionType) || !isObjectType(edgeType) || !isObjectType(nodeType)) {
|
|
89225
|
+
return parentOutcomes;
|
|
89226
|
+
}
|
|
89227
|
+
const edgeSelectionSets = info.fieldNodes.flatMap((node) => getActiveFieldSelectionSets(node.selectionSet, 'edges', connectionType, info, new Set()));
|
|
89228
|
+
const nodeSelectionSets = edgeSelectionSets.flatMap((selectionSet) => getActiveFieldSelectionSets(selectionSet, 'node', edgeType, info, new Set()));
|
|
89229
|
+
if (records.length > 0 && nodeSelectionSets.length > 0) {
|
|
89230
|
+
await prefetchParentBatch(records.flatMap((record) => nodeSelectionSets.map((selectionSet) => ({
|
|
89231
|
+
record,
|
|
89232
|
+
selectionSet,
|
|
89233
|
+
runtimeType: nodeType,
|
|
89234
|
+
}))), { info, context, parentOutcomes, visitedSelections: new Map() });
|
|
89235
|
+
}
|
|
89236
|
+
return parentOutcomes;
|
|
89237
|
+
}
|
|
89238
|
+
|
|
86210
89239
|
/*
|
|
86211
89240
|
resolves connections...
|
|
86212
89241
|
*/
|
|
@@ -86330,6 +89359,11 @@ async function connectionResolver(obj, args, context, info) {
|
|
|
86330
89359
|
earlyEnd,
|
|
86331
89360
|
};
|
|
86332
89361
|
});
|
|
89362
|
+
if (context.parentPrefetchEnabled) {
|
|
89363
|
+
const results = await prefetchSelectedParents(records, info, context);
|
|
89364
|
+
for (const [id, outcome] of results)
|
|
89365
|
+
context.parentResults.set(id, outcome);
|
|
89366
|
+
}
|
|
86333
89367
|
return {
|
|
86334
89368
|
records,
|
|
86335
89369
|
hasNextPage,
|
|
@@ -86602,7 +89636,8 @@ function addResolversToSchema(schema, polyFields) {
|
|
|
86602
89636
|
.getInterfaces()
|
|
86603
89637
|
.find((iface) => iface.name === 'Record')) ||
|
|
86604
89638
|
(recordFieldType && recordFieldType.name === 'Record')) {
|
|
86605
|
-
field.resolve =
|
|
89639
|
+
field.resolve = function relationResolver(obj, _args, context) {
|
|
89640
|
+
const { Record, seenRecordIds, parentPrefetchEnabled } = context;
|
|
86606
89641
|
const fetchRecordOrNull = async (key) => {
|
|
86607
89642
|
const recordRepresentation = await Record.load(key);
|
|
86608
89643
|
return recordRepresentation !== null
|
|
@@ -86613,29 +89648,22 @@ function addResolversToSchema(schema, polyFields) {
|
|
|
86613
89648
|
: null;
|
|
86614
89649
|
};
|
|
86615
89650
|
const { recordRepresentation: record, ingestionTimestamp } = obj;
|
|
86616
|
-
|
|
86617
|
-
if (
|
|
86618
|
-
|
|
86619
|
-
// not in record.fields, so check for it at the UIAPI root property location
|
|
86620
|
-
id = record.recordTypeId;
|
|
86621
|
-
}
|
|
86622
|
-
else if (field.name.endsWith('__r')) {
|
|
86623
|
-
// Custom relationships end in `__r` and the corresponding ID field should be `__c`
|
|
86624
|
-
let fieldName = field.name.replace('__r', '__c');
|
|
86625
|
-
id = record.fields[fieldName] && record.fields[fieldName].value;
|
|
86626
|
-
}
|
|
86627
|
-
else {
|
|
86628
|
-
// Standard relationships are just FieldNameId
|
|
86629
|
-
let fieldName = field.name + 'Id';
|
|
86630
|
-
id = record.fields[fieldName] && record.fields[fieldName].value;
|
|
86631
|
-
}
|
|
86632
|
-
if (!id || typeof id !== 'string') {
|
|
86633
|
-
// possibly field injection did not inject the necessary Id field
|
|
86634
|
-
// for the relationship, or we found a non-string value.
|
|
86635
|
-
return null;
|
|
89651
|
+
const id = relationshipId(field.name, record);
|
|
89652
|
+
if (!id) {
|
|
89653
|
+
return parentPrefetchEnabled ? null : Promise.resolve(null);
|
|
86636
89654
|
}
|
|
86637
89655
|
seenRecordIds.add(id);
|
|
86638
|
-
|
|
89656
|
+
if (!parentPrefetchEnabled)
|
|
89657
|
+
return fetchRecordOrNull(id);
|
|
89658
|
+
const prefetched = context.parentResults.get(id);
|
|
89659
|
+
if (!prefetched)
|
|
89660
|
+
return fetchRecordOrNull(id);
|
|
89661
|
+
if ('error' in prefetched)
|
|
89662
|
+
return Promise.reject(prefetched.error);
|
|
89663
|
+
const recordRepresentation = prefetched.record;
|
|
89664
|
+
return recordRepresentation === null
|
|
89665
|
+
? null
|
|
89666
|
+
: { recordRepresentation, ingestionTimestamp };
|
|
86639
89667
|
};
|
|
86640
89668
|
}
|
|
86641
89669
|
else if (isObjectType(recordFieldType) &&
|
|
@@ -90583,10 +93611,21 @@ function makeNetworkAdapterChunkRecordFields(networkAdapter, instrumentationSink
|
|
|
90583
93611
|
*
|
|
90584
93612
|
* This instance is leveraged in the LMR webview which proxies DraftQueue requests
|
|
90585
93613
|
* to a concrete implementation running in jscore.
|
|
93614
|
+
*
|
|
93615
|
+
* The Nimbus plugin name targeted by this proxy can be parameterized via the
|
|
93616
|
+
* constructor (defaults to 'LdsDraftQueue'); pass 'LdsPerformanceDraftQueue' to
|
|
93617
|
+
* target the primer's Performance Upload draft queue.
|
|
90586
93618
|
*/
|
|
90587
93619
|
class NimbusDraftQueue {
|
|
93620
|
+
constructor(pluginName = 'LdsDraftQueue') {
|
|
93621
|
+
this.pluginName = pluginName;
|
|
93622
|
+
}
|
|
93623
|
+
get callProxyMethod() {
|
|
93624
|
+
const plugin = __nimbus.plugins[this.pluginName];
|
|
93625
|
+
return plugin && plugin.callProxyMethod;
|
|
93626
|
+
}
|
|
90588
93627
|
enqueue(handlerId, data, observabilityContext) {
|
|
90589
|
-
const callProxyMethod =
|
|
93628
|
+
const callProxyMethod = this.callProxyMethod;
|
|
90590
93629
|
if (callProxyMethod === undefined) {
|
|
90591
93630
|
return Promise.reject(new Error('callProxyMethod not defined on the nimbus plugin'));
|
|
90592
93631
|
}
|
|
@@ -90613,7 +93652,7 @@ class NimbusDraftQueue {
|
|
|
90613
93652
|
return Promise.reject(new Error('Cannot call processNextAction from the NimbusDraftQueue'));
|
|
90614
93653
|
}
|
|
90615
93654
|
getQueueActions() {
|
|
90616
|
-
const callProxyMethod =
|
|
93655
|
+
const callProxyMethod = this.callProxyMethod;
|
|
90617
93656
|
if (callProxyMethod === undefined) {
|
|
90618
93657
|
return Promise.reject(new Error('callProxyMethod not defined on the nimbus plugin'));
|
|
90619
93658
|
}
|
|
@@ -90626,7 +93665,7 @@ class NimbusDraftQueue {
|
|
|
90626
93665
|
});
|
|
90627
93666
|
}
|
|
90628
93667
|
getDataForAction(action) {
|
|
90629
|
-
const callProxyMethod =
|
|
93668
|
+
const callProxyMethod = this.callProxyMethod;
|
|
90630
93669
|
if (callProxyMethod === undefined) {
|
|
90631
93670
|
return Promise.reject('callProxyMethod not defined on the nimbus plugin');
|
|
90632
93671
|
}
|
|
@@ -90767,6 +93806,12 @@ function instrumentDraftQueue(queue) {
|
|
|
90767
93806
|
case DraftQueueEventType.ActionUpdated:
|
|
90768
93807
|
reportDraftActionEvent('updated', draftQueueEvent.draftCount);
|
|
90769
93808
|
break;
|
|
93809
|
+
case DraftQueueEventType.BatchRecoveryFailed:
|
|
93810
|
+
{
|
|
93811
|
+
const recoveryEvent = draftQueueEvent;
|
|
93812
|
+
reportBatchRecoveryFailure(recoveryEvent.reason, recoveryEvent.draftCount);
|
|
93813
|
+
}
|
|
93814
|
+
break;
|
|
90770
93815
|
}
|
|
90771
93816
|
return Promise.resolve();
|
|
90772
93817
|
});
|
|
@@ -90815,14 +93860,42 @@ const draftQueueInstrumentation = {
|
|
|
90815
93860
|
|
|
90816
93861
|
// so eslint doesn't complain about nimbus
|
|
90817
93862
|
/* global __nimbus */
|
|
90818
|
-
|
|
93863
|
+
/**
|
|
93864
|
+
* Builds the regular draft queue.
|
|
93865
|
+
*
|
|
93866
|
+
* One-arg call = legacy plain DurableDraftQueue (unchanged shipped behavior);
|
|
93867
|
+
* the 3-arg call builds a PerformanceDraftQueue (serial, concurrency=1, cross-queue
|
|
93868
|
+
* coordination + optional batching) over the shared store for the gate-ON path.
|
|
93869
|
+
*/
|
|
93870
|
+
function buildLdsDraftQueue(durableStore, draftStore, options) {
|
|
90819
93871
|
if (typeof __nimbus !== 'undefined' &&
|
|
90820
93872
|
__nimbus.plugins !== undefined &&
|
|
90821
93873
|
__nimbus.plugins.LdsDraftQueue !== undefined) {
|
|
90822
|
-
return new NimbusDraftQueue();
|
|
93874
|
+
return new NimbusDraftQueue('LdsDraftQueue');
|
|
93875
|
+
}
|
|
93876
|
+
// Legacy gate-OFF path: no shared store / options → today's plain queue.
|
|
93877
|
+
if (draftStore === undefined && options === undefined) {
|
|
93878
|
+
const useSnapshot = draftQueueStoreSnapshotGate.isOpen({ fallback: true });
|
|
93879
|
+
const draftQueue = new DurableDraftQueue(new DurableDraftStore(durableStore, draftQueueInstrumentation, useSnapshot), draftQueueInstrumentation);
|
|
93880
|
+
return instrumentDraftQueue(draftQueue);
|
|
90823
93881
|
}
|
|
90824
|
-
|
|
90825
|
-
const
|
|
93882
|
+
// Performance Upload path: serial queue participating in cross-queue coordination.
|
|
93883
|
+
const store = draftStore ?? new DurableDraftStore(durableStore);
|
|
93884
|
+
const draftQueue = new PerformanceDraftQueue(store, options);
|
|
93885
|
+
return instrumentDraftQueue(draftQueue);
|
|
93886
|
+
}
|
|
93887
|
+
/**
|
|
93888
|
+
* Builds the Performance Upload draft queue (ContentDocument/Version/Link
|
|
93889
|
+
* multipart uploads, concurrency > 1). Only constructed on the gate-ON path.
|
|
93890
|
+
*/
|
|
93891
|
+
function buildLdsPerformanceDraftQueue(durableStore, draftStore, options) {
|
|
93892
|
+
if (typeof __nimbus !== 'undefined' &&
|
|
93893
|
+
__nimbus.plugins !== undefined &&
|
|
93894
|
+
__nimbus.plugins.LdsPerformanceDraftQueue !== undefined) {
|
|
93895
|
+
return new NimbusDraftQueue('LdsPerformanceDraftQueue');
|
|
93896
|
+
}
|
|
93897
|
+
const store = draftStore ?? new DurableDraftStore(durableStore);
|
|
93898
|
+
const draftQueue = new PerformanceDraftQueue(store, options);
|
|
90826
93899
|
return instrumentDraftQueue(draftQueue);
|
|
90827
93900
|
}
|
|
90828
93901
|
|
|
@@ -95765,13 +98838,17 @@ function buildServiceDescriptor$m() {
|
|
|
95765
98838
|
};
|
|
95766
98839
|
}
|
|
95767
98840
|
|
|
95768
|
-
function applyOperationRequestOptions$
|
|
95769
|
-
if (operationType === void 0) {
|
|
98841
|
+
function applyOperationRequestOptions$3(init, operationType, requestOptions) {
|
|
98842
|
+
if (operationType === void 0 && requestOptions === void 0) {
|
|
95770
98843
|
return init;
|
|
95771
98844
|
}
|
|
95772
98845
|
return {
|
|
95773
98846
|
...init,
|
|
95774
|
-
onestore: {
|
|
98847
|
+
onestore: {
|
|
98848
|
+
...requestOptions,
|
|
98849
|
+
...init?.onestore,
|
|
98850
|
+
...operationType !== void 0 && { retrySafe: operationType === "query" }
|
|
98851
|
+
}
|
|
95775
98852
|
};
|
|
95776
98853
|
}
|
|
95777
98854
|
class AuraNetworkCommand extends NetworkCommand$1 {
|
|
@@ -95804,6 +98881,10 @@ class AuraNetworkCommand extends NetworkCommand$1 {
|
|
|
95804
98881
|
get operationType() {
|
|
95805
98882
|
return void 0;
|
|
95806
98883
|
}
|
|
98884
|
+
/** Declared `onestore` options (`handleServerError`, `compression`); generated overrides. HTTP fallback only. */
|
|
98885
|
+
get requestOptions() {
|
|
98886
|
+
return void 0;
|
|
98887
|
+
}
|
|
95807
98888
|
coerceAuraErrors(auraErrors) {
|
|
95808
98889
|
const response = getFetchResponseFromAuraError(auraErrors[0]);
|
|
95809
98890
|
return new UserVisibleError(
|
|
@@ -95910,7 +98991,11 @@ class AuraNetworkCommand extends NetworkCommand$1 {
|
|
|
95910
98991
|
);
|
|
95911
98992
|
} else if (this.shouldUseFetch()) {
|
|
95912
98993
|
const [input, init] = this.fetchParams;
|
|
95913
|
-
const initWithOptions = applyOperationRequestOptions$
|
|
98994
|
+
const initWithOptions = applyOperationRequestOptions$3(
|
|
98995
|
+
init,
|
|
98996
|
+
this.operationType,
|
|
98997
|
+
this.requestOptions
|
|
98998
|
+
);
|
|
95914
98999
|
try {
|
|
95915
99000
|
const fetchCall = initWithOptions === void 0 ? this.services.fetch(input) : this.services.fetch(input, initWithOptions);
|
|
95916
99001
|
return this.convertFetchResponseToData(fetchCall);
|
|
@@ -96478,6 +99563,19 @@ function mergeCacheControlConfigs(baseConfig, overrides) {
|
|
|
96478
99563
|
};
|
|
96479
99564
|
}
|
|
96480
99565
|
|
|
99566
|
+
function applyOperationRequestOptions$2(init, operationType, requestOptions) {
|
|
99567
|
+
if (operationType === void 0 && requestOptions === void 0) {
|
|
99568
|
+
return init;
|
|
99569
|
+
}
|
|
99570
|
+
return {
|
|
99571
|
+
...init,
|
|
99572
|
+
onestore: {
|
|
99573
|
+
...requestOptions,
|
|
99574
|
+
...init?.onestore,
|
|
99575
|
+
...operationType !== void 0 && { retrySafe: operationType === "query" }
|
|
99576
|
+
}
|
|
99577
|
+
};
|
|
99578
|
+
}
|
|
96481
99579
|
class AuraCacheControlCommand extends CacheControlCommand {
|
|
96482
99580
|
constructor(services) {
|
|
96483
99581
|
super(services);
|
|
@@ -96495,6 +99593,20 @@ class AuraCacheControlCommand extends CacheControlCommand {
|
|
|
96495
99593
|
"Fetch parameters must be specified when using HTTP transport on an Aura adapter"
|
|
96496
99594
|
);
|
|
96497
99595
|
}
|
|
99596
|
+
/** Declared `onestore` options (`handleServerError`, `compression`); generated overrides. HTTP fallback only. */
|
|
99597
|
+
get requestOptions() {
|
|
99598
|
+
return void 0;
|
|
99599
|
+
}
|
|
99600
|
+
/**
|
|
99601
|
+
* `params` with request options merged into `init.onestore` (see `applyOperationRequestOptions`).
|
|
99602
|
+
* `retrySafe` follows `operationType` (default `'query'`): hand-written writes must override it.
|
|
99603
|
+
*/
|
|
99604
|
+
withRequestOptions([
|
|
99605
|
+
input,
|
|
99606
|
+
init
|
|
99607
|
+
]) {
|
|
99608
|
+
return [input, applyOperationRequestOptions$2(init, this.operationType, this.requestOptions)];
|
|
99609
|
+
}
|
|
96498
99610
|
shouldUseAuraNetwork() {
|
|
96499
99611
|
return this.services.auraNetwork !== void 0 && (this.networkPreference === "aura" || !this.services.fetch);
|
|
96500
99612
|
}
|
|
@@ -96513,7 +99625,7 @@ class AuraCacheControlCommand extends CacheControlCommand {
|
|
|
96513
99625
|
(errs) => this.coerceAuraErrors(errs)
|
|
96514
99626
|
);
|
|
96515
99627
|
} else if (this.shouldUseFetch()) {
|
|
96516
|
-
const params = this.fetchParams;
|
|
99628
|
+
const params = this.withRequestOptions(this.fetchParams);
|
|
96517
99629
|
try {
|
|
96518
99630
|
return this.convertFetchResponseToData(this.services.fetch(...params));
|
|
96519
99631
|
} catch (reason) {
|
|
@@ -96660,13 +99772,17 @@ class NetworkCommand extends BaseCommand {
|
|
|
96660
99772
|
async afterRequestHooks(_options) {
|
|
96661
99773
|
}
|
|
96662
99774
|
}
|
|
96663
|
-
function applyOperationRequestOptions(init, operationType) {
|
|
96664
|
-
if (operationType === void 0) {
|
|
99775
|
+
function applyOperationRequestOptions$1(init, operationType, requestOptions) {
|
|
99776
|
+
if (operationType === void 0 && requestOptions === void 0) {
|
|
96665
99777
|
return init;
|
|
96666
99778
|
}
|
|
96667
99779
|
return {
|
|
96668
99780
|
...init,
|
|
96669
|
-
onestore: {
|
|
99781
|
+
onestore: {
|
|
99782
|
+
...requestOptions,
|
|
99783
|
+
...init?.onestore,
|
|
99784
|
+
...operationType !== void 0 && { retrySafe: operationType === "query" }
|
|
99785
|
+
}
|
|
96670
99786
|
};
|
|
96671
99787
|
}
|
|
96672
99788
|
function isAbortableCommand(command) {
|
|
@@ -96747,11 +99863,19 @@ const _FetchNetworkCommand = class _FetchNetworkCommand extends NetworkCommand {
|
|
|
96747
99863
|
get operationType() {
|
|
96748
99864
|
return void 0;
|
|
96749
99865
|
}
|
|
99866
|
+
/** Declared `onestore` options (`handleServerError`, `compression`); generated overrides (`generateRequestOptionsGetter`). */
|
|
99867
|
+
get requestOptions() {
|
|
99868
|
+
return void 0;
|
|
99869
|
+
}
|
|
96750
99870
|
fetch(contextSeed) {
|
|
96751
99871
|
try {
|
|
96752
99872
|
const [input, init] = this.fetchParams;
|
|
96753
99873
|
const operationType = this.operationType;
|
|
96754
|
-
const initWithOptions = applyOperationRequestOptions(
|
|
99874
|
+
const initWithOptions = applyOperationRequestOptions$1(
|
|
99875
|
+
init,
|
|
99876
|
+
operationType,
|
|
99877
|
+
this.requestOptions
|
|
99878
|
+
);
|
|
96755
99879
|
const seed = operationType === void 0 ? contextSeed : { operationType, ...contextSeed };
|
|
96756
99880
|
const initWithSeed = seed === void 0 ? initWithOptions : { ...initWithOptions, __contextSeed: seed };
|
|
96757
99881
|
const fetchCall = initWithSeed === void 0 ? this.services.fetch(input) : this.services.fetch(input, initWithSeed);
|
|
@@ -96826,6 +99950,19 @@ function buildServiceDescriptor$j() {
|
|
|
96826
99950
|
};
|
|
96827
99951
|
}
|
|
96828
99952
|
|
|
99953
|
+
function applyOperationRequestOptions(init, operationType, requestOptions) {
|
|
99954
|
+
if (operationType === void 0 && requestOptions === void 0) {
|
|
99955
|
+
return init;
|
|
99956
|
+
}
|
|
99957
|
+
return {
|
|
99958
|
+
...init,
|
|
99959
|
+
onestore: {
|
|
99960
|
+
...requestOptions,
|
|
99961
|
+
...init?.onestore,
|
|
99962
|
+
...operationType !== void 0 && { retrySafe: operationType === "query" }
|
|
99963
|
+
}
|
|
99964
|
+
};
|
|
99965
|
+
}
|
|
96829
99966
|
class HttpCacheControlCommand extends CacheControlCommand {
|
|
96830
99967
|
constructor(services) {
|
|
96831
99968
|
super(services);
|
|
@@ -96835,9 +99972,25 @@ class HttpCacheControlCommand extends CacheControlCommand {
|
|
|
96835
99972
|
requestFromNetwork() {
|
|
96836
99973
|
return this.fetch();
|
|
96837
99974
|
}
|
|
99975
|
+
/** Declared `onestore` options (`handleServerError`, `compression`); generated overrides. */
|
|
99976
|
+
get requestOptions() {
|
|
99977
|
+
return void 0;
|
|
99978
|
+
}
|
|
99979
|
+
/**
|
|
99980
|
+
* `params` with request options merged into `init.onestore` (see `applyOperationRequestOptions`).
|
|
99981
|
+
* `retrySafe` follows `operationType` (default `'query'`): hand-written writes must override it.
|
|
99982
|
+
*/
|
|
99983
|
+
withRequestOptions([
|
|
99984
|
+
input,
|
|
99985
|
+
init
|
|
99986
|
+
]) {
|
|
99987
|
+
return [input, applyOperationRequestOptions(init, this.operationType, this.requestOptions)];
|
|
99988
|
+
}
|
|
96838
99989
|
fetch() {
|
|
96839
99990
|
try {
|
|
96840
|
-
return this.convertFetchResponseToData(
|
|
99991
|
+
return this.convertFetchResponseToData(
|
|
99992
|
+
this.services.fetch(...this.withRequestOptions(this.fetchParams))
|
|
99993
|
+
);
|
|
96841
99994
|
} catch (reason) {
|
|
96842
99995
|
return resolvedPromiseLike$2(err$1(toError(reason)));
|
|
96843
99996
|
}
|
|
@@ -97847,7 +101000,7 @@ function buildServiceDescriptor$b(luvio) {
|
|
|
97847
101000
|
},
|
|
97848
101001
|
};
|
|
97849
101002
|
}
|
|
97850
|
-
// version: 1.
|
|
101003
|
+
// version: 1.467.0-4f065d2a2a
|
|
97851
101004
|
|
|
97852
101005
|
/**
|
|
97853
101006
|
* Copyright (c) 2022, Salesforce, Inc.,
|
|
@@ -97873,7 +101026,7 @@ function buildServiceDescriptor$a(notifyRecordUpdateAvailable, getNormalizedLuvi
|
|
|
97873
101026
|
},
|
|
97874
101027
|
};
|
|
97875
101028
|
}
|
|
97876
|
-
// version: 1.
|
|
101029
|
+
// version: 1.467.0-4f065d2a2a
|
|
97877
101030
|
|
|
97878
101031
|
function findExecutableOperation$1(input) {
|
|
97879
101032
|
const operations = input.query.definitions.filter(
|
|
@@ -98070,7 +101223,7 @@ class AuraGraphQLNormalizedCacheControlCommand extends AuraNormalizedCacheContro
|
|
|
98070
101223
|
return buildSubscribableResult$1(result, this.buildSubscribe(), () => this.refresh());
|
|
98071
101224
|
});
|
|
98072
101225
|
} else if (this.shouldUseFetch()) {
|
|
98073
|
-
const params = this.originalFetchParams;
|
|
101226
|
+
const params = this.withRequestOptions(this.originalFetchParams);
|
|
98074
101227
|
try {
|
|
98075
101228
|
return this.convertFetchResponseToData(this.services.fetch(...params)).then(
|
|
98076
101229
|
(result) => {
|
|
@@ -98225,7 +101378,7 @@ class HttpGraphQLNormalizedCacheControlCommand extends HttpNormalizedCacheContro
|
|
|
98225
101378
|
}
|
|
98226
101379
|
runOriginalRequest() {
|
|
98227
101380
|
return this.convertFetchResponseToData(
|
|
98228
|
-
this.services.fetch(...this.originalFetchParams)
|
|
101381
|
+
this.services.fetch(...this.withRequestOptions(this.originalFetchParams))
|
|
98229
101382
|
).then((result) => {
|
|
98230
101383
|
return buildSubscribableResult$1(result, this.buildSubscribe(), () => this.refresh());
|
|
98231
101384
|
});
|
|
@@ -100285,10 +103438,6 @@ function initializeOneStore(sqliteStore, luvio) {
|
|
|
100285
103438
|
const instrumentationServiceDescriptor = buildServiceDescriptor$e(loggerService);
|
|
100286
103439
|
const nimbusSqliteOneStoreCacheServiceDescriptor = buildNimbusDurableCacheInclusionPolicyService(cacheServiceDescriptor.service, new NimbusSqliteOneStoreCache(sqliteStore));
|
|
100287
103440
|
const featureFlagsServiceDescriptor = buildServiceDescriptor$c();
|
|
100288
|
-
const featureFlagsService = featureFlagsServiceDescriptor.service;
|
|
100289
|
-
// This disables the GraphQL mapping from lightning/uiGraphQLApi to lightning/graphql in this runtime
|
|
100290
|
-
// The gate is likely NOT to rollout ever, but we keep it here for completeness
|
|
100291
|
-
featureFlagsService.set('useOneStoreGraphQL', false);
|
|
100292
103441
|
const luvioUiapiRecordsServiceDescriptor = buildServiceDescriptor$a((configs) => {
|
|
100293
103442
|
return notifyUpdateAvailableFactory(luvio)(configs);
|
|
100294
103443
|
}, (recordId, reader) => {
|
|
@@ -100387,7 +103536,13 @@ function queryAsync(sqliteStore, sql, params) {
|
|
|
100387
103536
|
// so eslint doesn't complain about nimbus
|
|
100388
103537
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
100389
103538
|
/* global __nimbus */
|
|
103539
|
+
const DRAFT_QUEUE_LANE = {
|
|
103540
|
+
serial: 'serial',
|
|
103541
|
+
concurrentBinary: 'concurrentBinary',
|
|
103542
|
+
};
|
|
100390
103543
|
let lazyDraftQueue;
|
|
103544
|
+
let lazyBinaryDraftQueue;
|
|
103545
|
+
let lazyRoutingDraftQueue;
|
|
100391
103546
|
let lazyDraftManager;
|
|
100392
103547
|
let lazyLuvio;
|
|
100393
103548
|
let lazyInternalLuvio;
|
|
@@ -100476,8 +103631,37 @@ function getRuntime({ primeUser = false } = {}) {
|
|
|
100476
103631
|
return false;
|
|
100477
103632
|
},
|
|
100478
103633
|
});
|
|
100479
|
-
// draft queue
|
|
100480
|
-
|
|
103634
|
+
// Concurrent binary draft queue configuration is supplied by the native
|
|
103635
|
+
// host on globalThis.LDS, injected before the lds bundle loads so the
|
|
103636
|
+
// runtime sees the Performance Upload Queue levers at startup.
|
|
103637
|
+
const concurrentBinaryQueueConfig = (typeof globalThis !== 'undefined' &&
|
|
103638
|
+
globalThis.LDS) ||
|
|
103639
|
+
{};
|
|
103640
|
+
const useConcurrentBinaryQueue = concurrentBinaryQueueConfig.enableConcurrentBinaryQueue === true;
|
|
103641
|
+
const useQueueBatching = concurrentBinaryQueueConfig.enableDraftBatching === true;
|
|
103642
|
+
const usePerformanceDraftQueue = useConcurrentBinaryQueue || useQueueBatching;
|
|
103643
|
+
if (usePerformanceDraftQueue) {
|
|
103644
|
+
const sharedDraftStore = new PerformanceDraftStore(lazyDurableStore);
|
|
103645
|
+
logInfo(`Serial draft queue created with batching=${useQueueBatching}, strictOrdering=true`);
|
|
103646
|
+
lazyDraftQueue = buildLdsDraftQueue(lazyDurableStore, sharedDraftStore, {
|
|
103647
|
+
// No peer queues when there's no binary lane (batching-only).
|
|
103648
|
+
getPeerQueues: () => (useConcurrentBinaryQueue ? [lazyBinaryDraftQueue] : []),
|
|
103649
|
+
concurrency: 1,
|
|
103650
|
+
strictOrdering: true,
|
|
103651
|
+
queueBatching: useQueueBatching,
|
|
103652
|
+
});
|
|
103653
|
+
if (useConcurrentBinaryQueue) {
|
|
103654
|
+
lazyBinaryDraftQueue = buildLdsPerformanceDraftQueue(lazyDurableStore, sharedDraftStore, {
|
|
103655
|
+
getPeerQueues: () => [lazyDraftQueue],
|
|
103656
|
+
concurrency: 4,
|
|
103657
|
+
strictOrdering: false,
|
|
103658
|
+
});
|
|
103659
|
+
logInfo('Concurrent draft queue created with concurrency=4, strictOrdering=false');
|
|
103660
|
+
}
|
|
103661
|
+
}
|
|
103662
|
+
else {
|
|
103663
|
+
lazyDraftQueue = buildLdsDraftQueue(lazyDurableStore);
|
|
103664
|
+
}
|
|
100481
103665
|
lazySideEffectStore = new SideEffectStore(lazyDurableStore);
|
|
100482
103666
|
lazySideEffectService = new SideEffectService(lazyDurableRecordStore, lazyObjectInfoService, userId, formatDisplayValue);
|
|
100483
103667
|
const draftService = new UiApiDraftRecordService(() => lazyLuvio, lazyDurableStore, lazyObjectInfoService, newRecordId, lazySideEffectStore, lazyDurableRecordStore);
|
|
@@ -100485,13 +103669,46 @@ function getRuntime({ primeUser = false } = {}) {
|
|
|
100485
103669
|
uiApiRecordHandler.setSideEffectHooks(createSfsSideEffectHooks(lazyDurableRecordStore));
|
|
100486
103670
|
const quickActionHandler = new QuickActionExecutionRepresentationHandler(() => lazyLuvio, draftService, lazyDraftQueue, lazyNetworkAdapter, isGenerated, lazySideEffectService, lazyObjectInfoService, getRecord);
|
|
100487
103671
|
const updateRecordQuickActionHandler = new UpdateRecordQuickActionExecutionRepresentationHandler(() => lazyLuvio, draftService, lazyDraftQueue, lazyNetworkAdapter, isGenerated, lazySideEffectService);
|
|
100488
|
-
|
|
103672
|
+
// Route the CD composite to the concurrent binary queue when gate-ON so
|
|
103673
|
+
// multipart uploads run in parallel and don't block other DML.
|
|
103674
|
+
const contentDocumentTargetQueue = useConcurrentBinaryQueue
|
|
103675
|
+
? lazyBinaryDraftQueue
|
|
103676
|
+
: lazyDraftQueue;
|
|
103677
|
+
logInfo(`ContentDocumentVersion handler routed to ${useConcurrentBinaryQueue ? 'concurrent draft queue' : 'serial draft queue'}`);
|
|
103678
|
+
const contentDocumentCompositeActionHandler = new ContentDocumentCompositeRepresentationActionHandler(() => lazyLuvio, userId, draftService, lazyObjectInfoService, contentDocumentTargetQueue, lazyNetworkAdapter, isGenerated, NimbusBinaryStore, lazySideEffectService);
|
|
100489
103679
|
lazyDraftQueue.addHandler(uiApiRecordHandler);
|
|
100490
103680
|
lazyDraftQueue.addHandler(quickActionHandler);
|
|
100491
103681
|
lazyDraftQueue.addHandler(updateRecordQuickActionHandler);
|
|
100492
|
-
|
|
100493
|
-
//
|
|
100494
|
-
|
|
103682
|
+
contentDocumentTargetQueue.addHandler(contentDocumentCompositeActionHandler);
|
|
103683
|
+
// Build the routing façade only with the flag on AND over concrete, handler-owning
|
|
103684
|
+
// queues. Only the worker-realm queues expose isHandlerRegistered; the WebView realm's
|
|
103685
|
+
// Nimbus proxies lack it. Feature-detect that method rather than sniffing the realm via
|
|
103686
|
+
// `window`: it tests the exact invariant the DurableDraftQueue casts below rely on, and
|
|
103687
|
+
// stays correct in non-browser (e.g. jest) realms where `window` is an unreliable realm
|
|
103688
|
+
// signal. The flag must stay the first term — it short-circuits the queue accesses, and
|
|
103689
|
+
// the gate-OFF path never builds lazyBinaryDraftQueue (it is undefined there).
|
|
103690
|
+
const shouldBuildRoutingQueue = useConcurrentBinaryQueue &&
|
|
103691
|
+
typeof lazyDraftQueue.isHandlerRegistered ===
|
|
103692
|
+
'function' &&
|
|
103693
|
+
typeof lazyBinaryDraftQueue
|
|
103694
|
+
.isHandlerRegistered === 'function';
|
|
103695
|
+
if (shouldBuildRoutingQueue) {
|
|
103696
|
+
// Serial is the primary/fallback lane, so the bridged `draftQueue` routes enqueue
|
|
103697
|
+
// by handler ownership in the worker. Lane labels are stamped onto each action
|
|
103698
|
+
// by getQueueActions so the WebView can display the two queues separately.
|
|
103699
|
+
lazyRoutingDraftQueue = new RoutingDraftQueue({
|
|
103700
|
+
queue: lazyDraftQueue,
|
|
103701
|
+
lane: DRAFT_QUEUE_LANE.serial,
|
|
103702
|
+
}, [
|
|
103703
|
+
{
|
|
103704
|
+
queue: lazyBinaryDraftQueue,
|
|
103705
|
+
lane: DRAFT_QUEUE_LANE.concurrentBinary,
|
|
103706
|
+
},
|
|
103707
|
+
]);
|
|
103708
|
+
}
|
|
103709
|
+
lazyDraftManager = useConcurrentBinaryQueue
|
|
103710
|
+
? new RoutingDraftManager(lazyDraftQueue, lazyBinaryDraftQueue, DRAFT_QUEUE_LANE)
|
|
103711
|
+
: new DraftManager(lazyDraftQueue);
|
|
100495
103712
|
const objectInfoCachingEnvironment = enableObjectInfoCaching(durableEnv, lazyObjectInfoService.ensureObjectInfoCached);
|
|
100496
103713
|
let draftEnv = makeEnvironmentUiApiRecordDraftAware(lazyLuvio, {
|
|
100497
103714
|
store,
|
|
@@ -100566,7 +103783,9 @@ function getRuntime({ primeUser = false } = {}) {
|
|
|
100566
103783
|
internalLuvio: lazyInternalLuvio,
|
|
100567
103784
|
environment: lazyEnvironment,
|
|
100568
103785
|
draftManager: lazyDraftManager,
|
|
100569
|
-
draftQueue: lazyDraftQueue,
|
|
103786
|
+
draftQueue: lazyRoutingDraftQueue ?? lazyDraftQueue,
|
|
103787
|
+
serialDraftQueue: lazyDraftQueue,
|
|
103788
|
+
binaryDraftQueue: lazyBinaryDraftQueue,
|
|
100570
103789
|
createPrimingSession: (config) => {
|
|
100571
103790
|
return primingSessionFactory({
|
|
100572
103791
|
store: lazyDurableStore,
|
|
@@ -100592,7 +103811,7 @@ register$1({
|
|
|
100592
103811
|
id: '@salesforce/lds-network-adapter',
|
|
100593
103812
|
instrument: instrument$2,
|
|
100594
103813
|
});
|
|
100595
|
-
// version: 1.
|
|
103814
|
+
// version: 1.467.0-4f065d2a2a
|
|
100596
103815
|
|
|
100597
103816
|
const { create: create$2, keys: keys$2 } = Object;
|
|
100598
103817
|
const { stringify, parse } = JSON;
|
|
@@ -107987,7 +111206,6 @@ let graphql_imperative$1;
|
|
|
107987
111206
|
let graphql_imperative_legacy;
|
|
107988
111207
|
let graphql_imperative_legacy_v1_import;
|
|
107989
111208
|
let graphql_state_manager;
|
|
107990
|
-
let useOneStoreGraphQL = false;
|
|
107991
111209
|
const serviceRequirements = {
|
|
107992
111210
|
instrumentCommand: {
|
|
107993
111211
|
type: 'instrumentCommand',
|
|
@@ -108131,9 +111349,8 @@ getServices(serviceRequirements).then((services) => {
|
|
|
108131
111349
|
const baseCommand = new executeMutation_ctor(config, mutationType, services);
|
|
108132
111350
|
return applyDecorators(baseCommand, [services.fetchNetworkCommandBaseClass.availableDecorators.abortable], options);
|
|
108133
111351
|
});
|
|
108134
|
-
useOneStoreGraphQL = services.featureFlags?.get('useOneStoreGraphQL') ?? false;
|
|
108135
111352
|
if (provisionedCallback) {
|
|
108136
|
-
provisionedCallback(graphql$1, graphql_imperative$1, graphql_imperative_legacy, graphql_state_manager
|
|
111353
|
+
provisionedCallback(graphql$1, graphql_imperative$1, graphql_imperative_legacy, graphql_state_manager);
|
|
108137
111354
|
}
|
|
108138
111355
|
}, (_err) => { });
|
|
108139
111356
|
function registerCallback(cb) {
|
|
@@ -108141,10 +111358,10 @@ function registerCallback(cb) {
|
|
|
108141
111358
|
provisionedCallback = cb;
|
|
108142
111359
|
}
|
|
108143
111360
|
else {
|
|
108144
|
-
cb(graphql_v1_import, graphql_imperative$1, graphql_imperative_legacy_v1_import, graphql_state_manager
|
|
111361
|
+
cb(graphql_v1_import, graphql_imperative$1, graphql_imperative_legacy_v1_import, graphql_state_manager);
|
|
108145
111362
|
}
|
|
108146
111363
|
}
|
|
108147
|
-
// version: 1.
|
|
111364
|
+
// version: 1.467.0-049a8b6027
|
|
108148
111365
|
|
|
108149
111366
|
function createFragmentMap(documentNode) {
|
|
108150
111367
|
const fragments = {};
|
|
@@ -136843,7 +140060,7 @@ let graphqlBatch;
|
|
|
136843
140060
|
// Imperative GET Adapters
|
|
136844
140061
|
let graphql_imperative;
|
|
136845
140062
|
let graphqlBatch_imperative;
|
|
136846
|
-
|
|
140063
|
+
const refreshGraphQL = (data) => {
|
|
136847
140064
|
return refresh$3(data, 'refreshUiApi');
|
|
136848
140065
|
};
|
|
136849
140066
|
// Adapter Metadata
|
|
@@ -136867,21 +140084,7 @@ withDefaultLuvio((luvio) => {
|
|
|
136867
140084
|
({ graphql, graphqlBatch, graphql_imperative, graphqlBatch_imperative } = bindExportsTo(luvio));
|
|
136868
140085
|
// We register the callback here rather than above withDefaultLuvio because we want environments
|
|
136869
140086
|
// that don't have onestore bootstrapped with the required services to default to the luvio versions of the adapters
|
|
136870
|
-
registerCallback((oneStoreGraphql, oneStoreGraphql_imperative,
|
|
136871
|
-
if (useOneStoreGraphQL) {
|
|
136872
|
-
graphql = oneStoreGraphql;
|
|
136873
|
-
graphql_imperative = oneStoreGraphql_imperative_legacy;
|
|
136874
|
-
refreshGraphQL = (data) => {
|
|
136875
|
-
if (data?.data === undefined && data?.errors === undefined) {
|
|
136876
|
-
return Promise.resolve();
|
|
136877
|
-
}
|
|
136878
|
-
if (typeof data?.refresh === 'function') {
|
|
136879
|
-
return data.refresh();
|
|
136880
|
-
}
|
|
136881
|
-
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
136882
|
-
throw new Error('Component attempted to refresh a GraphQL result that is not refreshable');
|
|
136883
|
-
};
|
|
136884
|
-
}
|
|
140087
|
+
registerCallback((oneStoreGraphql, oneStoreGraphql_imperative, _oneStoreGraphql_imperative_legacy, oneStoreGraphql_state_manager) => {
|
|
136885
140088
|
});
|
|
136886
140089
|
});
|
|
136887
140090
|
|
|
@@ -137387,7 +140590,7 @@ register$1({
|
|
|
137387
140590
|
configuration: { ...configurationForGraphQLAdapters$1 },
|
|
137388
140591
|
instrument: instrument$1,
|
|
137389
140592
|
});
|
|
137390
|
-
// version: 1.
|
|
140593
|
+
// version: 1.467.0-049a8b6027
|
|
137391
140594
|
|
|
137392
140595
|
// On core the unstable adapters are re-exported with different names,
|
|
137393
140596
|
// we want to match them here.
|
|
@@ -137399,7 +140602,7 @@ var unstableLightningRelatedListApi = /*#__PURE__*/Object.freeze({
|
|
|
137399
140602
|
gql: gql,
|
|
137400
140603
|
get graphql () { return graphql; },
|
|
137401
140604
|
get graphqlBatch () { return graphqlBatch; },
|
|
137402
|
-
|
|
140605
|
+
refreshGraphQL: refreshGraphQL,
|
|
137403
140606
|
get unstable_getActionOverrides_imperative () { return unstable_getActionOverrides_imperative; },
|
|
137404
140607
|
get unstable_getFlexipageFormulaOverrides_imperative () { return unstable_getFlexipageFormulaOverrides_imperative; },
|
|
137405
140608
|
get unstable_getGlobalActions_imperative () { return unstable_getGlobalActions_imperative; },
|
|
@@ -137539,7 +140742,7 @@ withDefaultLuvio((luvio) => {
|
|
|
137539
140742
|
unstable_graphQL_imperative = createImperativeAdapter(luvio, createInstrumentedAdapter(ldsAdapter, adapterMetadata), adapterMetadata);
|
|
137540
140743
|
graphQLImperative = ldsAdapter;
|
|
137541
140744
|
});
|
|
137542
|
-
// version: 1.
|
|
140745
|
+
// version: 1.467.0-049a8b6027
|
|
137543
140746
|
|
|
137544
140747
|
var gqlApi = /*#__PURE__*/Object.freeze({
|
|
137545
140748
|
__proto__: null,
|
|
@@ -138391,7 +141594,7 @@ const callbacks$1 = [];
|
|
|
138391
141594
|
function register(r) {
|
|
138392
141595
|
callbacks$1.forEach((callback) => callback(r));
|
|
138393
141596
|
}
|
|
138394
|
-
// version: 1.
|
|
141597
|
+
// version: 1.467.0-4f065d2a2a
|
|
138395
141598
|
|
|
138396
141599
|
/**
|
|
138397
141600
|
* Copyright (c) 2022, Salesforce, Inc.,
|
|
@@ -139680,4 +142883,4 @@ const { luvio } = getRuntime();
|
|
|
139680
142883
|
setDefaultLuvio({ luvio });
|
|
139681
142884
|
|
|
139682
142885
|
export { createPrimingSession, draftManager, draftQueue, evictCacheRecordsByIds, evictExpiredCacheEntries, executeAdapter, executeMutatingAdapter, getImperativeAdapterNames, importLuvioAdapterModule, importOneStoreAdapterModule, invokeAdapter, invokeAdapterWithDraftToMerge, invokeAdapterWithDraftToMergeAndMetadata, invokeAdapterWithDraftToReplace, invokeAdapterWithMetadata, nimbusDraftQueue, registerReportObserver, setMetadataTTL, setUiApiRecordTTL, stopEviction, subscribeToAdapter };
|
|
139683
|
-
// version: 1.
|
|
142886
|
+
// version: 1.467.0-4f065d2a2a
|