@salesforce/lds-worker-api 1.208.1 → 1.210.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.
@@ -743,22 +743,47 @@ function createPrimingSession(config) {
743
743
  const { primingEventHandler, concurrency, batchSize } = config;
744
744
  const session = createPrimingSession({ concurrency, batchSize });
745
745
  if (primingEventHandler) {
746
- const { onError, onPrimed } = primingEventHandler;
747
- if (onError) {
748
- session.on('error', onError.bind(primingEventHandler));
749
- }
750
- if (onPrimed) {
751
- session.on('primed', onPrimed.bind(primingEventHandler));
752
- }
753
- }
746
+ addEventHandler(session, primingEventHandler);
747
+ }
748
+ const registerEventHandler = (handler) => {
749
+ // bind creates a new function and we need to retain reference to that function
750
+ // in order to be able to remove it
751
+ const boundCallbacks = {
752
+ onError: handler.onError ? handler.onError.bind(handler) : undefined,
753
+ onPrimed: handler.onPrimed ? handler.onPrimed.bind(handler) : undefined,
754
+ };
755
+ addEventHandler(session, boundCallbacks);
756
+ return () => {
757
+ removeEventHandler(session, boundCallbacks);
758
+ };
759
+ };
754
760
  return {
755
761
  enqueue: session.enqueue.bind(session),
756
762
  cancel: session.cancel.bind(session),
757
763
  on: session.on.bind(session),
758
764
  once: session.once.bind(session),
759
765
  off: session.off.bind(session),
766
+ registerEventHandler,
760
767
  };
761
768
  }
769
+ function addEventHandler(session, handler) {
770
+ const { onError, onPrimed } = handler;
771
+ if (onError) {
772
+ session.on('error', onError);
773
+ }
774
+ if (onPrimed) {
775
+ session.on('primed', onPrimed);
776
+ }
777
+ }
778
+ function removeEventHandler(session, handler) {
779
+ const { onError, onPrimed } = handler;
780
+ if (onError) {
781
+ session.off('error', onError);
782
+ }
783
+ if (onPrimed) {
784
+ session.off('primed', onPrimed);
785
+ }
786
+ }
762
787
 
763
788
  if (process.env.NODE_ENV !== 'production') {
764
789
  // eslint-disable-next-line no-undef
@@ -770,4 +795,4 @@ if (process.env.NODE_ENV !== 'production') {
770
795
  }
771
796
 
772
797
  export { createPrimingSession, draftManager, draftQueue, executeAdapter, executeMutatingAdapter, getImperativeAdapterNames, invokeAdapter, invokeAdapterWithDraftToReplace, invokeAdapterWithMetadata, nimbusDraftQueue, setMetadataTTL, setUiApiRecordTTL, subscribeToAdapter };
773
- // version: 1.208.1-8f4c4550e
798
+ // version: 1.210.0-b2655462f
@@ -1,10 +1,11 @@
1
+ import type { PrimingSession } from '@salesforce/lds-priming';
1
2
  interface PrimingError {
2
3
  ids: string[];
3
4
  code: ErrorCode;
4
5
  message: string;
5
6
  }
6
7
  type ErrorCode = 'precondition-error' | 'not-found' | 'service-unavailable' | 'canceled' | 'unknown';
7
- interface PrimingEventHandler {
8
+ export interface PrimingEventHandler {
8
9
  onError?: (error: PrimingError) => void;
9
10
  onPrimed?: (ids: string[]) => void;
10
11
  }
@@ -16,8 +17,9 @@ export interface PrimingSessionConfig {
16
17
  export declare function createPrimingSession(config: PrimingSessionConfig): {
17
18
  enqueue: (work: PrimingWork) => Promise<void>;
18
19
  cancel: () => void;
19
- on: <K extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K]) => void) => import("@salesforce/lds-priming").PrimingSession;
20
- once: <K_1 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_1, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_1]) => void) => import("@salesforce/lds-priming").PrimingSession;
21
- off: <K_2 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_2, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_2]) => void) => import("@salesforce/lds-priming").PrimingSession;
20
+ on: <K extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K]) => void) => PrimingSession;
21
+ once: <K_1 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_1, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_1]) => void) => PrimingSession;
22
+ off: <K_2 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_2, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_2]) => void) => PrimingSession;
23
+ registerEventHandler: (handler: PrimingEventHandler) => () => void;
22
24
  };
23
25
  export {};
@@ -3862,7 +3862,7 @@ function withDefaultLuvio(callback) {
3862
3862
  }
3863
3863
  callbacks.push(callback);
3864
3864
  }
3865
- // version: 1.208.1-8f4c4550e
3865
+ // version: 1.210.0-b2655462f
3866
3866
 
3867
3867
  // TODO [TD-0081508]: once that TD is fulfilled we can probably change this file
3868
3868
  function instrumentAdapter$1(createFunction, _metadata) {
@@ -15295,7 +15295,7 @@ function parseAndVisit(source) {
15295
15295
  updateReferenceMapWithKnownKey(ast, luvioDocumentNode);
15296
15296
  return luvioDocumentNode;
15297
15297
  }
15298
- // version: 1.208.1-8f4c4550e
15298
+ // version: 1.210.0-b2655462f
15299
15299
 
15300
15300
  function unwrap(data) {
15301
15301
  // The lwc-luvio bindings import a function from lwc called "unwrap".
@@ -16218,7 +16218,7 @@ function createGraphQLWireAdapterConstructor(luvio, adapter, metadata, astResolv
16218
16218
  const { apiFamily, name } = metadata;
16219
16219
  return createGraphQLWireAdapterConstructor$1(adapter, `${apiFamily}.${name}`, luvio, astResolver);
16220
16220
  }
16221
- // version: 1.208.1-8f4c4550e
16221
+ // version: 1.210.0-b2655462f
16222
16222
 
16223
16223
  /**
16224
16224
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -43556,7 +43556,7 @@ withDefaultLuvio((luvio) => {
43556
43556
  });
43557
43557
  throttle(60, 60000, createLDSAdapter(luvio, 'notifyListViewSummaryUpdateAvailable', notifyUpdateAvailableFactory));
43558
43558
  });
43559
- // version: 1.208.1-9aaa359ad
43559
+ // version: 1.210.0-2883d2f5f
43560
43560
 
43561
43561
  var caseSensitiveUserId = '005B0000000GR4OIAW';
43562
43562
 
@@ -58066,6 +58066,8 @@ function reportDraftAwareContentVersionSynthesizeCalls(mimeType) {
58066
58066
  }
58067
58067
  function reportPrimingError(errorType, recordCount) {
58068
58068
  }
58069
+ function reportPrimingConflict(resolutionType, recordCount) {
58070
+ }
58069
58071
 
58070
58072
  /**
58071
58073
  * HOF (high-order-function) that instruments any async operation. If the operation
@@ -59041,6 +59043,245 @@ function generateTypedBatches(work, batchSize) {
59041
59043
  return batches;
59042
59044
  }
59043
59045
 
59046
+ function getMissingElementsFromSuperset(superset, subset) {
59047
+ return subset.filter((val) => !superset.includes(val));
59048
+ }
59049
+ function findReferenceFieldForSpanningField(fieldName, objectInfo) {
59050
+ const fieldNames = Object.keys(objectInfo.fields);
59051
+ for (const objectInfoFieldName of fieldNames) {
59052
+ const field = objectInfo.fields[objectInfoFieldName];
59053
+ if (field.reference === true && field.relationshipName === fieldName) {
59054
+ return objectInfoFieldName;
59055
+ }
59056
+ }
59057
+ }
59058
+ function buildFieldUnionArray(existingRecord, incomingRecord, objectInfo) {
59059
+ const allFields = Array.from(new Set([...Object.keys(existingRecord.fields), ...Object.keys(incomingRecord.fields)]));
59060
+ const fieldUnion = [];
59061
+ allFields.forEach((fieldName) => {
59062
+ const objectInfoField = objectInfo.fields[fieldName];
59063
+ if (objectInfoField === undefined) {
59064
+ // find the reference field for the spanning field
59065
+ const referenceField = findReferenceFieldForSpanningField(fieldName, objectInfo);
59066
+ if (referenceField !== undefined) {
59067
+ fieldUnion.push(`${fieldName}.Id`);
59068
+ }
59069
+ }
59070
+ else {
59071
+ fieldUnion.push(fieldName);
59072
+ }
59073
+ });
59074
+ return fieldUnion;
59075
+ }
59076
+ /**
59077
+ * Merges (if possible) an incoming record from a priming session with an existing record in the durable store.
59078
+ *
59079
+ * IMPORTANT NOTE: this is not a suitable function to use for general merging of two DurableRecordRepresentation since it
59080
+ * makes the assumption that the incoming record ONLY contains scalar field values and no spanning records. The same is not
59081
+ * necessarily true for the existing record as it may have been populated in the cache outside of a priming session.
59082
+ * This function should not be moved out of the priming module!
59083
+ *
59084
+ * @param existingRecord Existing record in the durable store
59085
+ * @param incomingRecord Incoming record from the priming session
59086
+ * @param objectInfo Object info for the incoming record type
59087
+ * @returns Merge result describing the success or failure of the merge operation
59088
+ */
59089
+ function mergeRecord(existingRecord, incomingRecord, objectInfo) {
59090
+ // cache already contains everything incoming has
59091
+ if (existingRecord.weakEtag >= incomingRecord.weakEtag &&
59092
+ getMissingElementsFromSuperset(Object.keys(existingRecord.fields), Object.keys(incomingRecord.fields)).length === 0) {
59093
+ return {
59094
+ ok: true,
59095
+ code: 'success',
59096
+ needsWrite: false,
59097
+ record: existingRecord,
59098
+ };
59099
+ }
59100
+ // don't touch records that contain drafts
59101
+ if (existingRecord.drafts !== undefined) {
59102
+ return {
59103
+ ok: false,
59104
+ code: 'conflict-drafts',
59105
+ hasDraft: true,
59106
+ fieldUnion: buildFieldUnionArray(existingRecord, incomingRecord, objectInfo),
59107
+ };
59108
+ }
59109
+ // Check if incoming record's Etag is equal to the existing one
59110
+ if (existingRecord.weakEtag === incomingRecord.weakEtag) {
59111
+ // If so, merge the fields and return the updated record
59112
+ return {
59113
+ ok: true,
59114
+ needsWrite: true,
59115
+ code: 'success',
59116
+ record: {
59117
+ ...existingRecord,
59118
+ fields: {
59119
+ ...existingRecord.fields,
59120
+ ...incomingRecord.fields,
59121
+ },
59122
+ links: {
59123
+ ...existingRecord.links,
59124
+ ...incomingRecord.links,
59125
+ },
59126
+ },
59127
+ };
59128
+ }
59129
+ else if (incomingRecord.weakEtag > existingRecord.weakEtag &&
59130
+ getMissingElementsFromSuperset(Object.keys(incomingRecord.fields), Object.keys(existingRecord.fields)).length === 0) {
59131
+ // If incoming record's Etag is higher and contains all the fields, overwrite the record
59132
+ // NOTE: if existing record contains spanning records, this condition will never hit since incoming won't have those fields
59133
+ return { ok: true, code: 'success', needsWrite: true, record: incomingRecord };
59134
+ }
59135
+ else {
59136
+ const missingFields = getMissingElementsFromSuperset(Object.keys(incomingRecord.fields), Object.keys(existingRecord.fields));
59137
+ // if the only missing fields are spanning fields and their corresponding lookup fields match, we can merge
59138
+ // since none of the changed fields are part of the incoming record
59139
+ if (missingFields.every((field) => {
59140
+ const referenceFieldName = findReferenceFieldForSpanningField(field, objectInfo);
59141
+ if (referenceFieldName !== undefined) {
59142
+ return (incomingRecord.fields[referenceFieldName].value ===
59143
+ existingRecord.fields[referenceFieldName].value);
59144
+ }
59145
+ else {
59146
+ return false;
59147
+ }
59148
+ })) {
59149
+ return {
59150
+ ok: true,
59151
+ needsWrite: true,
59152
+ code: 'success',
59153
+ record: {
59154
+ // we span the existing record to maintain spanning references
59155
+ ...incomingRecord,
59156
+ fields: {
59157
+ ...existingRecord.fields,
59158
+ ...incomingRecord.fields,
59159
+ },
59160
+ links: {
59161
+ ...existingRecord.links,
59162
+ ...incomingRecord.links,
59163
+ },
59164
+ },
59165
+ };
59166
+ }
59167
+ // If Etags do not match and the incoming record does not contain all fields, re-request the record
59168
+ return {
59169
+ ok: false,
59170
+ code: 'conflict-missing-fields',
59171
+ fieldUnion: buildFieldUnionArray(existingRecord, incomingRecord, objectInfo),
59172
+ hasDraft: false,
59173
+ };
59174
+ }
59175
+ }
59176
+
59177
+ const CONFLICT_POOL_SIZE = 5;
59178
+ /**
59179
+ * A pool of workers that resolve conflicts between incoming records and records in the store.
59180
+ */
59181
+ class ConflictPool {
59182
+ constructor(store, objectInfoLoader) {
59183
+ this.store = store;
59184
+ this.objectInfoLoader = objectInfoLoader;
59185
+ this.pool = new AsyncWorkerPool(CONFLICT_POOL_SIZE);
59186
+ }
59187
+ enqueueConflictedRecords(records, abortController) {
59188
+ return this.pool.push({
59189
+ workFn: () => this.resolveConflicts(records, abortController),
59190
+ });
59191
+ }
59192
+ async resolveConflicts(incomingRecords, abortController) {
59193
+ const result = {
59194
+ additionalWork: { type: 'record-fields', records: {} },
59195
+ recordsToWrite: [],
59196
+ resolvedRecords: [],
59197
+ recordsNeedingRefetch: new Map(),
59198
+ errors: [],
59199
+ };
59200
+ const ids = [];
59201
+ const trackedFieldsByType = new Map();
59202
+ const apiNames = new Set();
59203
+ incomingRecords.forEach((record) => {
59204
+ ids.push(record.id);
59205
+ apiNames.add(record.apiName);
59206
+ });
59207
+ const existingRecords = await this.store.readRecords(ids);
59208
+ if (abortController.aborted) {
59209
+ return result;
59210
+ }
59211
+ const objectInfos = await this.objectInfoLoader.getObjectInfos(Array.from(apiNames));
59212
+ if (abortController.aborted) {
59213
+ return result;
59214
+ }
59215
+ const existingRecordsById = new Map(existingRecords.map((record) => [record.record.id, record]));
59216
+ for (const incomingRecord of incomingRecords) {
59217
+ const existingDurableRecordRepresentation = existingRecordsById.get(incomingRecord.id);
59218
+ const objectInfo = objectInfos[incomingRecord.apiName];
59219
+ if (existingDurableRecordRepresentation === undefined) {
59220
+ // this shouldn't happen but if it does, we should write the incoming record since there's nothing to merge
59221
+ result.recordsToWrite.push(incomingRecord);
59222
+ continue;
59223
+ }
59224
+ if (objectInfo === undefined) {
59225
+ // object infos are a prerequisite for priming so if we don't have one, we can't do anything
59226
+ result.errors.push({ id: incomingRecord.id, reason: 'object-info-missing' });
59227
+ continue;
59228
+ }
59229
+ const existingRecord = existingDurableRecordRepresentation.record;
59230
+ const mergedRecordResult = mergeRecord(existingRecord, incomingRecord, objectInfo);
59231
+ if (mergedRecordResult.ok) {
59232
+ if (mergedRecordResult.needsWrite) {
59233
+ result.recordsToWrite.push(mergedRecordResult.record);
59234
+ }
59235
+ else {
59236
+ result.resolvedRecords.push(mergedRecordResult.record.id);
59237
+ }
59238
+ continue;
59239
+ }
59240
+ else {
59241
+ const { code } = mergedRecordResult;
59242
+ const isConflict = code === 'conflict-drafts' ||
59243
+ code === 'conflict-spanning-record' ||
59244
+ code === 'conflict-missing-fields';
59245
+ if (isConflict) {
59246
+ let trackedFields = trackedFieldsByType.get(incomingRecord.apiName);
59247
+ if (trackedFields === undefined) {
59248
+ trackedFields = new Set();
59249
+ trackedFieldsByType.set(incomingRecord.apiName, trackedFields);
59250
+ }
59251
+ mergedRecordResult.fieldUnion.forEach((field) => trackedFields.add(field));
59252
+ if (code === 'conflict-missing-fields') {
59253
+ const additionalWorkForType = result.additionalWork.records[incomingRecord.apiName];
59254
+ if (additionalWorkForType === undefined) {
59255
+ result.additionalWork.records[incomingRecord.apiName] = {
59256
+ ids: [incomingRecord.id],
59257
+ fields: Array.from(trackedFields),
59258
+ };
59259
+ }
59260
+ else {
59261
+ additionalWorkForType.ids.push(incomingRecord.id);
59262
+ additionalWorkForType.fields = Array.from(trackedFields);
59263
+ }
59264
+ }
59265
+ else if (code === 'conflict-drafts' || code === 'conflict-spanning-record') {
59266
+ const recordByType = result.recordsNeedingRefetch.get(incomingRecord.apiName);
59267
+ if (recordByType === undefined) {
59268
+ result.recordsNeedingRefetch.set(incomingRecord.apiName, {
59269
+ ids: [incomingRecord.id],
59270
+ fields: Array.from(trackedFields),
59271
+ });
59272
+ }
59273
+ else {
59274
+ recordByType.ids.push(incomingRecord.id);
59275
+ recordByType.fields = Array.from(trackedFields);
59276
+ }
59277
+ }
59278
+ }
59279
+ }
59280
+ }
59281
+ return result;
59282
+ }
59283
+ }
59284
+
59044
59285
  const DEFAULT_BATCH_SIZE = 500;
59045
59286
  const DEFAULT_CONCURRENCY = 6;
59046
59287
  const DEFAULT_GQL_QUERY_BATCH_SIZE = 5;
@@ -59054,8 +59295,10 @@ class PrimingSession extends EventEmitter {
59054
59295
  this.recordLoader = config.recordLoader;
59055
59296
  this.recordIngestor = config.recordIngestor;
59056
59297
  this.objectInfoLoader = config.objectInfoLoader;
59298
+ this.ldsRecordRefresher = config.ldsRecordRefresher;
59057
59299
  this.networkWorkerPool = new AsyncWorkerPool(this.concurrency);
59058
59300
  this.useBatchGQL = ldsPrimingGraphqlBatch.isOpen({ fallback: false });
59301
+ this.conflictPool = new ConflictPool(config.store, this.objectInfoLoader);
59059
59302
  }
59060
59303
  // function that enqueues priming work
59061
59304
  async enqueue(work) {
@@ -59180,7 +59423,9 @@ class PrimingSession extends EventEmitter {
59180
59423
  const { records } = result;
59181
59424
  const beforeWrite = Date.now();
59182
59425
  // dispatch the write but DO NOT wait on it to unblock the network pool
59183
- this.recordIngestor.insertRecords(records).then(({ written, conflicted, errors }) => {
59426
+ this.recordIngestor
59427
+ .insertRecords(records, false)
59428
+ .then(({ written, conflicted, errors }) => {
59184
59429
  this.emit('batch-written', {
59185
59430
  written,
59186
59431
  conflicted,
@@ -59203,16 +59448,69 @@ class PrimingSession extends EventEmitter {
59203
59448
  if (written.length > 0) {
59204
59449
  this.emit('primed', Array.from(written));
59205
59450
  }
59206
- // TODO [W-12436213]: implement conflict resolution
59451
+ // if any records could not be written to the store because there were conflicts, handle the conflicts
59452
+ if (conflicted.length > 0) {
59453
+ this.handleWriteConflicts(records, conflicted, abortController);
59454
+ }
59455
+ });
59456
+ }
59457
+ async handleWriteConflicts(records, conflicted, abortController) {
59458
+ const result = await this.conflictPool.enqueueConflictedRecords(records.filter((x) => conflicted.includes(x.id)), abortController);
59459
+ if (abortController.aborted) {
59460
+ return;
59461
+ }
59462
+ if (Object.keys(result.additionalWork.records).length > 0) {
59463
+ this.emit('conflict', {
59464
+ ids: Object.values(result.additionalWork.records).flatMap((record) => record.ids),
59465
+ resolution: 'priming-refresh',
59466
+ });
59467
+ this.enqueue(result.additionalWork);
59468
+ }
59469
+ if (result.resolvedRecords.length > 0) {
59470
+ this.emit('conflict', {
59471
+ ids: result.resolvedRecords,
59472
+ resolution: 'priming-merge',
59473
+ });
59474
+ this.emit('primed', result.resolvedRecords);
59475
+ }
59476
+ if (result.recordsToWrite.length > 0) {
59477
+ const { written, errors, conflicted } = await this.recordIngestor.insertRecords(result.recordsToWrite, true);
59478
+ if (written.length > 0) {
59479
+ const ids = Array.from(written);
59480
+ this.emit('conflict', { ids, resolution: 'priming-merge' });
59481
+ this.emit('primed', ids);
59482
+ }
59483
+ if (errors.length > 0) {
59484
+ errors.forEach(({ ids, message }) => {
59485
+ this.emit('error', {
59486
+ ids,
59487
+ code: 'unknown',
59488
+ message: message,
59489
+ });
59490
+ });
59491
+ }
59207
59492
  if (conflicted.length > 0) {
59208
- // for now emit conlicts as errors
59209
59493
  this.emit('error', {
59210
- ids: Array.from(conflicted),
59494
+ ids: conflicted,
59211
59495
  code: 'unknown',
59212
- message: 'conflict when persisting record',
59496
+ message: 'unexpected write conflict',
59213
59497
  });
59214
59498
  }
59215
- });
59499
+ }
59500
+ if (result.recordsNeedingRefetch.size > 0) {
59501
+ const { loaded, errored } = await this.ldsRecordRefresher.loadRecords(result.recordsNeedingRefetch);
59502
+ if (loaded.length > 0) {
59503
+ this.emit('conflict', { resolution: 'lds-refresh', ids: loaded });
59504
+ this.emit('primed', loaded);
59505
+ }
59506
+ if (errored.length > 0) {
59507
+ this.emit('error', {
59508
+ ids: errored,
59509
+ code: 'unknown',
59510
+ message: `could not resolve conflicts`,
59511
+ });
59512
+ }
59513
+ }
59216
59514
  }
59217
59515
  async fetchMetadata(batches) {
59218
59516
  const apiNames = Array.from(batches.reduce((acc, x) => {
@@ -59451,6 +59749,9 @@ function instrumentPrimingSession(session) {
59451
59749
  });
59452
59750
  session.on('primed', ({ length }) => {
59453
59751
  });
59752
+ session.on('conflict', ({ ids, resolution }) => {
59753
+ reportPrimingConflict(resolution, ids.length);
59754
+ });
59454
59755
  return session;
59455
59756
  }
59456
59757
 
@@ -59632,19 +59933,72 @@ function batchArray(arr, batchSize = BATCH_SIZE) {
59632
59933
  return batches;
59633
59934
  }
59634
59935
 
59936
+ /**
59937
+ * A polyfill for Promise.allSettled
59938
+ * @param promises An array of promises
59939
+ * @returns the result of all the promises when they either fulfill or reject
59940
+ */
59941
+ function allSettled(promises) {
59942
+ let wrappedPromises = promises.map((p) => Promise.resolve(p).then((value) => ({ status: 'fulfilled', value }), (reason) => ({ status: 'rejected', reason })));
59943
+ return Promise.all(wrappedPromises);
59944
+ }
59945
+
59946
+ class LdsPrimingRecordRefresher {
59947
+ constructor(getRecordsAdapter) {
59948
+ this.getRecordsAdapter = getRecordsAdapter;
59949
+ }
59950
+ async loadRecords(records) {
59951
+ const requestedRecords = new Set();
59952
+ const promises = Array.from(records).flatMap(([_apiName, value]) => {
59953
+ value.ids.forEach((id) => requestedRecords.add(id));
59954
+ return Promise.resolve(this.getRecordsAdapter({
59955
+ records: [
59956
+ {
59957
+ recordIds: value.ids,
59958
+ optionalFields: value.fields.map((f) => `${_apiName}.${f}`),
59959
+ },
59960
+ ],
59961
+ }));
59962
+ });
59963
+ const promiseResults = await allSettled(promises);
59964
+ const loaded = [];
59965
+ promiseResults.forEach((promiseResult) => {
59966
+ if (promiseResult.status === 'fulfilled') {
59967
+ const batchResultRepresenatation = promiseResult.value;
59968
+ if (batchResultRepresenatation &&
59969
+ batchResultRepresenatation.state === 'Fulfilled') {
59970
+ batchResultRepresenatation.data.results.forEach((result) => {
59971
+ if (result.statusCode === 200) {
59972
+ const id = result.result.id;
59973
+ loaded.push(id);
59974
+ requestedRecords.delete(id);
59975
+ }
59976
+ });
59977
+ }
59978
+ }
59979
+ });
59980
+ // errored contains all the requestedRecords that weren't loaded
59981
+ const errored = Array.from(requestedRecords);
59982
+ return { loaded, errored };
59983
+ }
59984
+ }
59985
+
59635
59986
  function primingSessionFactory(config) {
59636
59987
  const { store, objectInfoService, getLuvio } = config;
59637
59988
  const networkAdapter = new NimbusPrimingNetworkAdapter();
59638
59989
  const recordLoader = new RecordLoaderGraphQL(networkAdapter);
59639
- const recordIngestor = new RecordIngestor(new SqlitePrimingStore(getLuvio, store), getLuvio);
59990
+ const primingStore = new SqlitePrimingStore(getLuvio, store);
59991
+ const recordIngestor = new RecordIngestor(primingStore, getLuvio);
59640
59992
  const session = new PrimingSession({
59641
59993
  recordLoader,
59642
59994
  recordIngestor,
59995
+ store: primingStore,
59643
59996
  objectInfoLoader: {
59644
59997
  getObjectInfos: objectInfoService.getObjectInfos.bind(objectInfoService),
59645
59998
  },
59646
59999
  concurrency: config.concurrency,
59647
60000
  batchSize: config.batchSize,
60001
+ ldsRecordRefresher: new LdsPrimingRecordRefresher(config.getRecords),
59648
60002
  });
59649
60003
  return instrumentPrimingSession(session);
59650
60004
  }
@@ -59658,6 +60012,7 @@ let lazyEnvironment;
59658
60012
  let lazyBaseDurableStore;
59659
60013
  let lazyNetworkAdapter;
59660
60014
  let lazyObjectInfoService;
60015
+ let lazyGetRecords;
59661
60016
  /**
59662
60017
  * This returns the LDS on Mobile Runtime singleton object.
59663
60018
  */
@@ -59728,6 +60083,7 @@ function getRuntime() {
59728
60083
  lazyLuvio = new Luvio(lazyEnvironment, {
59729
60084
  instrument: instrumentLuvio,
59730
60085
  });
60086
+ lazyGetRecords = getRecordsAdapterFactory(lazyLuvio);
59731
60087
  // Currently instruments store runtime perf
59732
60088
  setupMobileInstrumentation();
59733
60089
  // If the inspection nimbus plugin is configured, inspection is enabled otherwise this is a no-op
@@ -59781,6 +60137,7 @@ function getRuntime() {
59781
60137
  getLuvio: () => lazyLuvio,
59782
60138
  concurrency: config.concurrency,
59783
60139
  batchSize: config.batchSize,
60140
+ getRecords: lazyGetRecords,
59784
60141
  });
59785
60142
  },
59786
60143
  };
@@ -59797,7 +60154,7 @@ register({
59797
60154
  id: '@salesforce/lds-network-adapter',
59798
60155
  instrument: instrument$1,
59799
60156
  });
59800
- // version: 1.208.1-8f4c4550e
60157
+ // version: 1.210.0-b2655462f
59801
60158
 
59802
60159
  const { create: create$2, keys: keys$2 } = Object;
59803
60160
  const { stringify: stringify$1, parse: parse$1 } = JSON;
@@ -77140,7 +77497,7 @@ register({
77140
77497
  configuration: { ...configurationForGraphQLAdapters },
77141
77498
  instrument,
77142
77499
  });
77143
- // version: 1.208.1-9aaa359ad
77500
+ // version: 1.210.0-2883d2f5f
77144
77501
 
77145
77502
  // On core the unstable adapters are re-exported with different names,
77146
77503
 
@@ -79387,7 +79744,7 @@ withDefaultLuvio((luvio) => {
79387
79744
  unstable_graphQL_imperative = createImperativeAdapter(luvio, createInstrumentedAdapter(ldsAdapter, adapterMetadata), adapterMetadata);
79388
79745
  graphQLImperative = ldsAdapter;
79389
79746
  });
79390
- // version: 1.208.1-9aaa359ad
79747
+ // version: 1.210.0-2883d2f5f
79391
79748
 
79392
79749
  var gqlApi = /*#__PURE__*/Object.freeze({
79393
79750
  __proto__: null,
@@ -80054,26 +80411,51 @@ function createPrimingSession(config) {
80054
80411
  const { primingEventHandler, concurrency, batchSize } = config;
80055
80412
  const session = createPrimingSession({ concurrency, batchSize });
80056
80413
  if (primingEventHandler) {
80057
- const { onError, onPrimed } = primingEventHandler;
80058
- if (onError) {
80059
- session.on('error', onError.bind(primingEventHandler));
80060
- }
80061
- if (onPrimed) {
80062
- session.on('primed', onPrimed.bind(primingEventHandler));
80063
- }
80064
- }
80414
+ addEventHandler(session, primingEventHandler);
80415
+ }
80416
+ const registerEventHandler = (handler) => {
80417
+ // bind creates a new function and we need to retain reference to that function
80418
+ // in order to be able to remove it
80419
+ const boundCallbacks = {
80420
+ onError: handler.onError ? handler.onError.bind(handler) : undefined,
80421
+ onPrimed: handler.onPrimed ? handler.onPrimed.bind(handler) : undefined,
80422
+ };
80423
+ addEventHandler(session, boundCallbacks);
80424
+ return () => {
80425
+ removeEventHandler(session, boundCallbacks);
80426
+ };
80427
+ };
80065
80428
  return {
80066
80429
  enqueue: session.enqueue.bind(session),
80067
80430
  cancel: session.cancel.bind(session),
80068
80431
  on: session.on.bind(session),
80069
80432
  once: session.once.bind(session),
80070
80433
  off: session.off.bind(session),
80434
+ registerEventHandler,
80071
80435
  };
80072
80436
  }
80437
+ function addEventHandler(session, handler) {
80438
+ const { onError, onPrimed } = handler;
80439
+ if (onError) {
80440
+ session.on('error', onError);
80441
+ }
80442
+ if (onPrimed) {
80443
+ session.on('primed', onPrimed);
80444
+ }
80445
+ }
80446
+ function removeEventHandler(session, handler) {
80447
+ const { onError, onPrimed } = handler;
80448
+ if (onError) {
80449
+ session.off('error', onError);
80450
+ }
80451
+ if (onPrimed) {
80452
+ session.off('primed', onPrimed);
80453
+ }
80454
+ }
80073
80455
 
80074
80456
  // LWR has a "setupLDS" bootstrap service/loader hook, we simulate this in
80075
80457
  const { luvio } = getRuntime();
80076
80458
  setDefaultLuvio({ luvio });
80077
80459
 
80078
80460
  export { createPrimingSession, draftManager, draftQueue, executeAdapter, executeMutatingAdapter, getImperativeAdapterNames, invokeAdapter, invokeAdapterWithDraftToReplace, invokeAdapterWithMetadata, nimbusDraftQueue, registerReportObserver, setMetadataTTL, setUiApiRecordTTL, subscribeToAdapter };
80079
- // version: 1.208.1-8f4c4550e
80461
+ // version: 1.210.0-b2655462f
@@ -1,10 +1,11 @@
1
+ import type { PrimingSession } from '@salesforce/lds-priming';
1
2
  interface PrimingError {
2
3
  ids: string[];
3
4
  code: ErrorCode;
4
5
  message: string;
5
6
  }
6
7
  type ErrorCode = 'precondition-error' | 'not-found' | 'service-unavailable' | 'canceled' | 'unknown';
7
- interface PrimingEventHandler {
8
+ export interface PrimingEventHandler {
8
9
  onError?: (error: PrimingError) => void;
9
10
  onPrimed?: (ids: string[]) => void;
10
11
  }
@@ -16,8 +17,9 @@ export interface PrimingSessionConfig {
16
17
  export declare function createPrimingSession(config: PrimingSessionConfig): {
17
18
  enqueue: (work: PrimingWork) => Promise<void>;
18
19
  cancel: () => void;
19
- on: <K extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K]) => void) => import("@salesforce/lds-priming").PrimingSession;
20
- once: <K_1 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_1, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_1]) => void) => import("@salesforce/lds-priming").PrimingSession;
21
- off: <K_2 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_2, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_2]) => void) => import("@salesforce/lds-priming").PrimingSession;
20
+ on: <K extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K]) => void) => PrimingSession;
21
+ once: <K_1 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_1, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_1]) => void) => PrimingSession;
22
+ off: <K_2 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_2, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_2]) => void) => PrimingSession;
23
+ registerEventHandler: (handler: PrimingEventHandler) => () => void;
22
24
  };
23
25
  export {};
@@ -3868,7 +3868,7 @@
3868
3868
  }
3869
3869
  callbacks.push(callback);
3870
3870
  }
3871
- // version: 1.208.1-8f4c4550e
3871
+ // version: 1.210.0-b2655462f
3872
3872
 
3873
3873
  // TODO [TD-0081508]: once that TD is fulfilled we can probably change this file
3874
3874
  function instrumentAdapter$1(createFunction, _metadata) {
@@ -15301,7 +15301,7 @@
15301
15301
  updateReferenceMapWithKnownKey(ast, luvioDocumentNode);
15302
15302
  return luvioDocumentNode;
15303
15303
  }
15304
- // version: 1.208.1-8f4c4550e
15304
+ // version: 1.210.0-b2655462f
15305
15305
 
15306
15306
  function unwrap(data) {
15307
15307
  // The lwc-luvio bindings import a function from lwc called "unwrap".
@@ -16224,7 +16224,7 @@
16224
16224
  const { apiFamily, name } = metadata;
16225
16225
  return createGraphQLWireAdapterConstructor$1(adapter, `${apiFamily}.${name}`, luvio, astResolver);
16226
16226
  }
16227
- // version: 1.208.1-8f4c4550e
16227
+ // version: 1.210.0-b2655462f
16228
16228
 
16229
16229
  /**
16230
16230
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -43562,7 +43562,7 @@
43562
43562
  });
43563
43563
  throttle(60, 60000, createLDSAdapter(luvio, 'notifyListViewSummaryUpdateAvailable', notifyUpdateAvailableFactory));
43564
43564
  });
43565
- // version: 1.208.1-9aaa359ad
43565
+ // version: 1.210.0-2883d2f5f
43566
43566
 
43567
43567
  var caseSensitiveUserId = '005B0000000GR4OIAW';
43568
43568
 
@@ -58072,6 +58072,8 @@
58072
58072
  }
58073
58073
  function reportPrimingError(errorType, recordCount) {
58074
58074
  }
58075
+ function reportPrimingConflict(resolutionType, recordCount) {
58076
+ }
58075
58077
 
58076
58078
  /**
58077
58079
  * HOF (high-order-function) that instruments any async operation. If the operation
@@ -59047,6 +59049,245 @@
59047
59049
  return batches;
59048
59050
  }
59049
59051
 
59052
+ function getMissingElementsFromSuperset(superset, subset) {
59053
+ return subset.filter((val) => !superset.includes(val));
59054
+ }
59055
+ function findReferenceFieldForSpanningField(fieldName, objectInfo) {
59056
+ const fieldNames = Object.keys(objectInfo.fields);
59057
+ for (const objectInfoFieldName of fieldNames) {
59058
+ const field = objectInfo.fields[objectInfoFieldName];
59059
+ if (field.reference === true && field.relationshipName === fieldName) {
59060
+ return objectInfoFieldName;
59061
+ }
59062
+ }
59063
+ }
59064
+ function buildFieldUnionArray(existingRecord, incomingRecord, objectInfo) {
59065
+ const allFields = Array.from(new Set([...Object.keys(existingRecord.fields), ...Object.keys(incomingRecord.fields)]));
59066
+ const fieldUnion = [];
59067
+ allFields.forEach((fieldName) => {
59068
+ const objectInfoField = objectInfo.fields[fieldName];
59069
+ if (objectInfoField === undefined) {
59070
+ // find the reference field for the spanning field
59071
+ const referenceField = findReferenceFieldForSpanningField(fieldName, objectInfo);
59072
+ if (referenceField !== undefined) {
59073
+ fieldUnion.push(`${fieldName}.Id`);
59074
+ }
59075
+ }
59076
+ else {
59077
+ fieldUnion.push(fieldName);
59078
+ }
59079
+ });
59080
+ return fieldUnion;
59081
+ }
59082
+ /**
59083
+ * Merges (if possible) an incoming record from a priming session with an existing record in the durable store.
59084
+ *
59085
+ * IMPORTANT NOTE: this is not a suitable function to use for general merging of two DurableRecordRepresentation since it
59086
+ * makes the assumption that the incoming record ONLY contains scalar field values and no spanning records. The same is not
59087
+ * necessarily true for the existing record as it may have been populated in the cache outside of a priming session.
59088
+ * This function should not be moved out of the priming module!
59089
+ *
59090
+ * @param existingRecord Existing record in the durable store
59091
+ * @param incomingRecord Incoming record from the priming session
59092
+ * @param objectInfo Object info for the incoming record type
59093
+ * @returns Merge result describing the success or failure of the merge operation
59094
+ */
59095
+ function mergeRecord(existingRecord, incomingRecord, objectInfo) {
59096
+ // cache already contains everything incoming has
59097
+ if (existingRecord.weakEtag >= incomingRecord.weakEtag &&
59098
+ getMissingElementsFromSuperset(Object.keys(existingRecord.fields), Object.keys(incomingRecord.fields)).length === 0) {
59099
+ return {
59100
+ ok: true,
59101
+ code: 'success',
59102
+ needsWrite: false,
59103
+ record: existingRecord,
59104
+ };
59105
+ }
59106
+ // don't touch records that contain drafts
59107
+ if (existingRecord.drafts !== undefined) {
59108
+ return {
59109
+ ok: false,
59110
+ code: 'conflict-drafts',
59111
+ hasDraft: true,
59112
+ fieldUnion: buildFieldUnionArray(existingRecord, incomingRecord, objectInfo),
59113
+ };
59114
+ }
59115
+ // Check if incoming record's Etag is equal to the existing one
59116
+ if (existingRecord.weakEtag === incomingRecord.weakEtag) {
59117
+ // If so, merge the fields and return the updated record
59118
+ return {
59119
+ ok: true,
59120
+ needsWrite: true,
59121
+ code: 'success',
59122
+ record: {
59123
+ ...existingRecord,
59124
+ fields: {
59125
+ ...existingRecord.fields,
59126
+ ...incomingRecord.fields,
59127
+ },
59128
+ links: {
59129
+ ...existingRecord.links,
59130
+ ...incomingRecord.links,
59131
+ },
59132
+ },
59133
+ };
59134
+ }
59135
+ else if (incomingRecord.weakEtag > existingRecord.weakEtag &&
59136
+ getMissingElementsFromSuperset(Object.keys(incomingRecord.fields), Object.keys(existingRecord.fields)).length === 0) {
59137
+ // If incoming record's Etag is higher and contains all the fields, overwrite the record
59138
+ // NOTE: if existing record contains spanning records, this condition will never hit since incoming won't have those fields
59139
+ return { ok: true, code: 'success', needsWrite: true, record: incomingRecord };
59140
+ }
59141
+ else {
59142
+ const missingFields = getMissingElementsFromSuperset(Object.keys(incomingRecord.fields), Object.keys(existingRecord.fields));
59143
+ // if the only missing fields are spanning fields and their corresponding lookup fields match, we can merge
59144
+ // since none of the changed fields are part of the incoming record
59145
+ if (missingFields.every((field) => {
59146
+ const referenceFieldName = findReferenceFieldForSpanningField(field, objectInfo);
59147
+ if (referenceFieldName !== undefined) {
59148
+ return (incomingRecord.fields[referenceFieldName].value ===
59149
+ existingRecord.fields[referenceFieldName].value);
59150
+ }
59151
+ else {
59152
+ return false;
59153
+ }
59154
+ })) {
59155
+ return {
59156
+ ok: true,
59157
+ needsWrite: true,
59158
+ code: 'success',
59159
+ record: {
59160
+ // we span the existing record to maintain spanning references
59161
+ ...incomingRecord,
59162
+ fields: {
59163
+ ...existingRecord.fields,
59164
+ ...incomingRecord.fields,
59165
+ },
59166
+ links: {
59167
+ ...existingRecord.links,
59168
+ ...incomingRecord.links,
59169
+ },
59170
+ },
59171
+ };
59172
+ }
59173
+ // If Etags do not match and the incoming record does not contain all fields, re-request the record
59174
+ return {
59175
+ ok: false,
59176
+ code: 'conflict-missing-fields',
59177
+ fieldUnion: buildFieldUnionArray(existingRecord, incomingRecord, objectInfo),
59178
+ hasDraft: false,
59179
+ };
59180
+ }
59181
+ }
59182
+
59183
+ const CONFLICT_POOL_SIZE = 5;
59184
+ /**
59185
+ * A pool of workers that resolve conflicts between incoming records and records in the store.
59186
+ */
59187
+ class ConflictPool {
59188
+ constructor(store, objectInfoLoader) {
59189
+ this.store = store;
59190
+ this.objectInfoLoader = objectInfoLoader;
59191
+ this.pool = new AsyncWorkerPool(CONFLICT_POOL_SIZE);
59192
+ }
59193
+ enqueueConflictedRecords(records, abortController) {
59194
+ return this.pool.push({
59195
+ workFn: () => this.resolveConflicts(records, abortController),
59196
+ });
59197
+ }
59198
+ async resolveConflicts(incomingRecords, abortController) {
59199
+ const result = {
59200
+ additionalWork: { type: 'record-fields', records: {} },
59201
+ recordsToWrite: [],
59202
+ resolvedRecords: [],
59203
+ recordsNeedingRefetch: new Map(),
59204
+ errors: [],
59205
+ };
59206
+ const ids = [];
59207
+ const trackedFieldsByType = new Map();
59208
+ const apiNames = new Set();
59209
+ incomingRecords.forEach((record) => {
59210
+ ids.push(record.id);
59211
+ apiNames.add(record.apiName);
59212
+ });
59213
+ const existingRecords = await this.store.readRecords(ids);
59214
+ if (abortController.aborted) {
59215
+ return result;
59216
+ }
59217
+ const objectInfos = await this.objectInfoLoader.getObjectInfos(Array.from(apiNames));
59218
+ if (abortController.aborted) {
59219
+ return result;
59220
+ }
59221
+ const existingRecordsById = new Map(existingRecords.map((record) => [record.record.id, record]));
59222
+ for (const incomingRecord of incomingRecords) {
59223
+ const existingDurableRecordRepresentation = existingRecordsById.get(incomingRecord.id);
59224
+ const objectInfo = objectInfos[incomingRecord.apiName];
59225
+ if (existingDurableRecordRepresentation === undefined) {
59226
+ // this shouldn't happen but if it does, we should write the incoming record since there's nothing to merge
59227
+ result.recordsToWrite.push(incomingRecord);
59228
+ continue;
59229
+ }
59230
+ if (objectInfo === undefined) {
59231
+ // object infos are a prerequisite for priming so if we don't have one, we can't do anything
59232
+ result.errors.push({ id: incomingRecord.id, reason: 'object-info-missing' });
59233
+ continue;
59234
+ }
59235
+ const existingRecord = existingDurableRecordRepresentation.record;
59236
+ const mergedRecordResult = mergeRecord(existingRecord, incomingRecord, objectInfo);
59237
+ if (mergedRecordResult.ok) {
59238
+ if (mergedRecordResult.needsWrite) {
59239
+ result.recordsToWrite.push(mergedRecordResult.record);
59240
+ }
59241
+ else {
59242
+ result.resolvedRecords.push(mergedRecordResult.record.id);
59243
+ }
59244
+ continue;
59245
+ }
59246
+ else {
59247
+ const { code } = mergedRecordResult;
59248
+ const isConflict = code === 'conflict-drafts' ||
59249
+ code === 'conflict-spanning-record' ||
59250
+ code === 'conflict-missing-fields';
59251
+ if (isConflict) {
59252
+ let trackedFields = trackedFieldsByType.get(incomingRecord.apiName);
59253
+ if (trackedFields === undefined) {
59254
+ trackedFields = new Set();
59255
+ trackedFieldsByType.set(incomingRecord.apiName, trackedFields);
59256
+ }
59257
+ mergedRecordResult.fieldUnion.forEach((field) => trackedFields.add(field));
59258
+ if (code === 'conflict-missing-fields') {
59259
+ const additionalWorkForType = result.additionalWork.records[incomingRecord.apiName];
59260
+ if (additionalWorkForType === undefined) {
59261
+ result.additionalWork.records[incomingRecord.apiName] = {
59262
+ ids: [incomingRecord.id],
59263
+ fields: Array.from(trackedFields),
59264
+ };
59265
+ }
59266
+ else {
59267
+ additionalWorkForType.ids.push(incomingRecord.id);
59268
+ additionalWorkForType.fields = Array.from(trackedFields);
59269
+ }
59270
+ }
59271
+ else if (code === 'conflict-drafts' || code === 'conflict-spanning-record') {
59272
+ const recordByType = result.recordsNeedingRefetch.get(incomingRecord.apiName);
59273
+ if (recordByType === undefined) {
59274
+ result.recordsNeedingRefetch.set(incomingRecord.apiName, {
59275
+ ids: [incomingRecord.id],
59276
+ fields: Array.from(trackedFields),
59277
+ });
59278
+ }
59279
+ else {
59280
+ recordByType.ids.push(incomingRecord.id);
59281
+ recordByType.fields = Array.from(trackedFields);
59282
+ }
59283
+ }
59284
+ }
59285
+ }
59286
+ }
59287
+ return result;
59288
+ }
59289
+ }
59290
+
59050
59291
  const DEFAULT_BATCH_SIZE = 500;
59051
59292
  const DEFAULT_CONCURRENCY = 6;
59052
59293
  const DEFAULT_GQL_QUERY_BATCH_SIZE = 5;
@@ -59060,8 +59301,10 @@
59060
59301
  this.recordLoader = config.recordLoader;
59061
59302
  this.recordIngestor = config.recordIngestor;
59062
59303
  this.objectInfoLoader = config.objectInfoLoader;
59304
+ this.ldsRecordRefresher = config.ldsRecordRefresher;
59063
59305
  this.networkWorkerPool = new AsyncWorkerPool(this.concurrency);
59064
59306
  this.useBatchGQL = ldsPrimingGraphqlBatch.isOpen({ fallback: false });
59307
+ this.conflictPool = new ConflictPool(config.store, this.objectInfoLoader);
59065
59308
  }
59066
59309
  // function that enqueues priming work
59067
59310
  async enqueue(work) {
@@ -59186,7 +59429,9 @@
59186
59429
  const { records } = result;
59187
59430
  const beforeWrite = Date.now();
59188
59431
  // dispatch the write but DO NOT wait on it to unblock the network pool
59189
- this.recordIngestor.insertRecords(records).then(({ written, conflicted, errors }) => {
59432
+ this.recordIngestor
59433
+ .insertRecords(records, false)
59434
+ .then(({ written, conflicted, errors }) => {
59190
59435
  this.emit('batch-written', {
59191
59436
  written,
59192
59437
  conflicted,
@@ -59209,16 +59454,69 @@
59209
59454
  if (written.length > 0) {
59210
59455
  this.emit('primed', Array.from(written));
59211
59456
  }
59212
- // TODO [W-12436213]: implement conflict resolution
59457
+ // if any records could not be written to the store because there were conflicts, handle the conflicts
59458
+ if (conflicted.length > 0) {
59459
+ this.handleWriteConflicts(records, conflicted, abortController);
59460
+ }
59461
+ });
59462
+ }
59463
+ async handleWriteConflicts(records, conflicted, abortController) {
59464
+ const result = await this.conflictPool.enqueueConflictedRecords(records.filter((x) => conflicted.includes(x.id)), abortController);
59465
+ if (abortController.aborted) {
59466
+ return;
59467
+ }
59468
+ if (Object.keys(result.additionalWork.records).length > 0) {
59469
+ this.emit('conflict', {
59470
+ ids: Object.values(result.additionalWork.records).flatMap((record) => record.ids),
59471
+ resolution: 'priming-refresh',
59472
+ });
59473
+ this.enqueue(result.additionalWork);
59474
+ }
59475
+ if (result.resolvedRecords.length > 0) {
59476
+ this.emit('conflict', {
59477
+ ids: result.resolvedRecords,
59478
+ resolution: 'priming-merge',
59479
+ });
59480
+ this.emit('primed', result.resolvedRecords);
59481
+ }
59482
+ if (result.recordsToWrite.length > 0) {
59483
+ const { written, errors, conflicted } = await this.recordIngestor.insertRecords(result.recordsToWrite, true);
59484
+ if (written.length > 0) {
59485
+ const ids = Array.from(written);
59486
+ this.emit('conflict', { ids, resolution: 'priming-merge' });
59487
+ this.emit('primed', ids);
59488
+ }
59489
+ if (errors.length > 0) {
59490
+ errors.forEach(({ ids, message }) => {
59491
+ this.emit('error', {
59492
+ ids,
59493
+ code: 'unknown',
59494
+ message: message,
59495
+ });
59496
+ });
59497
+ }
59213
59498
  if (conflicted.length > 0) {
59214
- // for now emit conlicts as errors
59215
59499
  this.emit('error', {
59216
- ids: Array.from(conflicted),
59500
+ ids: conflicted,
59217
59501
  code: 'unknown',
59218
- message: 'conflict when persisting record',
59502
+ message: 'unexpected write conflict',
59219
59503
  });
59220
59504
  }
59221
- });
59505
+ }
59506
+ if (result.recordsNeedingRefetch.size > 0) {
59507
+ const { loaded, errored } = await this.ldsRecordRefresher.loadRecords(result.recordsNeedingRefetch);
59508
+ if (loaded.length > 0) {
59509
+ this.emit('conflict', { resolution: 'lds-refresh', ids: loaded });
59510
+ this.emit('primed', loaded);
59511
+ }
59512
+ if (errored.length > 0) {
59513
+ this.emit('error', {
59514
+ ids: errored,
59515
+ code: 'unknown',
59516
+ message: `could not resolve conflicts`,
59517
+ });
59518
+ }
59519
+ }
59222
59520
  }
59223
59521
  async fetchMetadata(batches) {
59224
59522
  const apiNames = Array.from(batches.reduce((acc, x) => {
@@ -59457,6 +59755,9 @@
59457
59755
  });
59458
59756
  session.on('primed', ({ length }) => {
59459
59757
  });
59758
+ session.on('conflict', ({ ids, resolution }) => {
59759
+ reportPrimingConflict(resolution, ids.length);
59760
+ });
59460
59761
  return session;
59461
59762
  }
59462
59763
 
@@ -59638,19 +59939,72 @@
59638
59939
  return batches;
59639
59940
  }
59640
59941
 
59942
+ /**
59943
+ * A polyfill for Promise.allSettled
59944
+ * @param promises An array of promises
59945
+ * @returns the result of all the promises when they either fulfill or reject
59946
+ */
59947
+ function allSettled(promises) {
59948
+ let wrappedPromises = promises.map((p) => Promise.resolve(p).then((value) => ({ status: 'fulfilled', value }), (reason) => ({ status: 'rejected', reason })));
59949
+ return Promise.all(wrappedPromises);
59950
+ }
59951
+
59952
+ class LdsPrimingRecordRefresher {
59953
+ constructor(getRecordsAdapter) {
59954
+ this.getRecordsAdapter = getRecordsAdapter;
59955
+ }
59956
+ async loadRecords(records) {
59957
+ const requestedRecords = new Set();
59958
+ const promises = Array.from(records).flatMap(([_apiName, value]) => {
59959
+ value.ids.forEach((id) => requestedRecords.add(id));
59960
+ return Promise.resolve(this.getRecordsAdapter({
59961
+ records: [
59962
+ {
59963
+ recordIds: value.ids,
59964
+ optionalFields: value.fields.map((f) => `${_apiName}.${f}`),
59965
+ },
59966
+ ],
59967
+ }));
59968
+ });
59969
+ const promiseResults = await allSettled(promises);
59970
+ const loaded = [];
59971
+ promiseResults.forEach((promiseResult) => {
59972
+ if (promiseResult.status === 'fulfilled') {
59973
+ const batchResultRepresenatation = promiseResult.value;
59974
+ if (batchResultRepresenatation &&
59975
+ batchResultRepresenatation.state === 'Fulfilled') {
59976
+ batchResultRepresenatation.data.results.forEach((result) => {
59977
+ if (result.statusCode === 200) {
59978
+ const id = result.result.id;
59979
+ loaded.push(id);
59980
+ requestedRecords.delete(id);
59981
+ }
59982
+ });
59983
+ }
59984
+ }
59985
+ });
59986
+ // errored contains all the requestedRecords that weren't loaded
59987
+ const errored = Array.from(requestedRecords);
59988
+ return { loaded, errored };
59989
+ }
59990
+ }
59991
+
59641
59992
  function primingSessionFactory(config) {
59642
59993
  const { store, objectInfoService, getLuvio } = config;
59643
59994
  const networkAdapter = new NimbusPrimingNetworkAdapter();
59644
59995
  const recordLoader = new RecordLoaderGraphQL(networkAdapter);
59645
- const recordIngestor = new RecordIngestor(new SqlitePrimingStore(getLuvio, store), getLuvio);
59996
+ const primingStore = new SqlitePrimingStore(getLuvio, store);
59997
+ const recordIngestor = new RecordIngestor(primingStore, getLuvio);
59646
59998
  const session = new PrimingSession({
59647
59999
  recordLoader,
59648
60000
  recordIngestor,
60001
+ store: primingStore,
59649
60002
  objectInfoLoader: {
59650
60003
  getObjectInfos: objectInfoService.getObjectInfos.bind(objectInfoService),
59651
60004
  },
59652
60005
  concurrency: config.concurrency,
59653
60006
  batchSize: config.batchSize,
60007
+ ldsRecordRefresher: new LdsPrimingRecordRefresher(config.getRecords),
59654
60008
  });
59655
60009
  return instrumentPrimingSession(session);
59656
60010
  }
@@ -59664,6 +60018,7 @@
59664
60018
  let lazyBaseDurableStore;
59665
60019
  let lazyNetworkAdapter;
59666
60020
  let lazyObjectInfoService;
60021
+ let lazyGetRecords;
59667
60022
  /**
59668
60023
  * This returns the LDS on Mobile Runtime singleton object.
59669
60024
  */
@@ -59734,6 +60089,7 @@
59734
60089
  lazyLuvio = new Luvio(lazyEnvironment, {
59735
60090
  instrument: instrumentLuvio,
59736
60091
  });
60092
+ lazyGetRecords = getRecordsAdapterFactory(lazyLuvio);
59737
60093
  // Currently instruments store runtime perf
59738
60094
  setupMobileInstrumentation();
59739
60095
  // If the inspection nimbus plugin is configured, inspection is enabled otherwise this is a no-op
@@ -59787,6 +60143,7 @@
59787
60143
  getLuvio: () => lazyLuvio,
59788
60144
  concurrency: config.concurrency,
59789
60145
  batchSize: config.batchSize,
60146
+ getRecords: lazyGetRecords,
59790
60147
  });
59791
60148
  },
59792
60149
  };
@@ -59803,7 +60160,7 @@
59803
60160
  id: '@salesforce/lds-network-adapter',
59804
60161
  instrument: instrument$1,
59805
60162
  });
59806
- // version: 1.208.1-8f4c4550e
60163
+ // version: 1.210.0-b2655462f
59807
60164
 
59808
60165
  const { create: create$2, keys: keys$2 } = Object;
59809
60166
  const { stringify: stringify$1, parse: parse$1 } = JSON;
@@ -77146,7 +77503,7 @@
77146
77503
  configuration: { ...configurationForGraphQLAdapters },
77147
77504
  instrument,
77148
77505
  });
77149
- // version: 1.208.1-9aaa359ad
77506
+ // version: 1.210.0-2883d2f5f
77150
77507
 
77151
77508
  // On core the unstable adapters are re-exported with different names,
77152
77509
 
@@ -79393,7 +79750,7 @@
79393
79750
  unstable_graphQL_imperative = createImperativeAdapter(luvio, createInstrumentedAdapter(ldsAdapter, adapterMetadata), adapterMetadata);
79394
79751
  graphQLImperative = ldsAdapter;
79395
79752
  });
79396
- // version: 1.208.1-9aaa359ad
79753
+ // version: 1.210.0-2883d2f5f
79397
79754
 
79398
79755
  var gqlApi = /*#__PURE__*/Object.freeze({
79399
79756
  __proto__: null,
@@ -80060,22 +80417,47 @@
80060
80417
  const { primingEventHandler, concurrency, batchSize } = config;
80061
80418
  const session = createPrimingSession({ concurrency, batchSize });
80062
80419
  if (primingEventHandler) {
80063
- const { onError, onPrimed } = primingEventHandler;
80064
- if (onError) {
80065
- session.on('error', onError.bind(primingEventHandler));
80066
- }
80067
- if (onPrimed) {
80068
- session.on('primed', onPrimed.bind(primingEventHandler));
80069
- }
80070
- }
80420
+ addEventHandler(session, primingEventHandler);
80421
+ }
80422
+ const registerEventHandler = (handler) => {
80423
+ // bind creates a new function and we need to retain reference to that function
80424
+ // in order to be able to remove it
80425
+ const boundCallbacks = {
80426
+ onError: handler.onError ? handler.onError.bind(handler) : undefined,
80427
+ onPrimed: handler.onPrimed ? handler.onPrimed.bind(handler) : undefined,
80428
+ };
80429
+ addEventHandler(session, boundCallbacks);
80430
+ return () => {
80431
+ removeEventHandler(session, boundCallbacks);
80432
+ };
80433
+ };
80071
80434
  return {
80072
80435
  enqueue: session.enqueue.bind(session),
80073
80436
  cancel: session.cancel.bind(session),
80074
80437
  on: session.on.bind(session),
80075
80438
  once: session.once.bind(session),
80076
80439
  off: session.off.bind(session),
80440
+ registerEventHandler,
80077
80441
  };
80078
80442
  }
80443
+ function addEventHandler(session, handler) {
80444
+ const { onError, onPrimed } = handler;
80445
+ if (onError) {
80446
+ session.on('error', onError);
80447
+ }
80448
+ if (onPrimed) {
80449
+ session.on('primed', onPrimed);
80450
+ }
80451
+ }
80452
+ function removeEventHandler(session, handler) {
80453
+ const { onError, onPrimed } = handler;
80454
+ if (onError) {
80455
+ session.off('error', onError);
80456
+ }
80457
+ if (onPrimed) {
80458
+ session.off('primed', onPrimed);
80459
+ }
80460
+ }
80079
80461
 
80080
80462
  // LWR has a "setupLDS" bootstrap service/loader hook, we simulate this in
80081
80463
  const { luvio } = getRuntime();
@@ -80099,4 +80481,4 @@
80099
80481
  Object.defineProperty(exports, '__esModule', { value: true });
80100
80482
 
80101
80483
  }));
80102
- // version: 1.208.1-8f4c4550e
80484
+ // version: 1.210.0-b2655462f
@@ -1,10 +1,11 @@
1
+ import type { PrimingSession } from '@salesforce/lds-priming';
1
2
  interface PrimingError {
2
3
  ids: string[];
3
4
  code: ErrorCode;
4
5
  message: string;
5
6
  }
6
7
  type ErrorCode = 'precondition-error' | 'not-found' | 'service-unavailable' | 'canceled' | 'unknown';
7
- interface PrimingEventHandler {
8
+ export interface PrimingEventHandler {
8
9
  onError?: (error: PrimingError) => void;
9
10
  onPrimed?: (ids: string[]) => void;
10
11
  }
@@ -16,8 +17,9 @@ export interface PrimingSessionConfig {
16
17
  export declare function createPrimingSession(config: PrimingSessionConfig): {
17
18
  enqueue: (work: PrimingWork) => Promise<void>;
18
19
  cancel: () => void;
19
- on: <K extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K]) => void) => import("@salesforce/lds-priming").PrimingSession;
20
- once: <K_1 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_1, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_1]) => void) => import("@salesforce/lds-priming").PrimingSession;
21
- off: <K_2 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_2, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_2]) => void) => import("@salesforce/lds-priming").PrimingSession;
20
+ on: <K extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K]) => void) => PrimingSession;
21
+ once: <K_1 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_1, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_1]) => void) => PrimingSession;
22
+ off: <K_2 extends keyof import("@salesforce/lds-priming").PrimingEvents>(eventName: K_2, callback: (payload: import("@salesforce/lds-priming").PrimingEvents[K_2]) => void) => PrimingSession;
23
+ registerEventHandler: (handler: PrimingEventHandler) => () => void;
22
24
  };
23
25
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-worker-api",
3
- "version": "1.208.1",
3
+ "version": "1.210.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "",
6
6
  "main": "dist/standalone/es/lds-worker-api.js",