@salesforce/lds-worker-api 1.460.0 → 1.462.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4282,7 +4282,7 @@
4282
4282
  }
4283
4283
  callbacks.push(callback);
4284
4284
  }
4285
- // version: 1.460.0-f7dc7797dd
4285
+ // version: 1.462.0-50d9a13ba4
4286
4286
 
4287
4287
  // TODO [TD-0081508]: once that TD is fulfilled we can probably change this file
4288
4288
  function instrumentAdapter$1(createFunction, _metadata) {
@@ -5326,7 +5326,7 @@
5326
5326
  const { apiFamily, name } = metadata;
5327
5327
  return createGraphQLWireAdapterConstructor$1(adapter, `${apiFamily}.${name}`, luvio, astResolver);
5328
5328
  }
5329
- // version: 1.460.0-f7dc7797dd
5329
+ // version: 1.462.0-50d9a13ba4
5330
5330
 
5331
5331
  function isSupportedEntity(_objectApiName) {
5332
5332
  return true;
@@ -32553,7 +32553,7 @@
32553
32553
  throttle(60, 60000, setupNotifyAllListRecordUpdateAvailable(luvio));
32554
32554
  throttle(60, 60000, setupNotifyAllListInfoSummaryUpdateAvailable(luvio));
32555
32555
  });
32556
- // version: 1.460.0-3e24f1bbff
32556
+ // version: 1.462.0-2919385b97
32557
32557
 
32558
32558
  var draftQueueMaxRetryAttemptsGate = {
32559
32559
  isOpen: function (e) {
@@ -44185,24 +44185,6 @@
44185
44185
  return pattern;
44186
44186
  }
44187
44187
 
44188
- var ldsPrimingGraphqlBatch = {
44189
- isOpen: function (e) {
44190
- return e.fallback;
44191
- },
44192
- hasError: function () {
44193
- return !0;
44194
- },
44195
- };
44196
-
44197
- var lmrPrimingUseSoql = {
44198
- isOpen: function (e) {
44199
- return e.fallback;
44200
- },
44201
- hasError: function () {
44202
- return !0;
44203
- },
44204
- };
44205
-
44206
44188
  var aggressiveTrimCountKillSwitchGate = {
44207
44189
  isOpen: function (e) {
44208
44190
  return e.fallback;
@@ -44221,15 +44203,6 @@
44221
44203
  },
44222
44204
  };
44223
44205
 
44224
- var useOneStore = {
44225
- isOpen: function (e) {
44226
- return e.fallback;
44227
- },
44228
- hasError: function () {
44229
- return !0;
44230
- },
44231
- };
44232
-
44233
44206
  const { create: create$7, freeze: freeze$4, keys: keys$8, entries: entries$5 } = Object;
44234
44207
  const { hasOwnProperty: hasOwnProperty$3 } = Object.prototype;
44235
44208
  const { isArray: isArray$7 } = Array;
@@ -45949,7 +45922,7 @@
45949
45922
  },
45950
45923
  };
45951
45924
 
45952
- var reviveOnlyRequestedFields = {
45925
+ var isProduct2SerializedSideEffectKillSwitch = {
45953
45926
  isOpen: function (e) {
45954
45927
  return e.fallback;
45955
45928
  },
@@ -81470,6 +81443,35 @@
81470
81443
  return snapshot.data;
81471
81444
  }
81472
81445
 
81446
+ /**
81447
+ * A read-through cache over `DurableRecordStore.getRecord` scoped to a single draft-action
81448
+ * dispatch. Multiple side-effect hooks fire for one action (e.g. the van-stock and
81449
+ * IsProduct2Serialized ProductConsumed hooks) and are awaited one at a time, so they otherwise
81450
+ * each re-read the same draft record — and the IsProduct2Serialized chain re-reads the same
81451
+ * related ProductItem / Product2 — from the durable store. Sharing one reader across the
81452
+ * action's hooks collapses those repeated reads to a single durable call per key.
81453
+ *
81454
+ * The cache stores the in-flight promise (not the resolved value) so concurrent reads of the
81455
+ * same key share one durable call, and it caches `undefined` results so a miss is not re-fetched.
81456
+ * A reader must not outlive one action dispatch: durable state changes between actions, so a
81457
+ * longer-lived cache would return stale records.
81458
+ */
81459
+ class MemoizingRecordReader {
81460
+ constructor(durableRecordStore) {
81461
+ this.durableRecordStore = durableRecordStore;
81462
+ this.cache = new Map();
81463
+ }
81464
+ getRecord(key) {
81465
+ const cached = this.cache.get(key);
81466
+ if (cached !== undefined) {
81467
+ return cached;
81468
+ }
81469
+ const promise = this.durableRecordStore.getRecord(key);
81470
+ this.cache.set(key, promise);
81471
+ return promise;
81472
+ }
81473
+ }
81474
+
81473
81475
  const LDS_ACTION_HANDLER_ID = 'LDS_ACTION_HANDLER';
81474
81476
  const LDS_ACTION_METADATA_API_NAME = 'LDS_ACTION_METADATA_API_NAME';
81475
81477
  class UiApiActionHandler extends AbstractResourceRequestActionHandler {
@@ -81590,9 +81592,9 @@
81590
81592
  return super.getRedirectMappings(action);
81591
81593
  }
81592
81594
  async handleActionCompleted(completedAction, queueOperations) {
81593
- if (completedAction.response.status === 204 && completedAction.data.method === 'post') {
81594
- // if we get a 204 it means the record creation was successful but we don't have the record to ingest
81595
- // remove the synthesized draft and do not try to ingest the response
81595
+ const { method } = completedAction.data;
81596
+ if (completedAction.response.status === 204 && (method === 'post' || method === 'patch')) {
81597
+ await this.recordService.removeSideEffectsForAction(completedAction);
81596
81598
  await this.evictKey(completedAction.tag);
81597
81599
  return;
81598
81600
  }
@@ -81673,8 +81675,15 @@
81673
81675
  break;
81674
81676
  }
81675
81677
  }
81678
+ // One reader shared across every hook for this action so repeated reads of the same
81679
+ // record (the draft record, and any related records the hooks resolve) collapse to a
81680
+ // single durable call. Scoped to this dispatch only — durable state changes between
81681
+ // actions, so it must not be reused across them. Built only when hooks will run.
81682
+ const recordReader = hookTriggers.length > 0
81683
+ ? new MemoizingRecordReader(this.durableRecordStore)
81684
+ : undefined;
81676
81685
  for (const trigger of hookTriggers) {
81677
- const hookEffects = await trigger.then(action.id, action.timestamp, action.targetId, action.data.body?.fields ?? {});
81686
+ const hookEffects = await trigger.then(action.id, action.timestamp, action.targetId, action.data.body?.fields ?? {}, recordReader);
81678
81687
  for (const hookEffect of hookEffects) {
81679
81688
  effects.push(hookEffect);
81680
81689
  }
@@ -92746,211 +92755,6 @@
92746
92755
  }
92747
92756
  }
92748
92757
 
92749
- const apiPath$1 = basePath + '/graphql';
92750
- const requiredPrefix = `required_`;
92751
- const requiredFieldMap = {
92752
- ApiName: 'ApiName',
92753
- Id: 'Id',
92754
- LastModifiedById: 'LastModifiedById { value }',
92755
- LastModifiedDate: 'LastModifiedDate { value }',
92756
- RecordTypeId: 'RecordTypeId(fallback: true) { value }',
92757
- SystemModstamp: 'SystemModstamp { value }',
92758
- WeakEtag: 'WeakEtag',
92759
- };
92760
- class RecordLoaderGraphQL extends NetworkRecordLoader {
92761
- makeRequest(batch, observabilityContext) {
92762
- const query = this.generateGraphQLQuery(batch.type, batch.fields);
92763
- return {
92764
- request: {
92765
- method: 'POST',
92766
- path: apiPath$1,
92767
- body: JSON.stringify({
92768
- query,
92769
- variables: {
92770
- ids: batch.ids,
92771
- first: batch.ids.length,
92772
- },
92773
- }),
92774
- headers: {},
92775
- queryParams: {},
92776
- priority: 'background',
92777
- observabilityContext: observabilityContext !== undefined ? observabilityContext : {},
92778
- },
92779
- context: undefined,
92780
- };
92781
- }
92782
- generateFetchResults(repResult, batchInput, _context) {
92783
- const { data, errors } = repResult;
92784
- if (errors !== undefined && errors.length > 0) {
92785
- // right now if there are any errors in the batch we throw out the entire batch
92786
- // for now this is ok all errors will originate on the same node so there shouldn't be a mix of errors and data
92787
- return [
92788
- {
92789
- missingIds: batchInput.ids,
92790
- ok: false,
92791
- error: 'request-error',
92792
- messages: errors.map((x) => x.message),
92793
- },
92794
- ];
92795
- }
92796
- // this shouldn't really happen if there's no errors
92797
- if (!data || !data.uiapi) {
92798
- return [
92799
- {
92800
- ok: false,
92801
- error: 'unknown',
92802
- messages: ['unexpected response retrieved from graphql endpoint'],
92803
- missingIds: batchInput.ids,
92804
- },
92805
- ];
92806
- }
92807
- const objectInfo = batchInput.objectInfo;
92808
- const seenRecords = new Set(batchInput.ids);
92809
- const records = data.uiapi.query[batchInput.type].edges.map((edge) => {
92810
- const record = this.generateDurableRecordRepresentation(edge.node, objectInfo);
92811
- seenRecords.delete(record.id);
92812
- return record;
92813
- });
92814
- return [
92815
- {
92816
- ok: true,
92817
- records,
92818
- missingIds: Array.from(seenRecords),
92819
- },
92820
- ];
92821
- }
92822
- generateGraphQLQuery(type, fields) {
92823
- const fieldList = Object.keys(requiredFieldMap)
92824
- .map((field) => {
92825
- return `${requiredPrefix}${field}: ${requiredFieldMap[field]}`;
92826
- })
92827
- .join('\n');
92828
- return /* GraphQL */ `
92829
- fragment requiredRecordFields on Record {
92830
- ${fieldList}
92831
- }
92832
- query prime${type}($ids: [ID], $first: Int){
92833
- uiapi {
92834
- query {
92835
- ${type} (where: {Id: {in: $ids}} first: $first) {
92836
- edges {
92837
- node {
92838
- ...requiredRecordFields
92839
- ${fields
92840
- .filter((x) => x !== 'Id' && x !== 'RecordTypeId')
92841
- .map((field) => `${field} { value displayValue }`)}
92842
- }
92843
- }
92844
- }
92845
- }
92846
- }
92847
- }
92848
- `;
92849
- }
92850
- generateDurableRecordRepresentation(node, objectInfo) {
92851
- const standardRecordFields = new Set(Object.keys(requiredFieldMap).map((x) => `${requiredPrefix}${x}`));
92852
- const id = node[`${requiredPrefix}Id`];
92853
- const recordTypeId = node[`${requiredPrefix}RecordTypeId`].value;
92854
- const requestedFields = Object.keys(node).filter((field) => !standardRecordFields.has(field));
92855
- const fields = requestedFields.reduce((acc, field) => {
92856
- const fieldNode = node[field];
92857
- if (fieldNode.displayValue === undefined) {
92858
- fieldNode.displayValue = null;
92859
- }
92860
- acc[field] = node[field];
92861
- return acc;
92862
- }, {});
92863
- fields['Id'] = { value: id, displayValue: null };
92864
- if (objectInfo.fields['RecordTypeId'] !== undefined) {
92865
- fields['RecordTypeId'] = { value: recordTypeId, displayValue: null };
92866
- }
92867
- let recordTypeInfo = null;
92868
- if (recordTypeId !== null &&
92869
- objectInfo.recordTypeInfos &&
92870
- objectInfo.recordTypeInfos[recordTypeId]) {
92871
- const appliedRecordTypeInfo = objectInfo.recordTypeInfos[recordTypeId];
92872
- // ui-api only applies the record type if its not the default type, otherwise it sets it to null
92873
- if (appliedRecordTypeInfo.defaultRecordTypeMapping === false) {
92874
- recordTypeInfo = appliedRecordTypeInfo;
92875
- }
92876
- }
92877
- return {
92878
- apiName: node[`${requiredPrefix}ApiName`],
92879
- childRelationships: {},
92880
- eTag: '',
92881
- id,
92882
- lastModifiedById: node[`${requiredPrefix}LastModifiedById`] &&
92883
- node[`${requiredPrefix}LastModifiedById`].value
92884
- ? node[`${requiredPrefix}LastModifiedById`].value
92885
- : null,
92886
- lastModifiedDate: node[`${requiredPrefix}LastModifiedDate`] &&
92887
- node[`${requiredPrefix}LastModifiedDate`].value
92888
- ? node[`${requiredPrefix}LastModifiedDate`].value
92889
- : null,
92890
- recordTypeId,
92891
- recordTypeInfo: recordTypeInfo,
92892
- systemModstamp: node[`${requiredPrefix}SystemModstamp`] &&
92893
- node[`${requiredPrefix}SystemModstamp`].value
92894
- ? node[`${requiredPrefix}SystemModstamp`].value
92895
- : null,
92896
- weakEtag: node[`${requiredPrefix}WeakEtag`],
92897
- fields,
92898
- links: {},
92899
- };
92900
- }
92901
- }
92902
-
92903
- const DEFAULT_GQL_QUERY_BATCH_SIZE = 100;
92904
- const apiPath = basePath + '/graphql/batch';
92905
- class RecordLoaderGraphQLBatch extends RecordLoaderGraphQL {
92906
- makeRequest(batch, observabilityContext) {
92907
- const query = this.generateGraphQLQuery(batch.type, batch.fields);
92908
- const chunks = chunk(batch.ids, DEFAULT_GQL_QUERY_BATCH_SIZE);
92909
- const batches = chunks.map((chunk) => {
92910
- return {
92911
- query,
92912
- variables: {
92913
- ids: chunk,
92914
- first: chunk.length,
92915
- },
92916
- };
92917
- });
92918
- return {
92919
- request: {
92920
- method: 'POST',
92921
- path: apiPath,
92922
- body: JSON.stringify({
92923
- batchQuery: batches,
92924
- }),
92925
- headers: {},
92926
- queryParams: {},
92927
- priority: 'background',
92928
- observabilityContext: observabilityContext !== undefined ? observabilityContext : {},
92929
- },
92930
- context: chunks,
92931
- };
92932
- }
92933
- generateFetchResults(body, batchInput, context) {
92934
- const { results } = body;
92935
- const fetchResults = context.flatMap((chunk, index) => {
92936
- const result = results[index];
92937
- if (!result || result.statusCode !== 200) {
92938
- return {
92939
- ok: false,
92940
- error: 'request-error',
92941
- messages: ['unexpected response from graphql batch endpoint'],
92942
- missingIds: chunk,
92943
- };
92944
- }
92945
- return super.generateFetchResults(result.result, {
92946
- ...batchInput,
92947
- ids: chunk,
92948
- }, undefined);
92949
- });
92950
- return fetchResults;
92951
- }
92952
- }
92953
-
92954
92758
  class SoqlRecordConverter {
92955
92759
  /**
92956
92760
  *
@@ -93443,16 +93247,7 @@
93443
93247
  function primingSessionFactory(config) {
93444
93248
  const { store, objectInfoService, getLuvio } = config;
93445
93249
  const networkAdapter = new NimbusPrimingNetworkAdapter();
93446
- let recordLoader;
93447
- if (lmrPrimingUseSoql.isOpen({ fallback: false })) {
93448
- recordLoader = new RecordLoaderSOQLComposite(networkAdapter);
93449
- }
93450
- else if (ldsPrimingGraphqlBatch.isOpen({ fallback: false })) {
93451
- recordLoader = new RecordLoaderGraphQLBatch(networkAdapter);
93452
- }
93453
- else {
93454
- recordLoader = new RecordLoaderGraphQL(networkAdapter);
93455
- }
93250
+ const recordLoader = new RecordLoaderSOQLComposite(networkAdapter);
93456
93251
  const primingStore = new SqlitePrimingStore(getLuvio, store);
93457
93252
  const recordIngestor = new RecordIngestor(primingStore, getLuvio);
93458
93253
  const session = new PrimingSession({
@@ -94105,6 +93900,38 @@
94105
93900
  }
94106
93901
 
94107
93902
  const SIDE_EFFECT_SEGMENT = 'RECORD_SIDE_EFFECTS';
93903
+ // Base-record effects rebuild (or remove) the entire record for a key, so they must be
93904
+ // applied before any field-level effect on that same key — otherwise a field effect that
93905
+ // runs first is discarded when the base record is (re)built. keyToEffectMap ordering is not
93906
+ // stable: it is re-materialized from the durable store on reload / onChanged replay, where
93907
+ // entry order is not guaranteed. So two field-set effects on the same key from different
93908
+ // actions (e.g. two queued edits of one field) can come back in either order; applied out of
93909
+ // order the OLDER value wins and the newer edit is lost. Effects must therefore be applied in
93910
+ // action chronology (by timestamp), with base-record ops ordered ahead of field ops only as
93911
+ // the tie-breaker within one action — create/field effects on a draft-created record share a
93912
+ // key AND a timestamp, so timestamp alone can't order those.
93913
+ const BASE_RECORD_EFFECT_TYPES = new Set(['record-create', 'record-update', 'record-delete']);
93914
+ function orderEffectsForApply(effects) {
93915
+ // Nothing to reorder for a single effect; return the original array to avoid allocating on
93916
+ // the hot read-out path.
93917
+ if (effects.length <= 1) {
93918
+ return effects;
93919
+ }
93920
+ // Primary key: timestamp (action chronology). Secondary key: base-record ops before field
93921
+ // ops, which only matters within one action's shared timestamp. Array.prototype.sort is
93922
+ // stable (ES2019), so effects that tie on both keys keep their existing relative order.
93923
+ return [...effects].sort((a, b) => {
93924
+ if (a.timestamp !== b.timestamp) {
93925
+ return a.timestamp - b.timestamp;
93926
+ }
93927
+ const aIsBase = BASE_RECORD_EFFECT_TYPES.has(a.type);
93928
+ const bIsBase = BASE_RECORD_EFFECT_TYPES.has(b.type);
93929
+ if (aIsBase === bIsBase) {
93930
+ return 0;
93931
+ }
93932
+ return aIsBase ? -1 : 1;
93933
+ });
93934
+ }
94108
93935
  class SideEffectStore {
94109
93936
  constructor(durableStore) {
94110
93937
  this.durableStore = durableStore;
@@ -94183,7 +94010,7 @@
94183
94010
  if (effects === undefined) {
94184
94011
  return [];
94185
94012
  }
94186
- return effects;
94013
+ return orderEffectsForApply(effects);
94187
94014
  }
94188
94015
  getEffectsByTag(tag) {
94189
94016
  // This scan is still O(N) over every effect regardless of the incremental-map
@@ -94680,6 +94507,58 @@
94680
94507
  return record;
94681
94508
  }
94682
94509
  }
94510
+ class RecordFieldSetHandler {
94511
+ constructor() {
94512
+ this.logger = typeof __nimbus !== 'undefined' &&
94513
+ __nimbus.plugins !== undefined &&
94514
+ __nimbus.plugins.JSLoggerPlugin !== undefined
94515
+ ? __nimbus.plugins.JSLoggerPlugin
94516
+ : undefined;
94517
+ }
94518
+ apply(record, effect) {
94519
+ if (record === undefined) {
94520
+ if (this.logger) {
94521
+ this.logger.logWarning('RecordFieldSetHandler - cannot apply set side effect to non existing record');
94522
+ }
94523
+ return undefined;
94524
+ }
94525
+ // fieldName is an unconstrained string that has round-tripped through the durable
94526
+ // store, so a value like `__proto__` would hit the prototype setter on the plain
94527
+ // `fields`/`serverValues` objects below rather than creating an own field (prototype
94528
+ // pollution). Reject dangerous keys before any dynamic read/write and skip the effect.
94529
+ if (isUnsafeFieldName(effect.fieldName)) {
94530
+ if (this.logger) {
94531
+ this.logger.logError(`RecordFieldSetHandler - unsafe field name ${effect.fieldName}, skip applying side effect`);
94532
+ }
94533
+ return record;
94534
+ }
94535
+ applyDraftNode(record, effect);
94536
+ record.drafts.edited = true;
94537
+ // capture the server value so it can be restored when the draft syncs.
94538
+ // unlike the increment handler, the field may not yet exist on the record
94539
+ // (e.g. IsProduct2Serialized is not part of the draft-created record), in
94540
+ // which case we record `null` to indicate we never had the field.
94541
+ if (record.drafts.created === false) {
94542
+ const serverValue = record.drafts.serverValues[effect.fieldName];
94543
+ if (serverValue === undefined) {
94544
+ const existingField = record.fields[effect.fieldName];
94545
+ record.drafts.serverValues[effect.fieldName] =
94546
+ existingField === undefined ? null : existingField;
94547
+ }
94548
+ }
94549
+ record.fields[effect.fieldName] = {
94550
+ value: effect.value,
94551
+ displayValue: null,
94552
+ };
94553
+ return record;
94554
+ }
94555
+ }
94556
+ // Field names that would target the prototype chain rather than an own property when used
94557
+ // to index a plain object, enabling prototype pollution on the record's field containers.
94558
+ const UNSAFE_FIELD_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
94559
+ function isUnsafeFieldName(fieldName) {
94560
+ return UNSAFE_FIELD_NAMES.has(fieldName);
94561
+ }
94683
94562
  function applyDraftNode(record, effect) {
94684
94563
  if (record.drafts === undefined) {
94685
94564
  record.drafts = {
@@ -94714,6 +94593,7 @@
94714
94593
  'record-update': new UpdateRecordEffectHandler(),
94715
94594
  'record-delete': new DeleteRecordEffectHandler(),
94716
94595
  'record-field-increment': new RecordFieldIncrementHandler(),
94596
+ 'record-field-set': new RecordFieldSetHandler(),
94717
94597
  };
94718
94598
  /**
94719
94599
  * Removes any existing effects on a record and re-applies the given ordered list and returns the record
@@ -94878,14 +94758,181 @@
94878
94758
  }
94879
94759
 
94880
94760
  const instrumentation$4 = getInstrumentation();
94761
+ const ProductItem$2 = {
94762
+ Fields: {
94763
+ PRODUCT2_ID: 'Product2Id',
94764
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
94765
+ },
94766
+ };
94767
+ const Product2 = {
94768
+ Fields: {
94769
+ IS_SERIALIZED: 'IsSerialized',
94770
+ },
94771
+ };
94772
+ var CounterResult;
94773
+ (function (CounterResult) {
94774
+ // A side effect was created.
94775
+ CounterResult["Created"] = "Created";
94776
+ // The hook exited early.
94777
+ CounterResult["Exited"] = "Exited";
94778
+ // An unexpected error occurred.
94779
+ CounterResult["Error"] = "Error";
94780
+ })(CounterResult || (CounterResult = {}));
94781
+ function incrementCounter(name, result) {
94782
+ instrumentation$4.incrementCounter('SFSSideEffect', 1, result === CounterResult.Error, {
94783
+ Name: name,
94784
+ Result: result,
94785
+ });
94786
+ }
94787
+ /**
94788
+ * Reads a string-valued field off a draft-create `fields` map (the raw create body, where
94789
+ * values are the primitives themselves rather than `{ value }` wrappers). Returns `undefined`
94790
+ * when the field is absent or not a string.
94791
+ */
94792
+ function getStringDraftField(fields, fieldName) {
94793
+ const field = fields[fieldName];
94794
+ if (field === undefined || typeof field !== 'string') {
94795
+ return undefined;
94796
+ }
94797
+ return field;
94798
+ }
94799
+ /**
94800
+ * Reads a string-valued field off a durable record's `fields` map (where each value is a
94801
+ * `{ value }` wrapper). Returns `undefined` when the field is absent, null, or not a string.
94802
+ */
94803
+ function getStringRecordField(fields, fieldName) {
94804
+ const field = fields[fieldName];
94805
+ if (field === undefined || field.value === undefined || typeof field.value !== 'string') {
94806
+ return undefined;
94807
+ }
94808
+ return field.value;
94809
+ }
94810
+ /**
94811
+ * Resolves the reference id that points at a related record for a draft edit.
94812
+ *
94813
+ * On a `patch` the body only carries the fields being changed, so the link field may be
94814
+ * absent, present with a new id, or present but explicitly `null` (the link is being cleared).
94815
+ * These three cases are distinguished via `hasOwnProperty` because they demand different
94816
+ * behavior — falling back to the original durable-store id on an explicit `null` would resolve
94817
+ * `IsProduct2Serialized` from a relationship the record no longer has:
94818
+ *
94819
+ * - present + string -> `resolved` with the reassigned id.
94820
+ * - present + null -> `cleared`.
94821
+ * - absent -> fall back to the original record; `resolved` when it carries a usable
94822
+ * string, otherwise `unresolved`.
94823
+ */
94824
+ function resolveReferenceIdForUpdate(patchFields, originalFields, fieldName) {
94825
+ if (Object.prototype.hasOwnProperty.call(patchFields, fieldName)) {
94826
+ const patchValue = patchFields[fieldName];
94827
+ if (patchValue === null) {
94828
+ return { kind: 'cleared' };
94829
+ }
94830
+ const patchId = getStringDraftField(patchFields, fieldName);
94831
+ if (patchId !== undefined) {
94832
+ return { kind: 'resolved', id: patchId, source: 'patch' };
94833
+ }
94834
+ // Present but neither a string nor null (unexpected shape): treat as an absent link
94835
+ // and fall through to the original record rather than guessing.
94836
+ }
94837
+ const originalId = getStringRecordField(originalFields, fieldName);
94838
+ if (originalId !== undefined) {
94839
+ return { kind: 'resolved', id: originalId, source: 'original' };
94840
+ }
94841
+ return { kind: 'unresolved' };
94842
+ }
94843
+ /**
94844
+ * Resolves the server-derived `IsProduct2Serialized` boolean for a draft-created record by
94845
+ * following the related `ProductItem`:
94846
+ *
94847
+ * 1. If the cached `ProductItem` already carries `IsProduct2Serialized`, use it directly
94848
+ * (it mirrors the linked product's serialization, and avoids a second durable read).
94849
+ * 2. Otherwise fall back to the linked `Product2`:
94850
+ * `ProductItem.Product2Id` -> `Product2.IsSerialized`.
94851
+ *
94852
+ * All lookups go through the given `RecordReader` (the durable store, or a per-action memoizer
94853
+ * that dedups reads). Returns `undefined` when a link in the chain is missing so the caller can
94854
+ * exit without an effect and leave the field synthesized (null). Callers pass their own
94855
+ * `counterName` so instrumentation is attributed to the owning hook.
94856
+ */
94857
+ async function resolveIsProduct2SerializedFromProductItem(recordReader, productItemId, counterName) {
94858
+ const productItemRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(productItemId));
94859
+ if (productItemRecord === undefined) {
94860
+ incrementCounter(counterName, CounterResult.Exited);
94861
+ return undefined;
94862
+ }
94863
+ // Prefer IsProduct2Serialized straight off the ProductItem when it is cached there;
94864
+ // it saves a second durable read. Fall back to the linked Product2 otherwise.
94865
+ const fromProductItem = getIsProduct2SerializedFromRecordFields(productItemRecord.fields);
94866
+ if (fromProductItem !== undefined) {
94867
+ incrementCounter(counterName, CounterResult.Created);
94868
+ return fromProductItem;
94869
+ }
94870
+ const product2Id = getProduct2IdFromRecordFields(productItemRecord.fields);
94871
+ if (product2Id === undefined) {
94872
+ incrementCounter(counterName, CounterResult.Exited);
94873
+ return undefined;
94874
+ }
94875
+ return resolveIsSerializedFromProduct2Id(recordReader, product2Id, counterName);
94876
+ }
94877
+ /**
94878
+ * Resolves the `Product2.IsSerialized` boolean for a given `Product2Id` by reading the linked
94879
+ * `Product2` record through the given `RecordReader`. Used directly by the `ProductItem` hooks
94880
+ * (which carry `Product2Id` themselves) and as the fallback tail of
94881
+ * `resolveIsProduct2SerializedFromProductItem`.
94882
+ *
94883
+ * Returns `undefined` when the `Product2` record is missing so the caller can exit without an
94884
+ * effect and leave the field synthesized (null). Callers pass their own `counterName` so
94885
+ * instrumentation is attributed to the owning hook.
94886
+ */
94887
+ async function resolveIsSerializedFromProduct2Id(recordReader, product2Id, counterName) {
94888
+ const product2Record = await recordReader.getRecord(buildRecordRepKeyFromId$1(product2Id));
94889
+ if (product2Record === undefined) {
94890
+ incrementCounter(counterName, CounterResult.Exited);
94891
+ return undefined;
94892
+ }
94893
+ const isSerialized = getIsSerializedFromRecordFields(product2Record.fields);
94894
+ if (isSerialized === undefined) {
94895
+ incrementCounter(counterName, CounterResult.Error);
94896
+ // Keep the message a stable literal and append the variable parts as key=value
94897
+ // pairs, so the log line stays low-cardinality and greppable in Splunk (the raw
94898
+ // record id is a discrete field rather than baked into the message text).
94899
+ instrumentation$4.error(`SFS - Product2 IsSerialized not found: hook=${counterName}, product2Id=${product2Id}`);
94900
+ return undefined;
94901
+ }
94902
+ incrementCounter(counterName, CounterResult.Created);
94903
+ return isSerialized;
94904
+ }
94905
+ function getProduct2IdFromRecordFields(fields) {
94906
+ const field = fields[ProductItem$2.Fields.PRODUCT2_ID];
94907
+ if (field === undefined || field.value === undefined || typeof field.value !== 'string') {
94908
+ return undefined;
94909
+ }
94910
+ return field.value;
94911
+ }
94912
+ function getIsProduct2SerializedFromRecordFields(fields) {
94913
+ const field = fields[ProductItem$2.Fields.IS_PRODUCT2_SERIALIZED];
94914
+ if (field === undefined || field.value === undefined || typeof field.value !== 'boolean') {
94915
+ return undefined;
94916
+ }
94917
+ return field.value;
94918
+ }
94919
+ function getIsSerializedFromRecordFields(fields) {
94920
+ const field = fields[Product2.Fields.IS_SERIALIZED];
94921
+ if (field === undefined || field.value === undefined || typeof field.value !== 'boolean') {
94922
+ return undefined;
94923
+ }
94924
+ return field.value;
94925
+ }
94926
+
94881
94927
  const ProductConsumed = {
94882
94928
  API_NAME: 'ProductConsumed',
94883
94929
  Fields: {
94884
94930
  PRODUCT_ITEM_ID: 'ProductItemId',
94885
94931
  QUANTITY_CONSUMED: 'QuantityConsumed',
94932
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
94886
94933
  },
94887
94934
  };
94888
- const ProductItem = {
94935
+ const ProductItem$1 = {
94889
94936
  Fields: {
94890
94937
  QUANTITY_ON_HAND: 'QuantityOnHand',
94891
94938
  },
@@ -94896,7 +94943,7 @@
94896
94943
  this.apiName = ProductConsumed.API_NAME;
94897
94944
  }
94898
94945
  async then(draftActionId, draftActionTimestamp, _draftRecordId, fields) {
94899
- const productItemId = getProductItemId(fields);
94946
+ const productItemId = getStringDraftField(fields, ProductConsumed.Fields.PRODUCT_ITEM_ID);
94900
94947
  const quantityConsumed = getQuantityConsumed(fields);
94901
94948
  if (productItemId === undefined) {
94902
94949
  incrementCounter(ProductConsumedCreatedHook.COUNTER_NAME, CounterResult.Exited);
@@ -94909,7 +94956,7 @@
94909
94956
  incrementCounter(ProductConsumedCreatedHook.COUNTER_NAME, CounterResult.Created);
94910
94957
  const effect = {
94911
94958
  type: 'record-field-increment',
94912
- fieldName: ProductItem.Fields.QUANTITY_ON_HAND,
94959
+ fieldName: ProductItem$1.Fields.QUANTITY_ON_HAND,
94913
94960
  value: -quantityConsumed,
94914
94961
  key: buildRecordRepKeyFromId$1(productItemId),
94915
94962
  tag: draftActionId,
@@ -94926,16 +94973,17 @@
94926
94973
  this.on = 'patch';
94927
94974
  this.apiName = ProductConsumed.API_NAME;
94928
94975
  }
94929
- async then(draftActionId, draftActionTimestamp, draftRecordId, fields) {
94976
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
94930
94977
  const quantityConsumed = getQuantityConsumed(fields);
94931
94978
  if (quantityConsumed === undefined) {
94932
94979
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
94933
94980
  return [];
94934
94981
  }
94935
- const originalRecord = await this.durableRecordStore.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
94982
+ const recordReader = reader ?? this.durableRecordStore;
94983
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
94936
94984
  if (originalRecord === undefined) {
94937
94985
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Error);
94938
- instrumentation$4.error(`SFS - ProductConsumedUpdatedHook - Original record ${draftRecordId} not found`);
94986
+ instrumentation$4.error(`SFS - ProductConsumedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
94939
94987
  return [];
94940
94988
  }
94941
94989
  const originalProductItemId = getProductItemIdFromRecordFields(originalRecord.fields);
@@ -94946,13 +94994,13 @@
94946
94994
  }
94947
94995
  if (originalQuantityConsumed === undefined) {
94948
94996
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Error);
94949
- instrumentation$4.error(`SFS - ProductConsumedUpdatedHook - Original record ${draftRecordId} QuantityConsumed not found`);
94997
+ instrumentation$4.error(`SFS - ProductConsumedUpdatedHook original record QuantityConsumed not found: draftRecordId=${draftRecordId}`);
94950
94998
  return [];
94951
94999
  }
94952
95000
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Created);
94953
95001
  const effect = {
94954
95002
  type: 'record-field-increment',
94955
- fieldName: ProductItem.Fields.QUANTITY_ON_HAND,
95003
+ fieldName: ProductItem$1.Fields.QUANTITY_ON_HAND,
94956
95004
  value: originalQuantityConsumed - quantityConsumed,
94957
95005
  key: buildRecordRepKeyFromId$1(originalProductItemId),
94958
95006
  tag: draftActionId,
@@ -94969,11 +95017,12 @@
94969
95017
  this.on = 'delete';
94970
95018
  this.apiName = ProductConsumed.API_NAME;
94971
95019
  }
94972
- async then(draftActionId, draftActionTimestamp, draftRecordId) {
94973
- const originalRecord = await this.durableRecordStore.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
95020
+ async then(draftActionId, draftActionTimestamp, draftRecordId, _fields, reader) {
95021
+ const recordReader = reader ?? this.durableRecordStore;
95022
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
94974
95023
  if (originalRecord === undefined) {
94975
95024
  incrementCounter(ProductConsumedDeletedHook.COUNTER_NAME, CounterResult.Error);
94976
- instrumentation$4.error(`SFS - ProductConsumedDeletedHook - Original record ${draftRecordId} not found`);
95025
+ instrumentation$4.error(`SFS - ProductConsumedDeletedHook original record not found: draftRecordId=${draftRecordId}`);
94977
95026
  return [];
94978
95027
  }
94979
95028
  const productItemId = getProductItemIdFromRecordFields(originalRecord.fields);
@@ -94984,13 +95033,13 @@
94984
95033
  }
94985
95034
  if (quantityConsumed === undefined) {
94986
95035
  incrementCounter(ProductConsumedDeletedHook.COUNTER_NAME, CounterResult.Error);
94987
- instrumentation$4.error(`SFS - ProductConsumedDeletedHook - Original record ${draftRecordId} QuantityConsumed not found`);
95036
+ instrumentation$4.error(`SFS - ProductConsumedDeletedHook original record QuantityConsumed not found: draftRecordId=${draftRecordId}`);
94988
95037
  return [];
94989
95038
  }
94990
95039
  incrementCounter(ProductConsumedDeletedHook.COUNTER_NAME, CounterResult.Created);
94991
95040
  const effect = {
94992
95041
  type: 'record-field-increment',
94993
- fieldName: ProductItem.Fields.QUANTITY_ON_HAND,
95042
+ fieldName: ProductItem$1.Fields.QUANTITY_ON_HAND,
94994
95043
  value: quantityConsumed,
94995
95044
  key: buildRecordRepKeyFromId$1(productItemId),
94996
95045
  tag: draftActionId,
@@ -95001,13 +95050,115 @@
95001
95050
  }
95002
95051
  }
95003
95052
  ProductConsumedDeletedHook.COUNTER_NAME = 'ProductConsumedDeletedHook';
95004
- function getProductItemId(fields) {
95005
- const field = fields[ProductConsumed.Fields.PRODUCT_ITEM_ID];
95006
- if (field === undefined || typeof field !== 'string') {
95007
- return undefined;
95053
+ /**
95054
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-created
95055
+ * `ProductConsumed` record.
95056
+ *
95057
+ * `Product2Id` is not createable on `ProductConsumed`, so the draft only carries the
95058
+ * `ProductItemId`. The value is resolved through the related `ProductItem` record (see
95059
+ * `resolveIsProduct2SerializedFromProductItem`): the ProductItem's own
95060
+ * `IsProduct2Serialized` when cached, otherwise `ProductItem.Product2Id` ->
95061
+ * `Product2.IsSerialized`. When the chain cannot be resolved the hook exits without an
95062
+ * effect and the field is left as synthesized (null).
95063
+ */
95064
+ class ProductConsumedIsSerializedCreatedHook {
95065
+ constructor(durableRecordStore) {
95066
+ this.durableRecordStore = durableRecordStore;
95067
+ this.on = 'post';
95068
+ this.apiName = ProductConsumed.API_NAME;
95069
+ }
95070
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95071
+ const productItemId = getStringDraftField(fields, ProductConsumed.Fields.PRODUCT_ITEM_ID);
95072
+ if (productItemId === undefined) {
95073
+ incrementCounter(ProductConsumedIsSerializedCreatedHook.COUNTER_NAME, CounterResult.Exited);
95074
+ return [];
95075
+ }
95076
+ const isSerialized = await resolveIsProduct2SerializedFromProductItem(reader ?? this.durableRecordStore, productItemId, ProductConsumedIsSerializedCreatedHook.COUNTER_NAME);
95077
+ if (isSerialized === undefined) {
95078
+ return [];
95079
+ }
95080
+ const effect = {
95081
+ type: 'record-field-set',
95082
+ fieldName: ProductConsumed.Fields.IS_PRODUCT2_SERIALIZED,
95083
+ value: isSerialized,
95084
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95085
+ tag: draftActionId,
95086
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95087
+ timestamp: draftActionTimestamp,
95088
+ };
95089
+ return [effect];
95008
95090
  }
95009
- return field;
95010
95091
  }
95092
+ ProductConsumedIsSerializedCreatedHook.COUNTER_NAME = 'ProductConsumedIsSerializedCreatedHook';
95093
+ /**
95094
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-edited
95095
+ * `ProductConsumed` record.
95096
+ *
95097
+ * A `patch` body only carries changed fields, so the related `ProductItemId` may be absent.
95098
+ * The id is resolved by preferring the patch body (the link is being reassigned) and falling
95099
+ * back to the original record in the durable store, then the value is resolved through that
95100
+ * `ProductItem` exactly as on create (see `resolveIsProduct2SerializedFromProductItem`).
95101
+ *
95102
+ * When the patch explicitly clears `ProductItemId` (sets it to `null`) the record no longer
95103
+ * derives from any `ProductItem`, so `IsProduct2Serialized` is cleared (set to `null`) rather
95104
+ * than resolved from the now-stale original link. Likewise, when the patch reassigns
95105
+ * `ProductItemId` to a new id whose chain isn't cached, the stored value reflects the old link
95106
+ * and is stale, so it is cleared. When the link is unchanged and its chain cannot be resolved,
95107
+ * the stored value is still correct so the hook exits without an effect and leaves it unchanged.
95108
+ */
95109
+ class ProductConsumedIsSerializedUpdatedHook {
95110
+ constructor(durableRecordStore) {
95111
+ this.durableRecordStore = durableRecordStore;
95112
+ this.on = 'patch';
95113
+ this.apiName = ProductConsumed.API_NAME;
95114
+ }
95115
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95116
+ const recordReader = reader ?? this.durableRecordStore;
95117
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
95118
+ if (originalRecord === undefined) {
95119
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Error);
95120
+ instrumentation$4.error(`SFS - ProductConsumedIsSerializedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
95121
+ return [];
95122
+ }
95123
+ const reference = resolveReferenceIdForUpdate(fields, originalRecord.fields, ProductConsumed.Fields.PRODUCT_ITEM_ID);
95124
+ if (reference.kind === 'unresolved') {
95125
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
95126
+ return [];
95127
+ }
95128
+ let isSerialized;
95129
+ if (reference.kind === 'cleared') {
95130
+ // The link was explicitly cleared: the record derives from no ProductItem, so clear
95131
+ // IsProduct2Serialized rather than resolving it from the stale original link.
95132
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95133
+ isSerialized = null;
95134
+ }
95135
+ else {
95136
+ isSerialized = await resolveIsProduct2SerializedFromProductItem(recordReader, reference.id, ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME);
95137
+ if (isSerialized === undefined && reference.source === 'patch') {
95138
+ // The link was reassigned to a new ProductItem whose chain isn't cached yet, so
95139
+ // the stored IsProduct2Serialized still reflects the OLD link and is now stale.
95140
+ // Clear it rather than leaving the stale value alongside the new relationship;
95141
+ // the server recomputes the real value on sync.
95142
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95143
+ isSerialized = null;
95144
+ }
95145
+ }
95146
+ if (isSerialized === undefined) {
95147
+ return [];
95148
+ }
95149
+ const effect = {
95150
+ type: 'record-field-set',
95151
+ fieldName: ProductConsumed.Fields.IS_PRODUCT2_SERIALIZED,
95152
+ value: isSerialized,
95153
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95154
+ tag: draftActionId,
95155
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95156
+ timestamp: draftActionTimestamp,
95157
+ };
95158
+ return [effect];
95159
+ }
95160
+ }
95161
+ ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME = 'ProductConsumedIsSerializedUpdatedHook';
95011
95162
  function getProductItemIdFromRecordFields(fields) {
95012
95163
  const field = fields[ProductConsumed.Fields.PRODUCT_ITEM_ID];
95013
95164
  if (field === undefined || field.value === undefined || typeof field.value !== 'string') {
@@ -95029,29 +95180,265 @@
95029
95180
  }
95030
95181
  return field.value;
95031
95182
  }
95032
- var CounterResult;
95033
- (function (CounterResult) {
95034
- // A side effect was created.
95035
- CounterResult["Created"] = "Created";
95036
- // The hook exited early.
95037
- CounterResult["Exited"] = "Exited";
95038
- // An unexpected error occurred.
95039
- CounterResult["Error"] = "Error";
95040
- })(CounterResult || (CounterResult = {}));
95041
- function incrementCounter(name, result) {
95042
- instrumentation$4.incrementCounter('SFSSideEffect', 1, result === CounterResult.Error, {
95043
- Name: name,
95044
- Result: result,
95045
- });
95183
+
95184
+ const ProductTransfer = {
95185
+ API_NAME: 'ProductTransfer',
95186
+ Fields: {
95187
+ SOURCE_PRODUCT_ITEM_ID: 'SourceProductItemId',
95188
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
95189
+ },
95190
+ };
95191
+ /**
95192
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-created
95193
+ * `ProductTransfer` record.
95194
+ *
95195
+ * The value is resolved through the related `SourceProductItem` record (a `ProductItem`),
95196
+ * mirroring the `ProductConsumed` hook: the ProductItem's own `IsProduct2Serialized` when
95197
+ * cached, otherwise `ProductItem.Product2Id` -> `Product2.IsSerialized` (see
95198
+ * `resolveIsProduct2SerializedFromProductItem`). When the chain cannot be resolved the hook
95199
+ * exits without an effect and the field is left as synthesized (null).
95200
+ */
95201
+ class ProductTransferIsSerializedCreatedHook {
95202
+ constructor(durableRecordStore) {
95203
+ this.durableRecordStore = durableRecordStore;
95204
+ this.on = 'post';
95205
+ this.apiName = ProductTransfer.API_NAME;
95206
+ }
95207
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95208
+ const sourceProductItemId = getStringDraftField(fields, ProductTransfer.Fields.SOURCE_PRODUCT_ITEM_ID);
95209
+ if (sourceProductItemId === undefined) {
95210
+ incrementCounter(ProductTransferIsSerializedCreatedHook.COUNTER_NAME, CounterResult.Exited);
95211
+ return [];
95212
+ }
95213
+ const isSerialized = await resolveIsProduct2SerializedFromProductItem(reader ?? this.durableRecordStore, sourceProductItemId, ProductTransferIsSerializedCreatedHook.COUNTER_NAME);
95214
+ if (isSerialized === undefined) {
95215
+ return [];
95216
+ }
95217
+ const effect = {
95218
+ type: 'record-field-set',
95219
+ fieldName: ProductTransfer.Fields.IS_PRODUCT2_SERIALIZED,
95220
+ value: isSerialized,
95221
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95222
+ tag: draftActionId,
95223
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95224
+ timestamp: draftActionTimestamp,
95225
+ };
95226
+ return [effect];
95227
+ }
95228
+ }
95229
+ ProductTransferIsSerializedCreatedHook.COUNTER_NAME = 'ProductTransferIsSerializedCreatedHook';
95230
+ /**
95231
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-edited
95232
+ * `ProductTransfer` record.
95233
+ *
95234
+ * ProductTransfer can only be edited (not created) on mobile, so this is the hook that fires
95235
+ * in practice. A `patch` body only carries changed fields, so the related `SourceProductItemId`
95236
+ * may be absent; the id is resolved by preferring the patch body (the link is being reassigned)
95237
+ * and falling back to the original record in the durable store, then the value is resolved
95238
+ * through that `ProductItem` exactly as on create (see
95239
+ * `resolveIsProduct2SerializedFromProductItem`).
95240
+ *
95241
+ * When the patch explicitly clears `SourceProductItemId` (sets it to `null`) the record no
95242
+ * longer derives from any `ProductItem`, so `IsProduct2Serialized` is cleared (set to `null`)
95243
+ * rather than resolved from the now-stale original link. Likewise, when the patch reassigns
95244
+ * `SourceProductItemId` to a new id whose chain isn't cached, the stored value reflects the old
95245
+ * link and is stale, so it is cleared. When the link is unchanged and its chain cannot be
95246
+ * resolved, the stored value is still correct so the hook exits without an effect and leaves it
95247
+ * unchanged.
95248
+ */
95249
+ class ProductTransferIsSerializedUpdatedHook {
95250
+ constructor(durableRecordStore) {
95251
+ this.durableRecordStore = durableRecordStore;
95252
+ this.on = 'patch';
95253
+ this.apiName = ProductTransfer.API_NAME;
95254
+ }
95255
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95256
+ const recordReader = reader ?? this.durableRecordStore;
95257
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
95258
+ if (originalRecord === undefined) {
95259
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Error);
95260
+ instrumentation$4.error(`SFS - ProductTransferIsSerializedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
95261
+ return [];
95262
+ }
95263
+ const reference = resolveReferenceIdForUpdate(fields, originalRecord.fields, ProductTransfer.Fields.SOURCE_PRODUCT_ITEM_ID);
95264
+ if (reference.kind === 'unresolved') {
95265
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
95266
+ return [];
95267
+ }
95268
+ let isSerialized;
95269
+ if (reference.kind === 'cleared') {
95270
+ // The link was explicitly cleared: the record derives from no ProductItem, so clear
95271
+ // IsProduct2Serialized rather than resolving it from the stale original link.
95272
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95273
+ isSerialized = null;
95274
+ }
95275
+ else {
95276
+ isSerialized = await resolveIsProduct2SerializedFromProductItem(recordReader, reference.id, ProductTransferIsSerializedUpdatedHook.COUNTER_NAME);
95277
+ if (isSerialized === undefined && reference.source === 'patch') {
95278
+ // The link was reassigned to a new ProductItem whose chain isn't cached yet, so
95279
+ // the stored IsProduct2Serialized still reflects the OLD link and is now stale.
95280
+ // Clear it rather than leaving the stale value alongside the new relationship;
95281
+ // the server recomputes the real value on sync.
95282
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95283
+ isSerialized = null;
95284
+ }
95285
+ }
95286
+ if (isSerialized === undefined) {
95287
+ return [];
95288
+ }
95289
+ const effect = {
95290
+ type: 'record-field-set',
95291
+ fieldName: ProductTransfer.Fields.IS_PRODUCT2_SERIALIZED,
95292
+ value: isSerialized,
95293
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95294
+ tag: draftActionId,
95295
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95296
+ timestamp: draftActionTimestamp,
95297
+ };
95298
+ return [effect];
95299
+ }
95046
95300
  }
95301
+ ProductTransferIsSerializedUpdatedHook.COUNTER_NAME = 'ProductTransferIsSerializedUpdatedHook';
95302
+
95303
+ const ProductItem = {
95304
+ API_NAME: 'ProductItem',
95305
+ Fields: {
95306
+ PRODUCT2_ID: 'Product2Id',
95307
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
95308
+ },
95309
+ };
95310
+ /**
95311
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-created
95312
+ * `ProductItem` record.
95313
+ *
95314
+ * Unlike `ProductConsumed`/`ProductTransfer` — which reach the product through a related
95315
+ * `ProductItem` — a `ProductItem` carries `Product2Id` directly, so the value is resolved as
95316
+ * `Product2Id` -> `Product2.IsSerialized` (see `resolveIsSerializedFromProduct2Id`). When the
95317
+ * chain cannot be resolved the hook exits without an effect and the field is left as
95318
+ * synthesized (null).
95319
+ */
95320
+ class ProductItemIsSerializedCreatedHook {
95321
+ constructor(durableRecordStore) {
95322
+ this.durableRecordStore = durableRecordStore;
95323
+ this.on = 'post';
95324
+ this.apiName = ProductItem.API_NAME;
95325
+ }
95326
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95327
+ const product2Id = getStringDraftField(fields, ProductItem.Fields.PRODUCT2_ID);
95328
+ if (product2Id === undefined) {
95329
+ incrementCounter(ProductItemIsSerializedCreatedHook.COUNTER_NAME, CounterResult.Exited);
95330
+ return [];
95331
+ }
95332
+ const isSerialized = await resolveIsSerializedFromProduct2Id(reader ?? this.durableRecordStore, product2Id, ProductItemIsSerializedCreatedHook.COUNTER_NAME);
95333
+ if (isSerialized === undefined) {
95334
+ return [];
95335
+ }
95336
+ const effect = {
95337
+ type: 'record-field-set',
95338
+ fieldName: ProductItem.Fields.IS_PRODUCT2_SERIALIZED,
95339
+ value: isSerialized,
95340
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95341
+ tag: draftActionId,
95342
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95343
+ timestamp: draftActionTimestamp,
95344
+ };
95345
+ return [effect];
95346
+ }
95347
+ }
95348
+ ProductItemIsSerializedCreatedHook.COUNTER_NAME = 'ProductItemIsSerializedCreatedHook';
95349
+ /**
95350
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-edited
95351
+ * `ProductItem` record.
95352
+ *
95353
+ * A `patch` body only carries changed fields, so `Product2Id` may be absent; it is resolved by
95354
+ * preferring the patch body (the product is being reassigned) and falling back to the original
95355
+ * record in the durable store, then resolved as `Product2Id` -> `Product2.IsSerialized` (see
95356
+ * `resolveIsSerializedFromProduct2Id`).
95357
+ *
95358
+ * When the patch explicitly clears `Product2Id` (sets it to `null`) the record no longer
95359
+ * derives from any `Product2`, so `IsProduct2Serialized` is cleared (set to `null`) rather than
95360
+ * resolved from the now-stale original link. Likewise, when the patch reassigns `Product2Id` to
95361
+ * a new id whose `Product2` isn't cached, the stored value reflects the old link and is stale,
95362
+ * so it is cleared. When the link is unchanged and its `Product2` cannot be resolved, the stored
95363
+ * value is still correct so the hook exits without an effect and leaves it unchanged.
95364
+ */
95365
+ class ProductItemIsSerializedUpdatedHook {
95366
+ constructor(durableRecordStore) {
95367
+ this.durableRecordStore = durableRecordStore;
95368
+ this.on = 'patch';
95369
+ this.apiName = ProductItem.API_NAME;
95370
+ }
95371
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95372
+ const recordReader = reader ?? this.durableRecordStore;
95373
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
95374
+ if (originalRecord === undefined) {
95375
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Error);
95376
+ instrumentation$4.error(`SFS - ProductItemIsSerializedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
95377
+ return [];
95378
+ }
95379
+ const reference = resolveReferenceIdForUpdate(fields, originalRecord.fields, ProductItem.Fields.PRODUCT2_ID);
95380
+ if (reference.kind === 'unresolved') {
95381
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
95382
+ return [];
95383
+ }
95384
+ let isSerialized;
95385
+ if (reference.kind === 'cleared') {
95386
+ // The link was explicitly cleared: the record derives from no Product2, so clear
95387
+ // IsProduct2Serialized rather than resolving it from the stale original link.
95388
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95389
+ isSerialized = null;
95390
+ }
95391
+ else {
95392
+ isSerialized = await resolveIsSerializedFromProduct2Id(recordReader, reference.id, ProductItemIsSerializedUpdatedHook.COUNTER_NAME);
95393
+ if (isSerialized === undefined && reference.source === 'patch') {
95394
+ // The link was reassigned to a new Product2 that isn't cached yet, so the stored
95395
+ // IsProduct2Serialized still reflects the OLD link and is now stale. Clear it
95396
+ // rather than leaving the stale value alongside the new relationship; the server
95397
+ // recomputes the real value on sync.
95398
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95399
+ isSerialized = null;
95400
+ }
95401
+ }
95402
+ if (isSerialized === undefined) {
95403
+ return [];
95404
+ }
95405
+ const effect = {
95406
+ type: 'record-field-set',
95407
+ fieldName: ProductItem.Fields.IS_PRODUCT2_SERIALIZED,
95408
+ value: isSerialized,
95409
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95410
+ tag: draftActionId,
95411
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95412
+ timestamp: draftActionTimestamp,
95413
+ };
95414
+ return [effect];
95415
+ }
95416
+ }
95417
+ ProductItemIsSerializedUpdatedHook.COUNTER_NAME = 'ProductItemIsSerializedUpdatedHook';
95047
95418
 
95048
95419
  function createSfsSideEffectHooks(durableRecordStore) {
95049
95420
  if (productConsumedSideEffects.isOpen({ fallback: false })) {
95050
- return [
95421
+ const hooks = [
95051
95422
  new ProductConsumedCreatedHook(),
95052
95423
  new ProductConsumedUpdatedHook(durableRecordStore),
95053
95424
  new ProductConsumedDeletedHook(durableRecordStore),
95054
95425
  ];
95426
+ // The IsProduct2Serialized side effects (ProductConsumed, ProductTransfer and
95427
+ // ProductItem) ship on by default and can be disabled independently of the van stock
95428
+ // hooks above via a shared kill switch: the gate is closed (fallback false) by default,
95429
+ // which keeps the effects enabled, and opening the gate turns them off.
95430
+ //
95431
+ // Each object gets both a create (post) and edit (patch) hook so the field is
95432
+ // synthesized on either draft action, keeping the behavior uniform across the objects.
95433
+ if (!isProduct2SerializedSideEffectKillSwitch.isOpen({ fallback: false })) {
95434
+ hooks.push(new ProductConsumedIsSerializedCreatedHook(durableRecordStore));
95435
+ hooks.push(new ProductConsumedIsSerializedUpdatedHook(durableRecordStore));
95436
+ hooks.push(new ProductTransferIsSerializedCreatedHook(durableRecordStore));
95437
+ hooks.push(new ProductTransferIsSerializedUpdatedHook(durableRecordStore));
95438
+ hooks.push(new ProductItemIsSerializedCreatedHook(durableRecordStore));
95439
+ hooks.push(new ProductItemIsSerializedUpdatedHook(durableRecordStore));
95440
+ }
95441
+ return hooks;
95055
95442
  }
95056
95443
  return [];
95057
95444
  }
@@ -97419,7 +97806,7 @@
97419
97806
  },
97420
97807
  };
97421
97808
  }
97422
- // version: 1.460.0-f7dc7797dd
97809
+ // version: 1.462.0-50d9a13ba4
97423
97810
 
97424
97811
  /**
97425
97812
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -97445,7 +97832,7 @@
97445
97832
  },
97446
97833
  };
97447
97834
  }
97448
- // version: 1.460.0-f7dc7797dd
97835
+ // version: 1.462.0-50d9a13ba4
97449
97836
 
97450
97837
  function findExecutableOperation$1(input) {
97451
97838
  const operations = input.query.definitions.filter(
@@ -99834,9 +100221,6 @@
99834
100221
 
99835
100222
  const PURGE_CACHE_DAYS = 30;
99836
100223
  function initializeOneStore(sqliteStore, luvio) {
99837
- if (!useOneStore.isOpen({ fallback: true })) {
99838
- return;
99839
- }
99840
100224
  // Initialize performance.now() fallback if not available
99841
100225
  if (typeof globalThis.performance === 'undefined') {
99842
100226
  globalThis.performance = { now: () => Date.now() };
@@ -100023,12 +100407,6 @@
100023
100407
  lazyInternalLuvio = internalLuvio;
100024
100408
  lazyObjectInfoService = new ObjectInfoService(getObjectInfo, getObjectInfos, getObjectInfoDirectory, lazyDurableStore);
100025
100409
  const baseEnv = new Environment(store, lazyNetworkAdapter);
100026
- const shouldFilterFieldsOnRevive = (key) => {
100027
- if (reviveOnlyRequestedFields.isOpen({ fallback: true })) {
100028
- return isStoreKeyRecordId(key);
100029
- }
100030
- return false;
100031
- };
100032
100410
  const gqlEnv = makeEnvironmentGraphqlAware(baseEnv);
100033
100411
  const durableEnv = makeDurable(gqlEnv, {
100034
100412
  durableStore: lazyDurableStore,
@@ -100036,7 +100414,12 @@
100036
100414
  // disable luvio deep freeze in headless environments
100037
100415
  disableDeepFreeze: typeof window === 'undefined',
100038
100416
  shouldFlush,
100039
- shouldFilterFieldsOnRevive,
100417
+ // Disabling field filtering on revive for now (previously gated behind
100418
+ // the lmr.reviveOnlyRequestedFields gate). The code path is retained
100419
+ // below should we need to re-enable this behavior in the future:
100420
+ shouldFilterFieldsOnRevive: (_key) => {
100421
+ return false;
100422
+ },
100040
100423
  });
100041
100424
  // draft queue
100042
100425
  lazyDraftQueue = buildLdsDraftQueue(lazyDurableStore);
@@ -100154,7 +100537,7 @@
100154
100537
  id: '@salesforce/lds-network-adapter',
100155
100538
  instrument: instrument$2,
100156
100539
  });
100157
- // version: 1.460.0-f7dc7797dd
100540
+ // version: 1.462.0-50d9a13ba4
100158
100541
 
100159
100542
  const { create: create$2, keys: keys$2 } = Object;
100160
100543
  const { stringify, parse } = JSON;
@@ -107706,7 +108089,7 @@
107706
108089
  cb(graphql_v1_import, graphql_imperative$1, graphql_imperative_legacy_v1_import, graphql_state_manager, useOneStoreGraphQL);
107707
108090
  }
107708
108091
  }
107709
- // version: 1.460.0-3e24f1bbff
108092
+ // version: 1.462.0-2919385b97
107710
108093
 
107711
108094
  function createFragmentMap(documentNode) {
107712
108095
  const fragments = {};
@@ -136949,7 +137332,7 @@
136949
137332
  configuration: { ...configurationForGraphQLAdapters$1 },
136950
137333
  instrument: instrument$1,
136951
137334
  });
136952
- // version: 1.460.0-3e24f1bbff
137335
+ // version: 1.462.0-2919385b97
136953
137336
 
136954
137337
  // On core the unstable adapters are re-exported with different names,
136955
137338
  // we want to match them here.
@@ -137101,7 +137484,7 @@
137101
137484
  unstable_graphQL_imperative = createImperativeAdapter(luvio, createInstrumentedAdapter(ldsAdapter, adapterMetadata), adapterMetadata);
137102
137485
  graphQLImperative = ldsAdapter;
137103
137486
  });
137104
- // version: 1.460.0-3e24f1bbff
137487
+ // version: 1.462.0-2919385b97
137105
137488
 
137106
137489
  var gqlApi = /*#__PURE__*/Object.freeze({
137107
137490
  __proto__: null,
@@ -137900,7 +138283,7 @@
137900
138283
  function register(r) {
137901
138284
  callbacks$1.forEach((callback) => callback(r));
137902
138285
  }
137903
- // version: 1.460.0-f7dc7797dd
138286
+ // version: 1.462.0-50d9a13ba4
137904
138287
 
137905
138288
  /**
137906
138289
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -139210,4 +139593,4 @@
139210
139593
  exports.subscribeToAdapter = subscribeToAdapter;
139211
139594
 
139212
139595
  }));
139213
- // version: 1.460.0-f7dc7797dd
139596
+ // version: 1.462.0-50d9a13ba4