@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.
@@ -4276,7 +4276,7 @@ function withDefaultLuvio(callback) {
4276
4276
  }
4277
4277
  callbacks.push(callback);
4278
4278
  }
4279
- // version: 1.460.0-f7dc7797dd
4279
+ // version: 1.462.0-50d9a13ba4
4280
4280
 
4281
4281
  // TODO [TD-0081508]: once that TD is fulfilled we can probably change this file
4282
4282
  function instrumentAdapter$1(createFunction, _metadata) {
@@ -5320,7 +5320,7 @@ function createGraphQLWireAdapterConstructor(luvio, adapter, metadata, astResolv
5320
5320
  const { apiFamily, name } = metadata;
5321
5321
  return createGraphQLWireAdapterConstructor$1(adapter, `${apiFamily}.${name}`, luvio, astResolver);
5322
5322
  }
5323
- // version: 1.460.0-f7dc7797dd
5323
+ // version: 1.462.0-50d9a13ba4
5324
5324
 
5325
5325
  function isSupportedEntity(_objectApiName) {
5326
5326
  return true;
@@ -32547,7 +32547,7 @@ withDefaultLuvio((luvio) => {
32547
32547
  throttle(60, 60000, setupNotifyAllListRecordUpdateAvailable(luvio));
32548
32548
  throttle(60, 60000, setupNotifyAllListInfoSummaryUpdateAvailable(luvio));
32549
32549
  });
32550
- // version: 1.460.0-3e24f1bbff
32550
+ // version: 1.462.0-2919385b97
32551
32551
 
32552
32552
  var draftQueueMaxRetryAttemptsGate = {
32553
32553
  isOpen: function (e) {
@@ -44179,24 +44179,6 @@ function formattingOptions(pattern) {
44179
44179
  return pattern;
44180
44180
  }
44181
44181
 
44182
- var ldsPrimingGraphqlBatch = {
44183
- isOpen: function (e) {
44184
- return e.fallback;
44185
- },
44186
- hasError: function () {
44187
- return !0;
44188
- },
44189
- };
44190
-
44191
- var lmrPrimingUseSoql = {
44192
- isOpen: function (e) {
44193
- return e.fallback;
44194
- },
44195
- hasError: function () {
44196
- return !0;
44197
- },
44198
- };
44199
-
44200
44182
  var aggressiveTrimCountKillSwitchGate = {
44201
44183
  isOpen: function (e) {
44202
44184
  return e.fallback;
@@ -44215,15 +44197,6 @@ var graphqlL2AdapterGate = {
44215
44197
  },
44216
44198
  };
44217
44199
 
44218
- var useOneStore = {
44219
- isOpen: function (e) {
44220
- return e.fallback;
44221
- },
44222
- hasError: function () {
44223
- return !0;
44224
- },
44225
- };
44226
-
44227
44200
  const { create: create$7, freeze: freeze$4, keys: keys$8, entries: entries$5 } = Object;
44228
44201
  const { hasOwnProperty: hasOwnProperty$3 } = Object.prototype;
44229
44202
  const { isArray: isArray$7 } = Array;
@@ -45943,7 +45916,7 @@ var productConsumedSideEffects = {
45943
45916
  },
45944
45917
  };
45945
45918
 
45946
- var reviveOnlyRequestedFields = {
45919
+ var isProduct2SerializedSideEffectKillSwitch = {
45947
45920
  isOpen: function (e) {
45948
45921
  return e.fallback;
45949
45922
  },
@@ -81464,6 +81437,35 @@ async function getAdapterData(adapter, config) {
81464
81437
  return snapshot.data;
81465
81438
  }
81466
81439
 
81440
+ /**
81441
+ * A read-through cache over `DurableRecordStore.getRecord` scoped to a single draft-action
81442
+ * dispatch. Multiple side-effect hooks fire for one action (e.g. the van-stock and
81443
+ * IsProduct2Serialized ProductConsumed hooks) and are awaited one at a time, so they otherwise
81444
+ * each re-read the same draft record — and the IsProduct2Serialized chain re-reads the same
81445
+ * related ProductItem / Product2 — from the durable store. Sharing one reader across the
81446
+ * action's hooks collapses those repeated reads to a single durable call per key.
81447
+ *
81448
+ * The cache stores the in-flight promise (not the resolved value) so concurrent reads of the
81449
+ * same key share one durable call, and it caches `undefined` results so a miss is not re-fetched.
81450
+ * A reader must not outlive one action dispatch: durable state changes between actions, so a
81451
+ * longer-lived cache would return stale records.
81452
+ */
81453
+ class MemoizingRecordReader {
81454
+ constructor(durableRecordStore) {
81455
+ this.durableRecordStore = durableRecordStore;
81456
+ this.cache = new Map();
81457
+ }
81458
+ getRecord(key) {
81459
+ const cached = this.cache.get(key);
81460
+ if (cached !== undefined) {
81461
+ return cached;
81462
+ }
81463
+ const promise = this.durableRecordStore.getRecord(key);
81464
+ this.cache.set(key, promise);
81465
+ return promise;
81466
+ }
81467
+ }
81468
+
81467
81469
  const LDS_ACTION_HANDLER_ID = 'LDS_ACTION_HANDLER';
81468
81470
  const LDS_ACTION_METADATA_API_NAME = 'LDS_ACTION_METADATA_API_NAME';
81469
81471
  class UiApiActionHandler extends AbstractResourceRequestActionHandler {
@@ -81584,9 +81586,9 @@ class UiApiActionHandler extends AbstractResourceRequestActionHandler {
81584
81586
  return super.getRedirectMappings(action);
81585
81587
  }
81586
81588
  async handleActionCompleted(completedAction, queueOperations) {
81587
- if (completedAction.response.status === 204 && completedAction.data.method === 'post') {
81588
- // if we get a 204 it means the record creation was successful but we don't have the record to ingest
81589
- // remove the synthesized draft and do not try to ingest the response
81589
+ const { method } = completedAction.data;
81590
+ if (completedAction.response.status === 204 && (method === 'post' || method === 'patch')) {
81591
+ await this.recordService.removeSideEffectsForAction(completedAction);
81590
81592
  await this.evictKey(completedAction.tag);
81591
81593
  return;
81592
81594
  }
@@ -81667,8 +81669,15 @@ class UiApiActionHandler extends AbstractResourceRequestActionHandler {
81667
81669
  break;
81668
81670
  }
81669
81671
  }
81672
+ // One reader shared across every hook for this action so repeated reads of the same
81673
+ // record (the draft record, and any related records the hooks resolve) collapse to a
81674
+ // single durable call. Scoped to this dispatch only — durable state changes between
81675
+ // actions, so it must not be reused across them. Built only when hooks will run.
81676
+ const recordReader = hookTriggers.length > 0
81677
+ ? new MemoizingRecordReader(this.durableRecordStore)
81678
+ : undefined;
81670
81679
  for (const trigger of hookTriggers) {
81671
- const hookEffects = await trigger.then(action.id, action.timestamp, action.targetId, action.data.body?.fields ?? {});
81680
+ const hookEffects = await trigger.then(action.id, action.timestamp, action.targetId, action.data.body?.fields ?? {}, recordReader);
81672
81681
  for (const hookEffect of hookEffects) {
81673
81682
  effects.push(hookEffect);
81674
81683
  }
@@ -92740,211 +92749,6 @@ class NetworkRecordLoader {
92740
92749
  }
92741
92750
  }
92742
92751
 
92743
- const apiPath$1 = basePath + '/graphql';
92744
- const requiredPrefix = `required_`;
92745
- const requiredFieldMap = {
92746
- ApiName: 'ApiName',
92747
- Id: 'Id',
92748
- LastModifiedById: 'LastModifiedById { value }',
92749
- LastModifiedDate: 'LastModifiedDate { value }',
92750
- RecordTypeId: 'RecordTypeId(fallback: true) { value }',
92751
- SystemModstamp: 'SystemModstamp { value }',
92752
- WeakEtag: 'WeakEtag',
92753
- };
92754
- class RecordLoaderGraphQL extends NetworkRecordLoader {
92755
- makeRequest(batch, observabilityContext) {
92756
- const query = this.generateGraphQLQuery(batch.type, batch.fields);
92757
- return {
92758
- request: {
92759
- method: 'POST',
92760
- path: apiPath$1,
92761
- body: JSON.stringify({
92762
- query,
92763
- variables: {
92764
- ids: batch.ids,
92765
- first: batch.ids.length,
92766
- },
92767
- }),
92768
- headers: {},
92769
- queryParams: {},
92770
- priority: 'background',
92771
- observabilityContext: observabilityContext !== undefined ? observabilityContext : {},
92772
- },
92773
- context: undefined,
92774
- };
92775
- }
92776
- generateFetchResults(repResult, batchInput, _context) {
92777
- const { data, errors } = repResult;
92778
- if (errors !== undefined && errors.length > 0) {
92779
- // right now if there are any errors in the batch we throw out the entire batch
92780
- // for now this is ok all errors will originate on the same node so there shouldn't be a mix of errors and data
92781
- return [
92782
- {
92783
- missingIds: batchInput.ids,
92784
- ok: false,
92785
- error: 'request-error',
92786
- messages: errors.map((x) => x.message),
92787
- },
92788
- ];
92789
- }
92790
- // this shouldn't really happen if there's no errors
92791
- if (!data || !data.uiapi) {
92792
- return [
92793
- {
92794
- ok: false,
92795
- error: 'unknown',
92796
- messages: ['unexpected response retrieved from graphql endpoint'],
92797
- missingIds: batchInput.ids,
92798
- },
92799
- ];
92800
- }
92801
- const objectInfo = batchInput.objectInfo;
92802
- const seenRecords = new Set(batchInput.ids);
92803
- const records = data.uiapi.query[batchInput.type].edges.map((edge) => {
92804
- const record = this.generateDurableRecordRepresentation(edge.node, objectInfo);
92805
- seenRecords.delete(record.id);
92806
- return record;
92807
- });
92808
- return [
92809
- {
92810
- ok: true,
92811
- records,
92812
- missingIds: Array.from(seenRecords),
92813
- },
92814
- ];
92815
- }
92816
- generateGraphQLQuery(type, fields) {
92817
- const fieldList = Object.keys(requiredFieldMap)
92818
- .map((field) => {
92819
- return `${requiredPrefix}${field}: ${requiredFieldMap[field]}`;
92820
- })
92821
- .join('\n');
92822
- return /* GraphQL */ `
92823
- fragment requiredRecordFields on Record {
92824
- ${fieldList}
92825
- }
92826
- query prime${type}($ids: [ID], $first: Int){
92827
- uiapi {
92828
- query {
92829
- ${type} (where: {Id: {in: $ids}} first: $first) {
92830
- edges {
92831
- node {
92832
- ...requiredRecordFields
92833
- ${fields
92834
- .filter((x) => x !== 'Id' && x !== 'RecordTypeId')
92835
- .map((field) => `${field} { value displayValue }`)}
92836
- }
92837
- }
92838
- }
92839
- }
92840
- }
92841
- }
92842
- `;
92843
- }
92844
- generateDurableRecordRepresentation(node, objectInfo) {
92845
- const standardRecordFields = new Set(Object.keys(requiredFieldMap).map((x) => `${requiredPrefix}${x}`));
92846
- const id = node[`${requiredPrefix}Id`];
92847
- const recordTypeId = node[`${requiredPrefix}RecordTypeId`].value;
92848
- const requestedFields = Object.keys(node).filter((field) => !standardRecordFields.has(field));
92849
- const fields = requestedFields.reduce((acc, field) => {
92850
- const fieldNode = node[field];
92851
- if (fieldNode.displayValue === undefined) {
92852
- fieldNode.displayValue = null;
92853
- }
92854
- acc[field] = node[field];
92855
- return acc;
92856
- }, {});
92857
- fields['Id'] = { value: id, displayValue: null };
92858
- if (objectInfo.fields['RecordTypeId'] !== undefined) {
92859
- fields['RecordTypeId'] = { value: recordTypeId, displayValue: null };
92860
- }
92861
- let recordTypeInfo = null;
92862
- if (recordTypeId !== null &&
92863
- objectInfo.recordTypeInfos &&
92864
- objectInfo.recordTypeInfos[recordTypeId]) {
92865
- const appliedRecordTypeInfo = objectInfo.recordTypeInfos[recordTypeId];
92866
- // ui-api only applies the record type if its not the default type, otherwise it sets it to null
92867
- if (appliedRecordTypeInfo.defaultRecordTypeMapping === false) {
92868
- recordTypeInfo = appliedRecordTypeInfo;
92869
- }
92870
- }
92871
- return {
92872
- apiName: node[`${requiredPrefix}ApiName`],
92873
- childRelationships: {},
92874
- eTag: '',
92875
- id,
92876
- lastModifiedById: node[`${requiredPrefix}LastModifiedById`] &&
92877
- node[`${requiredPrefix}LastModifiedById`].value
92878
- ? node[`${requiredPrefix}LastModifiedById`].value
92879
- : null,
92880
- lastModifiedDate: node[`${requiredPrefix}LastModifiedDate`] &&
92881
- node[`${requiredPrefix}LastModifiedDate`].value
92882
- ? node[`${requiredPrefix}LastModifiedDate`].value
92883
- : null,
92884
- recordTypeId,
92885
- recordTypeInfo: recordTypeInfo,
92886
- systemModstamp: node[`${requiredPrefix}SystemModstamp`] &&
92887
- node[`${requiredPrefix}SystemModstamp`].value
92888
- ? node[`${requiredPrefix}SystemModstamp`].value
92889
- : null,
92890
- weakEtag: node[`${requiredPrefix}WeakEtag`],
92891
- fields,
92892
- links: {},
92893
- };
92894
- }
92895
- }
92896
-
92897
- const DEFAULT_GQL_QUERY_BATCH_SIZE = 100;
92898
- const apiPath = basePath + '/graphql/batch';
92899
- class RecordLoaderGraphQLBatch extends RecordLoaderGraphQL {
92900
- makeRequest(batch, observabilityContext) {
92901
- const query = this.generateGraphQLQuery(batch.type, batch.fields);
92902
- const chunks = chunk(batch.ids, DEFAULT_GQL_QUERY_BATCH_SIZE);
92903
- const batches = chunks.map((chunk) => {
92904
- return {
92905
- query,
92906
- variables: {
92907
- ids: chunk,
92908
- first: chunk.length,
92909
- },
92910
- };
92911
- });
92912
- return {
92913
- request: {
92914
- method: 'POST',
92915
- path: apiPath,
92916
- body: JSON.stringify({
92917
- batchQuery: batches,
92918
- }),
92919
- headers: {},
92920
- queryParams: {},
92921
- priority: 'background',
92922
- observabilityContext: observabilityContext !== undefined ? observabilityContext : {},
92923
- },
92924
- context: chunks,
92925
- };
92926
- }
92927
- generateFetchResults(body, batchInput, context) {
92928
- const { results } = body;
92929
- const fetchResults = context.flatMap((chunk, index) => {
92930
- const result = results[index];
92931
- if (!result || result.statusCode !== 200) {
92932
- return {
92933
- ok: false,
92934
- error: 'request-error',
92935
- messages: ['unexpected response from graphql batch endpoint'],
92936
- missingIds: chunk,
92937
- };
92938
- }
92939
- return super.generateFetchResults(result.result, {
92940
- ...batchInput,
92941
- ids: chunk,
92942
- }, undefined);
92943
- });
92944
- return fetchResults;
92945
- }
92946
- }
92947
-
92948
92752
  class SoqlRecordConverter {
92949
92753
  /**
92950
92754
  *
@@ -93437,16 +93241,7 @@ class LdsPrimingRecordRefresher {
93437
93241
  function primingSessionFactory(config) {
93438
93242
  const { store, objectInfoService, getLuvio } = config;
93439
93243
  const networkAdapter = new NimbusPrimingNetworkAdapter();
93440
- let recordLoader;
93441
- if (lmrPrimingUseSoql.isOpen({ fallback: false })) {
93442
- recordLoader = new RecordLoaderSOQLComposite(networkAdapter);
93443
- }
93444
- else if (ldsPrimingGraphqlBatch.isOpen({ fallback: false })) {
93445
- recordLoader = new RecordLoaderGraphQLBatch(networkAdapter);
93446
- }
93447
- else {
93448
- recordLoader = new RecordLoaderGraphQL(networkAdapter);
93449
- }
93244
+ const recordLoader = new RecordLoaderSOQLComposite(networkAdapter);
93450
93245
  const primingStore = new SqlitePrimingStore(getLuvio, store);
93451
93246
  const recordIngestor = new RecordIngestor(primingStore, getLuvio);
93452
93247
  const session = new PrimingSession({
@@ -94099,6 +93894,38 @@ function buildCachedSnapshotCachePolicy$e(context, storeLookup) {
94099
93894
  }
94100
93895
 
94101
93896
  const SIDE_EFFECT_SEGMENT = 'RECORD_SIDE_EFFECTS';
93897
+ // Base-record effects rebuild (or remove) the entire record for a key, so they must be
93898
+ // applied before any field-level effect on that same key — otherwise a field effect that
93899
+ // runs first is discarded when the base record is (re)built. keyToEffectMap ordering is not
93900
+ // stable: it is re-materialized from the durable store on reload / onChanged replay, where
93901
+ // entry order is not guaranteed. So two field-set effects on the same key from different
93902
+ // actions (e.g. two queued edits of one field) can come back in either order; applied out of
93903
+ // order the OLDER value wins and the newer edit is lost. Effects must therefore be applied in
93904
+ // action chronology (by timestamp), with base-record ops ordered ahead of field ops only as
93905
+ // the tie-breaker within one action — create/field effects on a draft-created record share a
93906
+ // key AND a timestamp, so timestamp alone can't order those.
93907
+ const BASE_RECORD_EFFECT_TYPES = new Set(['record-create', 'record-update', 'record-delete']);
93908
+ function orderEffectsForApply(effects) {
93909
+ // Nothing to reorder for a single effect; return the original array to avoid allocating on
93910
+ // the hot read-out path.
93911
+ if (effects.length <= 1) {
93912
+ return effects;
93913
+ }
93914
+ // Primary key: timestamp (action chronology). Secondary key: base-record ops before field
93915
+ // ops, which only matters within one action's shared timestamp. Array.prototype.sort is
93916
+ // stable (ES2019), so effects that tie on both keys keep their existing relative order.
93917
+ return [...effects].sort((a, b) => {
93918
+ if (a.timestamp !== b.timestamp) {
93919
+ return a.timestamp - b.timestamp;
93920
+ }
93921
+ const aIsBase = BASE_RECORD_EFFECT_TYPES.has(a.type);
93922
+ const bIsBase = BASE_RECORD_EFFECT_TYPES.has(b.type);
93923
+ if (aIsBase === bIsBase) {
93924
+ return 0;
93925
+ }
93926
+ return aIsBase ? -1 : 1;
93927
+ });
93928
+ }
94102
93929
  class SideEffectStore {
94103
93930
  constructor(durableStore) {
94104
93931
  this.durableStore = durableStore;
@@ -94177,7 +94004,7 @@ class SideEffectStore {
94177
94004
  if (effects === undefined) {
94178
94005
  return [];
94179
94006
  }
94180
- return effects;
94007
+ return orderEffectsForApply(effects);
94181
94008
  }
94182
94009
  getEffectsByTag(tag) {
94183
94010
  // This scan is still O(N) over every effect regardless of the incremental-map
@@ -94674,6 +94501,58 @@ class RecordFieldIncrementHandler {
94674
94501
  return record;
94675
94502
  }
94676
94503
  }
94504
+ class RecordFieldSetHandler {
94505
+ constructor() {
94506
+ this.logger = typeof __nimbus !== 'undefined' &&
94507
+ __nimbus.plugins !== undefined &&
94508
+ __nimbus.plugins.JSLoggerPlugin !== undefined
94509
+ ? __nimbus.plugins.JSLoggerPlugin
94510
+ : undefined;
94511
+ }
94512
+ apply(record, effect) {
94513
+ if (record === undefined) {
94514
+ if (this.logger) {
94515
+ this.logger.logWarning('RecordFieldSetHandler - cannot apply set side effect to non existing record');
94516
+ }
94517
+ return undefined;
94518
+ }
94519
+ // fieldName is an unconstrained string that has round-tripped through the durable
94520
+ // store, so a value like `__proto__` would hit the prototype setter on the plain
94521
+ // `fields`/`serverValues` objects below rather than creating an own field (prototype
94522
+ // pollution). Reject dangerous keys before any dynamic read/write and skip the effect.
94523
+ if (isUnsafeFieldName(effect.fieldName)) {
94524
+ if (this.logger) {
94525
+ this.logger.logError(`RecordFieldSetHandler - unsafe field name ${effect.fieldName}, skip applying side effect`);
94526
+ }
94527
+ return record;
94528
+ }
94529
+ applyDraftNode(record, effect);
94530
+ record.drafts.edited = true;
94531
+ // capture the server value so it can be restored when the draft syncs.
94532
+ // unlike the increment handler, the field may not yet exist on the record
94533
+ // (e.g. IsProduct2Serialized is not part of the draft-created record), in
94534
+ // which case we record `null` to indicate we never had the field.
94535
+ if (record.drafts.created === false) {
94536
+ const serverValue = record.drafts.serverValues[effect.fieldName];
94537
+ if (serverValue === undefined) {
94538
+ const existingField = record.fields[effect.fieldName];
94539
+ record.drafts.serverValues[effect.fieldName] =
94540
+ existingField === undefined ? null : existingField;
94541
+ }
94542
+ }
94543
+ record.fields[effect.fieldName] = {
94544
+ value: effect.value,
94545
+ displayValue: null,
94546
+ };
94547
+ return record;
94548
+ }
94549
+ }
94550
+ // Field names that would target the prototype chain rather than an own property when used
94551
+ // to index a plain object, enabling prototype pollution on the record's field containers.
94552
+ const UNSAFE_FIELD_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
94553
+ function isUnsafeFieldName(fieldName) {
94554
+ return UNSAFE_FIELD_NAMES.has(fieldName);
94555
+ }
94677
94556
  function applyDraftNode(record, effect) {
94678
94557
  if (record.drafts === undefined) {
94679
94558
  record.drafts = {
@@ -94708,6 +94587,7 @@ const handlerMap = {
94708
94587
  'record-update': new UpdateRecordEffectHandler(),
94709
94588
  'record-delete': new DeleteRecordEffectHandler(),
94710
94589
  'record-field-increment': new RecordFieldIncrementHandler(),
94590
+ 'record-field-set': new RecordFieldSetHandler(),
94711
94591
  };
94712
94592
  /**
94713
94593
  * Removes any existing effects on a record and re-applies the given ordered list and returns the record
@@ -94872,14 +94752,181 @@ function isStoreRecordError(storeRecord) {
94872
94752
  }
94873
94753
 
94874
94754
  const instrumentation$4 = getInstrumentation();
94755
+ const ProductItem$2 = {
94756
+ Fields: {
94757
+ PRODUCT2_ID: 'Product2Id',
94758
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
94759
+ },
94760
+ };
94761
+ const Product2 = {
94762
+ Fields: {
94763
+ IS_SERIALIZED: 'IsSerialized',
94764
+ },
94765
+ };
94766
+ var CounterResult;
94767
+ (function (CounterResult) {
94768
+ // A side effect was created.
94769
+ CounterResult["Created"] = "Created";
94770
+ // The hook exited early.
94771
+ CounterResult["Exited"] = "Exited";
94772
+ // An unexpected error occurred.
94773
+ CounterResult["Error"] = "Error";
94774
+ })(CounterResult || (CounterResult = {}));
94775
+ function incrementCounter(name, result) {
94776
+ instrumentation$4.incrementCounter('SFSSideEffect', 1, result === CounterResult.Error, {
94777
+ Name: name,
94778
+ Result: result,
94779
+ });
94780
+ }
94781
+ /**
94782
+ * Reads a string-valued field off a draft-create `fields` map (the raw create body, where
94783
+ * values are the primitives themselves rather than `{ value }` wrappers). Returns `undefined`
94784
+ * when the field is absent or not a string.
94785
+ */
94786
+ function getStringDraftField(fields, fieldName) {
94787
+ const field = fields[fieldName];
94788
+ if (field === undefined || typeof field !== 'string') {
94789
+ return undefined;
94790
+ }
94791
+ return field;
94792
+ }
94793
+ /**
94794
+ * Reads a string-valued field off a durable record's `fields` map (where each value is a
94795
+ * `{ value }` wrapper). Returns `undefined` when the field is absent, null, or not a string.
94796
+ */
94797
+ function getStringRecordField(fields, fieldName) {
94798
+ const field = fields[fieldName];
94799
+ if (field === undefined || field.value === undefined || typeof field.value !== 'string') {
94800
+ return undefined;
94801
+ }
94802
+ return field.value;
94803
+ }
94804
+ /**
94805
+ * Resolves the reference id that points at a related record for a draft edit.
94806
+ *
94807
+ * On a `patch` the body only carries the fields being changed, so the link field may be
94808
+ * absent, present with a new id, or present but explicitly `null` (the link is being cleared).
94809
+ * These three cases are distinguished via `hasOwnProperty` because they demand different
94810
+ * behavior — falling back to the original durable-store id on an explicit `null` would resolve
94811
+ * `IsProduct2Serialized` from a relationship the record no longer has:
94812
+ *
94813
+ * - present + string -> `resolved` with the reassigned id.
94814
+ * - present + null -> `cleared`.
94815
+ * - absent -> fall back to the original record; `resolved` when it carries a usable
94816
+ * string, otherwise `unresolved`.
94817
+ */
94818
+ function resolveReferenceIdForUpdate(patchFields, originalFields, fieldName) {
94819
+ if (Object.prototype.hasOwnProperty.call(patchFields, fieldName)) {
94820
+ const patchValue = patchFields[fieldName];
94821
+ if (patchValue === null) {
94822
+ return { kind: 'cleared' };
94823
+ }
94824
+ const patchId = getStringDraftField(patchFields, fieldName);
94825
+ if (patchId !== undefined) {
94826
+ return { kind: 'resolved', id: patchId, source: 'patch' };
94827
+ }
94828
+ // Present but neither a string nor null (unexpected shape): treat as an absent link
94829
+ // and fall through to the original record rather than guessing.
94830
+ }
94831
+ const originalId = getStringRecordField(originalFields, fieldName);
94832
+ if (originalId !== undefined) {
94833
+ return { kind: 'resolved', id: originalId, source: 'original' };
94834
+ }
94835
+ return { kind: 'unresolved' };
94836
+ }
94837
+ /**
94838
+ * Resolves the server-derived `IsProduct2Serialized` boolean for a draft-created record by
94839
+ * following the related `ProductItem`:
94840
+ *
94841
+ * 1. If the cached `ProductItem` already carries `IsProduct2Serialized`, use it directly
94842
+ * (it mirrors the linked product's serialization, and avoids a second durable read).
94843
+ * 2. Otherwise fall back to the linked `Product2`:
94844
+ * `ProductItem.Product2Id` -> `Product2.IsSerialized`.
94845
+ *
94846
+ * All lookups go through the given `RecordReader` (the durable store, or a per-action memoizer
94847
+ * that dedups reads). Returns `undefined` when a link in the chain is missing so the caller can
94848
+ * exit without an effect and leave the field synthesized (null). Callers pass their own
94849
+ * `counterName` so instrumentation is attributed to the owning hook.
94850
+ */
94851
+ async function resolveIsProduct2SerializedFromProductItem(recordReader, productItemId, counterName) {
94852
+ const productItemRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(productItemId));
94853
+ if (productItemRecord === undefined) {
94854
+ incrementCounter(counterName, CounterResult.Exited);
94855
+ return undefined;
94856
+ }
94857
+ // Prefer IsProduct2Serialized straight off the ProductItem when it is cached there;
94858
+ // it saves a second durable read. Fall back to the linked Product2 otherwise.
94859
+ const fromProductItem = getIsProduct2SerializedFromRecordFields(productItemRecord.fields);
94860
+ if (fromProductItem !== undefined) {
94861
+ incrementCounter(counterName, CounterResult.Created);
94862
+ return fromProductItem;
94863
+ }
94864
+ const product2Id = getProduct2IdFromRecordFields(productItemRecord.fields);
94865
+ if (product2Id === undefined) {
94866
+ incrementCounter(counterName, CounterResult.Exited);
94867
+ return undefined;
94868
+ }
94869
+ return resolveIsSerializedFromProduct2Id(recordReader, product2Id, counterName);
94870
+ }
94871
+ /**
94872
+ * Resolves the `Product2.IsSerialized` boolean for a given `Product2Id` by reading the linked
94873
+ * `Product2` record through the given `RecordReader`. Used directly by the `ProductItem` hooks
94874
+ * (which carry `Product2Id` themselves) and as the fallback tail of
94875
+ * `resolveIsProduct2SerializedFromProductItem`.
94876
+ *
94877
+ * Returns `undefined` when the `Product2` record is missing so the caller can exit without an
94878
+ * effect and leave the field synthesized (null). Callers pass their own `counterName` so
94879
+ * instrumentation is attributed to the owning hook.
94880
+ */
94881
+ async function resolveIsSerializedFromProduct2Id(recordReader, product2Id, counterName) {
94882
+ const product2Record = await recordReader.getRecord(buildRecordRepKeyFromId$1(product2Id));
94883
+ if (product2Record === undefined) {
94884
+ incrementCounter(counterName, CounterResult.Exited);
94885
+ return undefined;
94886
+ }
94887
+ const isSerialized = getIsSerializedFromRecordFields(product2Record.fields);
94888
+ if (isSerialized === undefined) {
94889
+ incrementCounter(counterName, CounterResult.Error);
94890
+ // Keep the message a stable literal and append the variable parts as key=value
94891
+ // pairs, so the log line stays low-cardinality and greppable in Splunk (the raw
94892
+ // record id is a discrete field rather than baked into the message text).
94893
+ instrumentation$4.error(`SFS - Product2 IsSerialized not found: hook=${counterName}, product2Id=${product2Id}`);
94894
+ return undefined;
94895
+ }
94896
+ incrementCounter(counterName, CounterResult.Created);
94897
+ return isSerialized;
94898
+ }
94899
+ function getProduct2IdFromRecordFields(fields) {
94900
+ const field = fields[ProductItem$2.Fields.PRODUCT2_ID];
94901
+ if (field === undefined || field.value === undefined || typeof field.value !== 'string') {
94902
+ return undefined;
94903
+ }
94904
+ return field.value;
94905
+ }
94906
+ function getIsProduct2SerializedFromRecordFields(fields) {
94907
+ const field = fields[ProductItem$2.Fields.IS_PRODUCT2_SERIALIZED];
94908
+ if (field === undefined || field.value === undefined || typeof field.value !== 'boolean') {
94909
+ return undefined;
94910
+ }
94911
+ return field.value;
94912
+ }
94913
+ function getIsSerializedFromRecordFields(fields) {
94914
+ const field = fields[Product2.Fields.IS_SERIALIZED];
94915
+ if (field === undefined || field.value === undefined || typeof field.value !== 'boolean') {
94916
+ return undefined;
94917
+ }
94918
+ return field.value;
94919
+ }
94920
+
94875
94921
  const ProductConsumed = {
94876
94922
  API_NAME: 'ProductConsumed',
94877
94923
  Fields: {
94878
94924
  PRODUCT_ITEM_ID: 'ProductItemId',
94879
94925
  QUANTITY_CONSUMED: 'QuantityConsumed',
94926
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
94880
94927
  },
94881
94928
  };
94882
- const ProductItem = {
94929
+ const ProductItem$1 = {
94883
94930
  Fields: {
94884
94931
  QUANTITY_ON_HAND: 'QuantityOnHand',
94885
94932
  },
@@ -94890,7 +94937,7 @@ class ProductConsumedCreatedHook {
94890
94937
  this.apiName = ProductConsumed.API_NAME;
94891
94938
  }
94892
94939
  async then(draftActionId, draftActionTimestamp, _draftRecordId, fields) {
94893
- const productItemId = getProductItemId(fields);
94940
+ const productItemId = getStringDraftField(fields, ProductConsumed.Fields.PRODUCT_ITEM_ID);
94894
94941
  const quantityConsumed = getQuantityConsumed(fields);
94895
94942
  if (productItemId === undefined) {
94896
94943
  incrementCounter(ProductConsumedCreatedHook.COUNTER_NAME, CounterResult.Exited);
@@ -94903,7 +94950,7 @@ class ProductConsumedCreatedHook {
94903
94950
  incrementCounter(ProductConsumedCreatedHook.COUNTER_NAME, CounterResult.Created);
94904
94951
  const effect = {
94905
94952
  type: 'record-field-increment',
94906
- fieldName: ProductItem.Fields.QUANTITY_ON_HAND,
94953
+ fieldName: ProductItem$1.Fields.QUANTITY_ON_HAND,
94907
94954
  value: -quantityConsumed,
94908
94955
  key: buildRecordRepKeyFromId$1(productItemId),
94909
94956
  tag: draftActionId,
@@ -94920,16 +94967,17 @@ class ProductConsumedUpdatedHook {
94920
94967
  this.on = 'patch';
94921
94968
  this.apiName = ProductConsumed.API_NAME;
94922
94969
  }
94923
- async then(draftActionId, draftActionTimestamp, draftRecordId, fields) {
94970
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
94924
94971
  const quantityConsumed = getQuantityConsumed(fields);
94925
94972
  if (quantityConsumed === undefined) {
94926
94973
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
94927
94974
  return [];
94928
94975
  }
94929
- const originalRecord = await this.durableRecordStore.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
94976
+ const recordReader = reader ?? this.durableRecordStore;
94977
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
94930
94978
  if (originalRecord === undefined) {
94931
94979
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Error);
94932
- instrumentation$4.error(`SFS - ProductConsumedUpdatedHook - Original record ${draftRecordId} not found`);
94980
+ instrumentation$4.error(`SFS - ProductConsumedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
94933
94981
  return [];
94934
94982
  }
94935
94983
  const originalProductItemId = getProductItemIdFromRecordFields(originalRecord.fields);
@@ -94940,13 +94988,13 @@ class ProductConsumedUpdatedHook {
94940
94988
  }
94941
94989
  if (originalQuantityConsumed === undefined) {
94942
94990
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Error);
94943
- instrumentation$4.error(`SFS - ProductConsumedUpdatedHook - Original record ${draftRecordId} QuantityConsumed not found`);
94991
+ instrumentation$4.error(`SFS - ProductConsumedUpdatedHook original record QuantityConsumed not found: draftRecordId=${draftRecordId}`);
94944
94992
  return [];
94945
94993
  }
94946
94994
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Created);
94947
94995
  const effect = {
94948
94996
  type: 'record-field-increment',
94949
- fieldName: ProductItem.Fields.QUANTITY_ON_HAND,
94997
+ fieldName: ProductItem$1.Fields.QUANTITY_ON_HAND,
94950
94998
  value: originalQuantityConsumed - quantityConsumed,
94951
94999
  key: buildRecordRepKeyFromId$1(originalProductItemId),
94952
95000
  tag: draftActionId,
@@ -94963,11 +95011,12 @@ class ProductConsumedDeletedHook {
94963
95011
  this.on = 'delete';
94964
95012
  this.apiName = ProductConsumed.API_NAME;
94965
95013
  }
94966
- async then(draftActionId, draftActionTimestamp, draftRecordId) {
94967
- const originalRecord = await this.durableRecordStore.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
95014
+ async then(draftActionId, draftActionTimestamp, draftRecordId, _fields, reader) {
95015
+ const recordReader = reader ?? this.durableRecordStore;
95016
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
94968
95017
  if (originalRecord === undefined) {
94969
95018
  incrementCounter(ProductConsumedDeletedHook.COUNTER_NAME, CounterResult.Error);
94970
- instrumentation$4.error(`SFS - ProductConsumedDeletedHook - Original record ${draftRecordId} not found`);
95019
+ instrumentation$4.error(`SFS - ProductConsumedDeletedHook original record not found: draftRecordId=${draftRecordId}`);
94971
95020
  return [];
94972
95021
  }
94973
95022
  const productItemId = getProductItemIdFromRecordFields(originalRecord.fields);
@@ -94978,13 +95027,13 @@ class ProductConsumedDeletedHook {
94978
95027
  }
94979
95028
  if (quantityConsumed === undefined) {
94980
95029
  incrementCounter(ProductConsumedDeletedHook.COUNTER_NAME, CounterResult.Error);
94981
- instrumentation$4.error(`SFS - ProductConsumedDeletedHook - Original record ${draftRecordId} QuantityConsumed not found`);
95030
+ instrumentation$4.error(`SFS - ProductConsumedDeletedHook original record QuantityConsumed not found: draftRecordId=${draftRecordId}`);
94982
95031
  return [];
94983
95032
  }
94984
95033
  incrementCounter(ProductConsumedDeletedHook.COUNTER_NAME, CounterResult.Created);
94985
95034
  const effect = {
94986
95035
  type: 'record-field-increment',
94987
- fieldName: ProductItem.Fields.QUANTITY_ON_HAND,
95036
+ fieldName: ProductItem$1.Fields.QUANTITY_ON_HAND,
94988
95037
  value: quantityConsumed,
94989
95038
  key: buildRecordRepKeyFromId$1(productItemId),
94990
95039
  tag: draftActionId,
@@ -94995,13 +95044,115 @@ class ProductConsumedDeletedHook {
94995
95044
  }
94996
95045
  }
94997
95046
  ProductConsumedDeletedHook.COUNTER_NAME = 'ProductConsumedDeletedHook';
94998
- function getProductItemId(fields) {
94999
- const field = fields[ProductConsumed.Fields.PRODUCT_ITEM_ID];
95000
- if (field === undefined || typeof field !== 'string') {
95001
- return undefined;
95047
+ /**
95048
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-created
95049
+ * `ProductConsumed` record.
95050
+ *
95051
+ * `Product2Id` is not createable on `ProductConsumed`, so the draft only carries the
95052
+ * `ProductItemId`. The value is resolved through the related `ProductItem` record (see
95053
+ * `resolveIsProduct2SerializedFromProductItem`): the ProductItem's own
95054
+ * `IsProduct2Serialized` when cached, otherwise `ProductItem.Product2Id` ->
95055
+ * `Product2.IsSerialized`. When the chain cannot be resolved the hook exits without an
95056
+ * effect and the field is left as synthesized (null).
95057
+ */
95058
+ class ProductConsumedIsSerializedCreatedHook {
95059
+ constructor(durableRecordStore) {
95060
+ this.durableRecordStore = durableRecordStore;
95061
+ this.on = 'post';
95062
+ this.apiName = ProductConsumed.API_NAME;
95063
+ }
95064
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95065
+ const productItemId = getStringDraftField(fields, ProductConsumed.Fields.PRODUCT_ITEM_ID);
95066
+ if (productItemId === undefined) {
95067
+ incrementCounter(ProductConsumedIsSerializedCreatedHook.COUNTER_NAME, CounterResult.Exited);
95068
+ return [];
95069
+ }
95070
+ const isSerialized = await resolveIsProduct2SerializedFromProductItem(reader ?? this.durableRecordStore, productItemId, ProductConsumedIsSerializedCreatedHook.COUNTER_NAME);
95071
+ if (isSerialized === undefined) {
95072
+ return [];
95073
+ }
95074
+ const effect = {
95075
+ type: 'record-field-set',
95076
+ fieldName: ProductConsumed.Fields.IS_PRODUCT2_SERIALIZED,
95077
+ value: isSerialized,
95078
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95079
+ tag: draftActionId,
95080
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95081
+ timestamp: draftActionTimestamp,
95082
+ };
95083
+ return [effect];
95002
95084
  }
95003
- return field;
95004
95085
  }
95086
+ ProductConsumedIsSerializedCreatedHook.COUNTER_NAME = 'ProductConsumedIsSerializedCreatedHook';
95087
+ /**
95088
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-edited
95089
+ * `ProductConsumed` record.
95090
+ *
95091
+ * A `patch` body only carries changed fields, so the related `ProductItemId` may be absent.
95092
+ * The id is resolved by preferring the patch body (the link is being reassigned) and falling
95093
+ * back to the original record in the durable store, then the value is resolved through that
95094
+ * `ProductItem` exactly as on create (see `resolveIsProduct2SerializedFromProductItem`).
95095
+ *
95096
+ * When the patch explicitly clears `ProductItemId` (sets it to `null`) the record no longer
95097
+ * derives from any `ProductItem`, so `IsProduct2Serialized` is cleared (set to `null`) rather
95098
+ * than resolved from the now-stale original link. Likewise, when the patch reassigns
95099
+ * `ProductItemId` to a new id whose chain isn't cached, the stored value reflects the old link
95100
+ * and is stale, so it is cleared. When the link is unchanged and its chain cannot be resolved,
95101
+ * the stored value is still correct so the hook exits without an effect and leaves it unchanged.
95102
+ */
95103
+ class ProductConsumedIsSerializedUpdatedHook {
95104
+ constructor(durableRecordStore) {
95105
+ this.durableRecordStore = durableRecordStore;
95106
+ this.on = 'patch';
95107
+ this.apiName = ProductConsumed.API_NAME;
95108
+ }
95109
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95110
+ const recordReader = reader ?? this.durableRecordStore;
95111
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
95112
+ if (originalRecord === undefined) {
95113
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Error);
95114
+ instrumentation$4.error(`SFS - ProductConsumedIsSerializedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
95115
+ return [];
95116
+ }
95117
+ const reference = resolveReferenceIdForUpdate(fields, originalRecord.fields, ProductConsumed.Fields.PRODUCT_ITEM_ID);
95118
+ if (reference.kind === 'unresolved') {
95119
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
95120
+ return [];
95121
+ }
95122
+ let isSerialized;
95123
+ if (reference.kind === 'cleared') {
95124
+ // The link was explicitly cleared: the record derives from no ProductItem, so clear
95125
+ // IsProduct2Serialized rather than resolving it from the stale original link.
95126
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95127
+ isSerialized = null;
95128
+ }
95129
+ else {
95130
+ isSerialized = await resolveIsProduct2SerializedFromProductItem(recordReader, reference.id, ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME);
95131
+ if (isSerialized === undefined && reference.source === 'patch') {
95132
+ // The link was reassigned to a new ProductItem whose chain isn't cached yet, so
95133
+ // the stored IsProduct2Serialized still reflects the OLD link and is now stale.
95134
+ // Clear it rather than leaving the stale value alongside the new relationship;
95135
+ // the server recomputes the real value on sync.
95136
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95137
+ isSerialized = null;
95138
+ }
95139
+ }
95140
+ if (isSerialized === undefined) {
95141
+ return [];
95142
+ }
95143
+ const effect = {
95144
+ type: 'record-field-set',
95145
+ fieldName: ProductConsumed.Fields.IS_PRODUCT2_SERIALIZED,
95146
+ value: isSerialized,
95147
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95148
+ tag: draftActionId,
95149
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95150
+ timestamp: draftActionTimestamp,
95151
+ };
95152
+ return [effect];
95153
+ }
95154
+ }
95155
+ ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME = 'ProductConsumedIsSerializedUpdatedHook';
95005
95156
  function getProductItemIdFromRecordFields(fields) {
95006
95157
  const field = fields[ProductConsumed.Fields.PRODUCT_ITEM_ID];
95007
95158
  if (field === undefined || field.value === undefined || typeof field.value !== 'string') {
@@ -95023,29 +95174,265 @@ function getQuantityConsumedFromRecordFields(fields) {
95023
95174
  }
95024
95175
  return field.value;
95025
95176
  }
95026
- var CounterResult;
95027
- (function (CounterResult) {
95028
- // A side effect was created.
95029
- CounterResult["Created"] = "Created";
95030
- // The hook exited early.
95031
- CounterResult["Exited"] = "Exited";
95032
- // An unexpected error occurred.
95033
- CounterResult["Error"] = "Error";
95034
- })(CounterResult || (CounterResult = {}));
95035
- function incrementCounter(name, result) {
95036
- instrumentation$4.incrementCounter('SFSSideEffect', 1, result === CounterResult.Error, {
95037
- Name: name,
95038
- Result: result,
95039
- });
95177
+
95178
+ const ProductTransfer = {
95179
+ API_NAME: 'ProductTransfer',
95180
+ Fields: {
95181
+ SOURCE_PRODUCT_ITEM_ID: 'SourceProductItemId',
95182
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
95183
+ },
95184
+ };
95185
+ /**
95186
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-created
95187
+ * `ProductTransfer` record.
95188
+ *
95189
+ * The value is resolved through the related `SourceProductItem` record (a `ProductItem`),
95190
+ * mirroring the `ProductConsumed` hook: the ProductItem's own `IsProduct2Serialized` when
95191
+ * cached, otherwise `ProductItem.Product2Id` -> `Product2.IsSerialized` (see
95192
+ * `resolveIsProduct2SerializedFromProductItem`). When the chain cannot be resolved the hook
95193
+ * exits without an effect and the field is left as synthesized (null).
95194
+ */
95195
+ class ProductTransferIsSerializedCreatedHook {
95196
+ constructor(durableRecordStore) {
95197
+ this.durableRecordStore = durableRecordStore;
95198
+ this.on = 'post';
95199
+ this.apiName = ProductTransfer.API_NAME;
95200
+ }
95201
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95202
+ const sourceProductItemId = getStringDraftField(fields, ProductTransfer.Fields.SOURCE_PRODUCT_ITEM_ID);
95203
+ if (sourceProductItemId === undefined) {
95204
+ incrementCounter(ProductTransferIsSerializedCreatedHook.COUNTER_NAME, CounterResult.Exited);
95205
+ return [];
95206
+ }
95207
+ const isSerialized = await resolveIsProduct2SerializedFromProductItem(reader ?? this.durableRecordStore, sourceProductItemId, ProductTransferIsSerializedCreatedHook.COUNTER_NAME);
95208
+ if (isSerialized === undefined) {
95209
+ return [];
95210
+ }
95211
+ const effect = {
95212
+ type: 'record-field-set',
95213
+ fieldName: ProductTransfer.Fields.IS_PRODUCT2_SERIALIZED,
95214
+ value: isSerialized,
95215
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95216
+ tag: draftActionId,
95217
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95218
+ timestamp: draftActionTimestamp,
95219
+ };
95220
+ return [effect];
95221
+ }
95222
+ }
95223
+ ProductTransferIsSerializedCreatedHook.COUNTER_NAME = 'ProductTransferIsSerializedCreatedHook';
95224
+ /**
95225
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-edited
95226
+ * `ProductTransfer` record.
95227
+ *
95228
+ * ProductTransfer can only be edited (not created) on mobile, so this is the hook that fires
95229
+ * in practice. A `patch` body only carries changed fields, so the related `SourceProductItemId`
95230
+ * may be absent; the id is resolved by preferring the patch body (the link is being reassigned)
95231
+ * and falling back to the original record in the durable store, then the value is resolved
95232
+ * through that `ProductItem` exactly as on create (see
95233
+ * `resolveIsProduct2SerializedFromProductItem`).
95234
+ *
95235
+ * When the patch explicitly clears `SourceProductItemId` (sets it to `null`) the record no
95236
+ * longer derives from any `ProductItem`, so `IsProduct2Serialized` is cleared (set to `null`)
95237
+ * rather than resolved from the now-stale original link. Likewise, when the patch reassigns
95238
+ * `SourceProductItemId` to a new id whose chain isn't cached, the stored value reflects the old
95239
+ * link and is stale, so it is cleared. When the link is unchanged and its chain cannot be
95240
+ * resolved, the stored value is still correct so the hook exits without an effect and leaves it
95241
+ * unchanged.
95242
+ */
95243
+ class ProductTransferIsSerializedUpdatedHook {
95244
+ constructor(durableRecordStore) {
95245
+ this.durableRecordStore = durableRecordStore;
95246
+ this.on = 'patch';
95247
+ this.apiName = ProductTransfer.API_NAME;
95248
+ }
95249
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95250
+ const recordReader = reader ?? this.durableRecordStore;
95251
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
95252
+ if (originalRecord === undefined) {
95253
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Error);
95254
+ instrumentation$4.error(`SFS - ProductTransferIsSerializedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
95255
+ return [];
95256
+ }
95257
+ const reference = resolveReferenceIdForUpdate(fields, originalRecord.fields, ProductTransfer.Fields.SOURCE_PRODUCT_ITEM_ID);
95258
+ if (reference.kind === 'unresolved') {
95259
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
95260
+ return [];
95261
+ }
95262
+ let isSerialized;
95263
+ if (reference.kind === 'cleared') {
95264
+ // The link was explicitly cleared: the record derives from no ProductItem, so clear
95265
+ // IsProduct2Serialized rather than resolving it from the stale original link.
95266
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95267
+ isSerialized = null;
95268
+ }
95269
+ else {
95270
+ isSerialized = await resolveIsProduct2SerializedFromProductItem(recordReader, reference.id, ProductTransferIsSerializedUpdatedHook.COUNTER_NAME);
95271
+ if (isSerialized === undefined && reference.source === 'patch') {
95272
+ // The link was reassigned to a new ProductItem whose chain isn't cached yet, so
95273
+ // the stored IsProduct2Serialized still reflects the OLD link and is now stale.
95274
+ // Clear it rather than leaving the stale value alongside the new relationship;
95275
+ // the server recomputes the real value on sync.
95276
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95277
+ isSerialized = null;
95278
+ }
95279
+ }
95280
+ if (isSerialized === undefined) {
95281
+ return [];
95282
+ }
95283
+ const effect = {
95284
+ type: 'record-field-set',
95285
+ fieldName: ProductTransfer.Fields.IS_PRODUCT2_SERIALIZED,
95286
+ value: isSerialized,
95287
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95288
+ tag: draftActionId,
95289
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95290
+ timestamp: draftActionTimestamp,
95291
+ };
95292
+ return [effect];
95293
+ }
95040
95294
  }
95295
+ ProductTransferIsSerializedUpdatedHook.COUNTER_NAME = 'ProductTransferIsSerializedUpdatedHook';
95296
+
95297
+ const ProductItem = {
95298
+ API_NAME: 'ProductItem',
95299
+ Fields: {
95300
+ PRODUCT2_ID: 'Product2Id',
95301
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
95302
+ },
95303
+ };
95304
+ /**
95305
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-created
95306
+ * `ProductItem` record.
95307
+ *
95308
+ * Unlike `ProductConsumed`/`ProductTransfer` — which reach the product through a related
95309
+ * `ProductItem` — a `ProductItem` carries `Product2Id` directly, so the value is resolved as
95310
+ * `Product2Id` -> `Product2.IsSerialized` (see `resolveIsSerializedFromProduct2Id`). When the
95311
+ * chain cannot be resolved the hook exits without an effect and the field is left as
95312
+ * synthesized (null).
95313
+ */
95314
+ class ProductItemIsSerializedCreatedHook {
95315
+ constructor(durableRecordStore) {
95316
+ this.durableRecordStore = durableRecordStore;
95317
+ this.on = 'post';
95318
+ this.apiName = ProductItem.API_NAME;
95319
+ }
95320
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95321
+ const product2Id = getStringDraftField(fields, ProductItem.Fields.PRODUCT2_ID);
95322
+ if (product2Id === undefined) {
95323
+ incrementCounter(ProductItemIsSerializedCreatedHook.COUNTER_NAME, CounterResult.Exited);
95324
+ return [];
95325
+ }
95326
+ const isSerialized = await resolveIsSerializedFromProduct2Id(reader ?? this.durableRecordStore, product2Id, ProductItemIsSerializedCreatedHook.COUNTER_NAME);
95327
+ if (isSerialized === undefined) {
95328
+ return [];
95329
+ }
95330
+ const effect = {
95331
+ type: 'record-field-set',
95332
+ fieldName: ProductItem.Fields.IS_PRODUCT2_SERIALIZED,
95333
+ value: isSerialized,
95334
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95335
+ tag: draftActionId,
95336
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95337
+ timestamp: draftActionTimestamp,
95338
+ };
95339
+ return [effect];
95340
+ }
95341
+ }
95342
+ ProductItemIsSerializedCreatedHook.COUNTER_NAME = 'ProductItemIsSerializedCreatedHook';
95343
+ /**
95344
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-edited
95345
+ * `ProductItem` record.
95346
+ *
95347
+ * A `patch` body only carries changed fields, so `Product2Id` may be absent; it is resolved by
95348
+ * preferring the patch body (the product is being reassigned) and falling back to the original
95349
+ * record in the durable store, then resolved as `Product2Id` -> `Product2.IsSerialized` (see
95350
+ * `resolveIsSerializedFromProduct2Id`).
95351
+ *
95352
+ * When the patch explicitly clears `Product2Id` (sets it to `null`) the record no longer
95353
+ * derives from any `Product2`, so `IsProduct2Serialized` is cleared (set to `null`) rather than
95354
+ * resolved from the now-stale original link. Likewise, when the patch reassigns `Product2Id` to
95355
+ * a new id whose `Product2` isn't cached, the stored value reflects the old link and is stale,
95356
+ * so it is cleared. When the link is unchanged and its `Product2` cannot be resolved, the stored
95357
+ * value is still correct so the hook exits without an effect and leaves it unchanged.
95358
+ */
95359
+ class ProductItemIsSerializedUpdatedHook {
95360
+ constructor(durableRecordStore) {
95361
+ this.durableRecordStore = durableRecordStore;
95362
+ this.on = 'patch';
95363
+ this.apiName = ProductItem.API_NAME;
95364
+ }
95365
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
95366
+ const recordReader = reader ?? this.durableRecordStore;
95367
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId$1(draftRecordId));
95368
+ if (originalRecord === undefined) {
95369
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Error);
95370
+ instrumentation$4.error(`SFS - ProductItemIsSerializedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
95371
+ return [];
95372
+ }
95373
+ const reference = resolveReferenceIdForUpdate(fields, originalRecord.fields, ProductItem.Fields.PRODUCT2_ID);
95374
+ if (reference.kind === 'unresolved') {
95375
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
95376
+ return [];
95377
+ }
95378
+ let isSerialized;
95379
+ if (reference.kind === 'cleared') {
95380
+ // The link was explicitly cleared: the record derives from no Product2, so clear
95381
+ // IsProduct2Serialized rather than resolving it from the stale original link.
95382
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95383
+ isSerialized = null;
95384
+ }
95385
+ else {
95386
+ isSerialized = await resolveIsSerializedFromProduct2Id(recordReader, reference.id, ProductItemIsSerializedUpdatedHook.COUNTER_NAME);
95387
+ if (isSerialized === undefined && reference.source === 'patch') {
95388
+ // The link was reassigned to a new Product2 that isn't cached yet, so the stored
95389
+ // IsProduct2Serialized still reflects the OLD link and is now stale. Clear it
95390
+ // rather than leaving the stale value alongside the new relationship; the server
95391
+ // recomputes the real value on sync.
95392
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
95393
+ isSerialized = null;
95394
+ }
95395
+ }
95396
+ if (isSerialized === undefined) {
95397
+ return [];
95398
+ }
95399
+ const effect = {
95400
+ type: 'record-field-set',
95401
+ fieldName: ProductItem.Fields.IS_PRODUCT2_SERIALIZED,
95402
+ value: isSerialized,
95403
+ key: buildRecordRepKeyFromId$1(draftRecordId),
95404
+ tag: draftActionId,
95405
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
95406
+ timestamp: draftActionTimestamp,
95407
+ };
95408
+ return [effect];
95409
+ }
95410
+ }
95411
+ ProductItemIsSerializedUpdatedHook.COUNTER_NAME = 'ProductItemIsSerializedUpdatedHook';
95041
95412
 
95042
95413
  function createSfsSideEffectHooks(durableRecordStore) {
95043
95414
  if (productConsumedSideEffects.isOpen({ fallback: false })) {
95044
- return [
95415
+ const hooks = [
95045
95416
  new ProductConsumedCreatedHook(),
95046
95417
  new ProductConsumedUpdatedHook(durableRecordStore),
95047
95418
  new ProductConsumedDeletedHook(durableRecordStore),
95048
95419
  ];
95420
+ // The IsProduct2Serialized side effects (ProductConsumed, ProductTransfer and
95421
+ // ProductItem) ship on by default and can be disabled independently of the van stock
95422
+ // hooks above via a shared kill switch: the gate is closed (fallback false) by default,
95423
+ // which keeps the effects enabled, and opening the gate turns them off.
95424
+ //
95425
+ // Each object gets both a create (post) and edit (patch) hook so the field is
95426
+ // synthesized on either draft action, keeping the behavior uniform across the objects.
95427
+ if (!isProduct2SerializedSideEffectKillSwitch.isOpen({ fallback: false })) {
95428
+ hooks.push(new ProductConsumedIsSerializedCreatedHook(durableRecordStore));
95429
+ hooks.push(new ProductConsumedIsSerializedUpdatedHook(durableRecordStore));
95430
+ hooks.push(new ProductTransferIsSerializedCreatedHook(durableRecordStore));
95431
+ hooks.push(new ProductTransferIsSerializedUpdatedHook(durableRecordStore));
95432
+ hooks.push(new ProductItemIsSerializedCreatedHook(durableRecordStore));
95433
+ hooks.push(new ProductItemIsSerializedUpdatedHook(durableRecordStore));
95434
+ }
95435
+ return hooks;
95049
95436
  }
95050
95437
  return [];
95051
95438
  }
@@ -97413,7 +97800,7 @@ function buildServiceDescriptor$b(luvio) {
97413
97800
  },
97414
97801
  };
97415
97802
  }
97416
- // version: 1.460.0-f7dc7797dd
97803
+ // version: 1.462.0-50d9a13ba4
97417
97804
 
97418
97805
  /**
97419
97806
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -97439,7 +97826,7 @@ function buildServiceDescriptor$a(notifyRecordUpdateAvailable, getNormalizedLuvi
97439
97826
  },
97440
97827
  };
97441
97828
  }
97442
- // version: 1.460.0-f7dc7797dd
97829
+ // version: 1.462.0-50d9a13ba4
97443
97830
 
97444
97831
  function findExecutableOperation$1(input) {
97445
97832
  const operations = input.query.definitions.filter(
@@ -99828,9 +100215,6 @@ class NimbusSqliteOneStoreCache {
99828
100215
 
99829
100216
  const PURGE_CACHE_DAYS = 30;
99830
100217
  function initializeOneStore(sqliteStore, luvio) {
99831
- if (!useOneStore.isOpen({ fallback: true })) {
99832
- return;
99833
- }
99834
100218
  // Initialize performance.now() fallback if not available
99835
100219
  if (typeof globalThis.performance === 'undefined') {
99836
100220
  globalThis.performance = { now: () => Date.now() };
@@ -100017,12 +100401,6 @@ function getRuntime({ primeUser = false } = {}) {
100017
100401
  lazyInternalLuvio = internalLuvio;
100018
100402
  lazyObjectInfoService = new ObjectInfoService(getObjectInfo, getObjectInfos, getObjectInfoDirectory, lazyDurableStore);
100019
100403
  const baseEnv = new Environment(store, lazyNetworkAdapter);
100020
- const shouldFilterFieldsOnRevive = (key) => {
100021
- if (reviveOnlyRequestedFields.isOpen({ fallback: true })) {
100022
- return isStoreKeyRecordId(key);
100023
- }
100024
- return false;
100025
- };
100026
100404
  const gqlEnv = makeEnvironmentGraphqlAware(baseEnv);
100027
100405
  const durableEnv = makeDurable(gqlEnv, {
100028
100406
  durableStore: lazyDurableStore,
@@ -100030,7 +100408,12 @@ function getRuntime({ primeUser = false } = {}) {
100030
100408
  // disable luvio deep freeze in headless environments
100031
100409
  disableDeepFreeze: typeof window === 'undefined',
100032
100410
  shouldFlush,
100033
- shouldFilterFieldsOnRevive,
100411
+ // Disabling field filtering on revive for now (previously gated behind
100412
+ // the lmr.reviveOnlyRequestedFields gate). The code path is retained
100413
+ // below should we need to re-enable this behavior in the future:
100414
+ shouldFilterFieldsOnRevive: (_key) => {
100415
+ return false;
100416
+ },
100034
100417
  });
100035
100418
  // draft queue
100036
100419
  lazyDraftQueue = buildLdsDraftQueue(lazyDurableStore);
@@ -100148,7 +100531,7 @@ register$1({
100148
100531
  id: '@salesforce/lds-network-adapter',
100149
100532
  instrument: instrument$2,
100150
100533
  });
100151
- // version: 1.460.0-f7dc7797dd
100534
+ // version: 1.462.0-50d9a13ba4
100152
100535
 
100153
100536
  const { create: create$2, keys: keys$2 } = Object;
100154
100537
  const { stringify, parse } = JSON;
@@ -107700,7 +108083,7 @@ function registerCallback(cb) {
107700
108083
  cb(graphql_v1_import, graphql_imperative$1, graphql_imperative_legacy_v1_import, graphql_state_manager, useOneStoreGraphQL);
107701
108084
  }
107702
108085
  }
107703
- // version: 1.460.0-3e24f1bbff
108086
+ // version: 1.462.0-2919385b97
107704
108087
 
107705
108088
  function createFragmentMap(documentNode) {
107706
108089
  const fragments = {};
@@ -136943,7 +137326,7 @@ register$1({
136943
137326
  configuration: { ...configurationForGraphQLAdapters$1 },
136944
137327
  instrument: instrument$1,
136945
137328
  });
136946
- // version: 1.460.0-3e24f1bbff
137329
+ // version: 1.462.0-2919385b97
136947
137330
 
136948
137331
  // On core the unstable adapters are re-exported with different names,
136949
137332
  // we want to match them here.
@@ -137095,7 +137478,7 @@ withDefaultLuvio((luvio) => {
137095
137478
  unstable_graphQL_imperative = createImperativeAdapter(luvio, createInstrumentedAdapter(ldsAdapter, adapterMetadata), adapterMetadata);
137096
137479
  graphQLImperative = ldsAdapter;
137097
137480
  });
137098
- // version: 1.460.0-3e24f1bbff
137481
+ // version: 1.462.0-2919385b97
137099
137482
 
137100
137483
  var gqlApi = /*#__PURE__*/Object.freeze({
137101
137484
  __proto__: null,
@@ -137894,7 +138277,7 @@ const callbacks$1 = [];
137894
138277
  function register(r) {
137895
138278
  callbacks$1.forEach((callback) => callback(r));
137896
138279
  }
137897
- // version: 1.460.0-f7dc7797dd
138280
+ // version: 1.462.0-50d9a13ba4
137898
138281
 
137899
138282
  /**
137900
138283
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -139183,4 +139566,4 @@ const { luvio } = getRuntime();
139183
139566
  setDefaultLuvio({ luvio });
139184
139567
 
139185
139568
  export { createPrimingSession, draftManager, draftQueue, evictCacheRecordsByIds, evictExpiredCacheEntries, executeAdapter, executeMutatingAdapter, getImperativeAdapterNames, importLuvioAdapterModule, importOneStoreAdapterModule, invokeAdapter, invokeAdapterWithDraftToMerge, invokeAdapterWithDraftToReplace, invokeAdapterWithMetadata, nimbusDraftQueue, registerReportObserver, setMetadataTTL, setUiApiRecordTTL, stopEviction, subscribeToAdapter };
139186
- // version: 1.460.0-f7dc7797dd
139569
+ // version: 1.462.0-50d9a13ba4