@salesforce/lds-runtime-mobile 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.
package/sfdc/main.js CHANGED
@@ -43,17 +43,14 @@ import shortTimeFormat from '@salesforce/i18n/dateTime.shortTimeFormat';
43
43
  import shortDateTimeFormat from '@salesforce/i18n/dateTime.shortDateTimeFormat';
44
44
  import { getDateTimeFormat, getDateTimeISO8601Parser, getNumberFormat } from 'lightning/i18nService';
45
45
  import formattingOptions from 'lightning/i18nCldrOptions';
46
- import ldsPrimingGraphqlBatch from '@salesforce/gate/lds.primingGraphqlBatch';
47
- import lmrPrimingUseSoql from '@salesforce/gate/lmr.primingUseSoql';
48
46
  import aggressiveTrimCountKillSwitchGate from '@salesforce/gate/lmr.disableAggresiveTrimCountChange';
49
47
  import graphqlL2AdapterGate from '@salesforce/gate/lmr.graphqlL2Adapter';
50
- import useOneStore from '@salesforce/gate/lmr.useOneStore';
51
48
  import { setServices } from 'force/luvioServiceProvisioner1';
52
49
  import 'force/luvioTypeNormalization1';
53
50
  import { Kind as Kind$2, visit as visit$2, print as print$1, resolveAndValidateGraphQLConfig, toGraphQLErrorResponse } from 'force/luvioOnestoreGraphqlParser';
54
51
  import draftQueueSideEffectMapGate from '@salesforce/gate/lmr.draft-queue-sideeffect-map';
55
52
  import productConsumedSideEffects from '@salesforce/gate/com.salesforce.fieldservice.vanStockLDSBypass256';
56
- import reviveOnlyRequestedFields from '@salesforce/gate/lmr.reviveOnlyRequestedFields';
53
+ import isProduct2SerializedSideEffectKillSwitch from '@salesforce/gate/com.salesforce.fieldservice.productConsumedSideEffectsKillSwitch';
57
54
 
58
55
  /**
59
56
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -43595,6 +43592,35 @@ async function getAdapterData(adapter, config) {
43595
43592
  return snapshot.data;
43596
43593
  }
43597
43594
 
43595
+ /**
43596
+ * A read-through cache over `DurableRecordStore.getRecord` scoped to a single draft-action
43597
+ * dispatch. Multiple side-effect hooks fire for one action (e.g. the van-stock and
43598
+ * IsProduct2Serialized ProductConsumed hooks) and are awaited one at a time, so they otherwise
43599
+ * each re-read the same draft record — and the IsProduct2Serialized chain re-reads the same
43600
+ * related ProductItem / Product2 — from the durable store. Sharing one reader across the
43601
+ * action's hooks collapses those repeated reads to a single durable call per key.
43602
+ *
43603
+ * The cache stores the in-flight promise (not the resolved value) so concurrent reads of the
43604
+ * same key share one durable call, and it caches `undefined` results so a miss is not re-fetched.
43605
+ * A reader must not outlive one action dispatch: durable state changes between actions, so a
43606
+ * longer-lived cache would return stale records.
43607
+ */
43608
+ class MemoizingRecordReader {
43609
+ constructor(durableRecordStore) {
43610
+ this.durableRecordStore = durableRecordStore;
43611
+ this.cache = new Map();
43612
+ }
43613
+ getRecord(key) {
43614
+ const cached = this.cache.get(key);
43615
+ if (cached !== undefined) {
43616
+ return cached;
43617
+ }
43618
+ const promise = this.durableRecordStore.getRecord(key);
43619
+ this.cache.set(key, promise);
43620
+ return promise;
43621
+ }
43622
+ }
43623
+
43598
43624
  const LDS_ACTION_HANDLER_ID = 'LDS_ACTION_HANDLER';
43599
43625
  const LDS_ACTION_METADATA_API_NAME = 'LDS_ACTION_METADATA_API_NAME';
43600
43626
  class UiApiActionHandler extends AbstractResourceRequestActionHandler {
@@ -43715,9 +43741,9 @@ class UiApiActionHandler extends AbstractResourceRequestActionHandler {
43715
43741
  return super.getRedirectMappings(action);
43716
43742
  }
43717
43743
  async handleActionCompleted(completedAction, queueOperations) {
43718
- if (completedAction.response.status === 204 && completedAction.data.method === 'post') {
43719
- // if we get a 204 it means the record creation was successful but we don't have the record to ingest
43720
- // remove the synthesized draft and do not try to ingest the response
43744
+ const { method } = completedAction.data;
43745
+ if (completedAction.response.status === 204 && (method === 'post' || method === 'patch')) {
43746
+ await this.recordService.removeSideEffectsForAction(completedAction);
43721
43747
  await this.evictKey(completedAction.tag);
43722
43748
  return;
43723
43749
  }
@@ -43798,8 +43824,15 @@ class UiApiActionHandler extends AbstractResourceRequestActionHandler {
43798
43824
  break;
43799
43825
  }
43800
43826
  }
43827
+ // One reader shared across every hook for this action so repeated reads of the same
43828
+ // record (the draft record, and any related records the hooks resolve) collapse to a
43829
+ // single durable call. Scoped to this dispatch only — durable state changes between
43830
+ // actions, so it must not be reused across them. Built only when hooks will run.
43831
+ const recordReader = hookTriggers.length > 0
43832
+ ? new MemoizingRecordReader(this.durableRecordStore)
43833
+ : undefined;
43801
43834
  for (const trigger of hookTriggers) {
43802
- const hookEffects = await trigger.then(action.id, action.timestamp, action.targetId, action.data.body?.fields ?? {});
43835
+ const hookEffects = await trigger.then(action.id, action.timestamp, action.targetId, action.data.body?.fields ?? {}, recordReader);
43803
43836
  for (const hookEffect of hookEffects) {
43804
43837
  effects.push(hookEffect);
43805
43838
  }
@@ -55004,211 +55037,6 @@ class NetworkRecordLoader {
55004
55037
  }
55005
55038
  }
55006
55039
 
55007
- const apiPath$1 = basePath + '/graphql';
55008
- const requiredPrefix = `required_`;
55009
- const requiredFieldMap = {
55010
- ApiName: 'ApiName',
55011
- Id: 'Id',
55012
- LastModifiedById: 'LastModifiedById { value }',
55013
- LastModifiedDate: 'LastModifiedDate { value }',
55014
- RecordTypeId: 'RecordTypeId(fallback: true) { value }',
55015
- SystemModstamp: 'SystemModstamp { value }',
55016
- WeakEtag: 'WeakEtag',
55017
- };
55018
- class RecordLoaderGraphQL extends NetworkRecordLoader {
55019
- makeRequest(batch, observabilityContext) {
55020
- const query = this.generateGraphQLQuery(batch.type, batch.fields);
55021
- return {
55022
- request: {
55023
- method: 'POST',
55024
- path: apiPath$1,
55025
- body: JSON.stringify({
55026
- query,
55027
- variables: {
55028
- ids: batch.ids,
55029
- first: batch.ids.length,
55030
- },
55031
- }),
55032
- headers: {},
55033
- queryParams: {},
55034
- priority: 'background',
55035
- observabilityContext: observabilityContext !== undefined ? observabilityContext : {},
55036
- },
55037
- context: undefined,
55038
- };
55039
- }
55040
- generateFetchResults(repResult, batchInput, _context) {
55041
- const { data, errors } = repResult;
55042
- if (errors !== undefined && errors.length > 0) {
55043
- // right now if there are any errors in the batch we throw out the entire batch
55044
- // for now this is ok all errors will originate on the same node so there shouldn't be a mix of errors and data
55045
- return [
55046
- {
55047
- missingIds: batchInput.ids,
55048
- ok: false,
55049
- error: 'request-error',
55050
- messages: errors.map((x) => x.message),
55051
- },
55052
- ];
55053
- }
55054
- // this shouldn't really happen if there's no errors
55055
- if (!data || !data.uiapi) {
55056
- return [
55057
- {
55058
- ok: false,
55059
- error: 'unknown',
55060
- messages: ['unexpected response retrieved from graphql endpoint'],
55061
- missingIds: batchInput.ids,
55062
- },
55063
- ];
55064
- }
55065
- const objectInfo = batchInput.objectInfo;
55066
- const seenRecords = new Set(batchInput.ids);
55067
- const records = data.uiapi.query[batchInput.type].edges.map((edge) => {
55068
- const record = this.generateDurableRecordRepresentation(edge.node, objectInfo);
55069
- seenRecords.delete(record.id);
55070
- return record;
55071
- });
55072
- return [
55073
- {
55074
- ok: true,
55075
- records,
55076
- missingIds: Array.from(seenRecords),
55077
- },
55078
- ];
55079
- }
55080
- generateGraphQLQuery(type, fields) {
55081
- const fieldList = Object.keys(requiredFieldMap)
55082
- .map((field) => {
55083
- return `${requiredPrefix}${field}: ${requiredFieldMap[field]}`;
55084
- })
55085
- .join('\n');
55086
- return /* GraphQL */ `
55087
- fragment requiredRecordFields on Record {
55088
- ${fieldList}
55089
- }
55090
- query prime${type}($ids: [ID], $first: Int){
55091
- uiapi {
55092
- query {
55093
- ${type} (where: {Id: {in: $ids}} first: $first) {
55094
- edges {
55095
- node {
55096
- ...requiredRecordFields
55097
- ${fields
55098
- .filter((x) => x !== 'Id' && x !== 'RecordTypeId')
55099
- .map((field) => `${field} { value displayValue }`)}
55100
- }
55101
- }
55102
- }
55103
- }
55104
- }
55105
- }
55106
- `;
55107
- }
55108
- generateDurableRecordRepresentation(node, objectInfo) {
55109
- const standardRecordFields = new Set(Object.keys(requiredFieldMap).map((x) => `${requiredPrefix}${x}`));
55110
- const id = node[`${requiredPrefix}Id`];
55111
- const recordTypeId = node[`${requiredPrefix}RecordTypeId`].value;
55112
- const requestedFields = Object.keys(node).filter((field) => !standardRecordFields.has(field));
55113
- const fields = requestedFields.reduce((acc, field) => {
55114
- const fieldNode = node[field];
55115
- if (fieldNode.displayValue === undefined) {
55116
- fieldNode.displayValue = null;
55117
- }
55118
- acc[field] = node[field];
55119
- return acc;
55120
- }, {});
55121
- fields['Id'] = { value: id, displayValue: null };
55122
- if (objectInfo.fields['RecordTypeId'] !== undefined) {
55123
- fields['RecordTypeId'] = { value: recordTypeId, displayValue: null };
55124
- }
55125
- let recordTypeInfo = null;
55126
- if (recordTypeId !== null &&
55127
- objectInfo.recordTypeInfos &&
55128
- objectInfo.recordTypeInfos[recordTypeId]) {
55129
- const appliedRecordTypeInfo = objectInfo.recordTypeInfos[recordTypeId];
55130
- // ui-api only applies the record type if its not the default type, otherwise it sets it to null
55131
- if (appliedRecordTypeInfo.defaultRecordTypeMapping === false) {
55132
- recordTypeInfo = appliedRecordTypeInfo;
55133
- }
55134
- }
55135
- return {
55136
- apiName: node[`${requiredPrefix}ApiName`],
55137
- childRelationships: {},
55138
- eTag: '',
55139
- id,
55140
- lastModifiedById: node[`${requiredPrefix}LastModifiedById`] &&
55141
- node[`${requiredPrefix}LastModifiedById`].value
55142
- ? node[`${requiredPrefix}LastModifiedById`].value
55143
- : null,
55144
- lastModifiedDate: node[`${requiredPrefix}LastModifiedDate`] &&
55145
- node[`${requiredPrefix}LastModifiedDate`].value
55146
- ? node[`${requiredPrefix}LastModifiedDate`].value
55147
- : null,
55148
- recordTypeId,
55149
- recordTypeInfo: recordTypeInfo,
55150
- systemModstamp: node[`${requiredPrefix}SystemModstamp`] &&
55151
- node[`${requiredPrefix}SystemModstamp`].value
55152
- ? node[`${requiredPrefix}SystemModstamp`].value
55153
- : null,
55154
- weakEtag: node[`${requiredPrefix}WeakEtag`],
55155
- fields,
55156
- links: {},
55157
- };
55158
- }
55159
- }
55160
-
55161
- const DEFAULT_GQL_QUERY_BATCH_SIZE = 100;
55162
- const apiPath = basePath + '/graphql/batch';
55163
- class RecordLoaderGraphQLBatch extends RecordLoaderGraphQL {
55164
- makeRequest(batch, observabilityContext) {
55165
- const query = this.generateGraphQLQuery(batch.type, batch.fields);
55166
- const chunks = chunk(batch.ids, DEFAULT_GQL_QUERY_BATCH_SIZE);
55167
- const batches = chunks.map((chunk) => {
55168
- return {
55169
- query,
55170
- variables: {
55171
- ids: chunk,
55172
- first: chunk.length,
55173
- },
55174
- };
55175
- });
55176
- return {
55177
- request: {
55178
- method: 'POST',
55179
- path: apiPath,
55180
- body: JSON.stringify({
55181
- batchQuery: batches,
55182
- }),
55183
- headers: {},
55184
- queryParams: {},
55185
- priority: 'background',
55186
- observabilityContext: observabilityContext !== undefined ? observabilityContext : {},
55187
- },
55188
- context: chunks,
55189
- };
55190
- }
55191
- generateFetchResults(body, batchInput, context) {
55192
- const { results } = body;
55193
- const fetchResults = context.flatMap((chunk, index) => {
55194
- const result = results[index];
55195
- if (!result || result.statusCode !== 200) {
55196
- return {
55197
- ok: false,
55198
- error: 'request-error',
55199
- messages: ['unexpected response from graphql batch endpoint'],
55200
- missingIds: chunk,
55201
- };
55202
- }
55203
- return super.generateFetchResults(result.result, {
55204
- ...batchInput,
55205
- ids: chunk,
55206
- }, undefined);
55207
- });
55208
- return fetchResults;
55209
- }
55210
- }
55211
-
55212
55040
  class SoqlRecordConverter {
55213
55041
  /**
55214
55042
  *
@@ -55701,16 +55529,7 @@ class LdsPrimingRecordRefresher {
55701
55529
  function primingSessionFactory(config) {
55702
55530
  const { store, objectInfoService, getLuvio } = config;
55703
55531
  const networkAdapter = new NimbusPrimingNetworkAdapter();
55704
- let recordLoader;
55705
- if (lmrPrimingUseSoql.isOpen({ fallback: false })) {
55706
- recordLoader = new RecordLoaderSOQLComposite(networkAdapter);
55707
- }
55708
- else if (ldsPrimingGraphqlBatch.isOpen({ fallback: false })) {
55709
- recordLoader = new RecordLoaderGraphQLBatch(networkAdapter);
55710
- }
55711
- else {
55712
- recordLoader = new RecordLoaderGraphQL(networkAdapter);
55713
- }
55532
+ const recordLoader = new RecordLoaderSOQLComposite(networkAdapter);
55714
55533
  const primingStore = new SqlitePrimingStore(getLuvio, store);
55715
55534
  const recordIngestor = new RecordIngestor(primingStore, getLuvio);
55716
55535
  const session = new PrimingSession({
@@ -56384,6 +56203,38 @@ function buildCachedSnapshotCachePolicy(context, storeLookup) {
56384
56203
  }
56385
56204
 
56386
56205
  const SIDE_EFFECT_SEGMENT = 'RECORD_SIDE_EFFECTS';
56206
+ // Base-record effects rebuild (or remove) the entire record for a key, so they must be
56207
+ // applied before any field-level effect on that same key — otherwise a field effect that
56208
+ // runs first is discarded when the base record is (re)built. keyToEffectMap ordering is not
56209
+ // stable: it is re-materialized from the durable store on reload / onChanged replay, where
56210
+ // entry order is not guaranteed. So two field-set effects on the same key from different
56211
+ // actions (e.g. two queued edits of one field) can come back in either order; applied out of
56212
+ // order the OLDER value wins and the newer edit is lost. Effects must therefore be applied in
56213
+ // action chronology (by timestamp), with base-record ops ordered ahead of field ops only as
56214
+ // the tie-breaker within one action — create/field effects on a draft-created record share a
56215
+ // key AND a timestamp, so timestamp alone can't order those.
56216
+ const BASE_RECORD_EFFECT_TYPES = new Set(['record-create', 'record-update', 'record-delete']);
56217
+ function orderEffectsForApply(effects) {
56218
+ // Nothing to reorder for a single effect; return the original array to avoid allocating on
56219
+ // the hot read-out path.
56220
+ if (effects.length <= 1) {
56221
+ return effects;
56222
+ }
56223
+ // Primary key: timestamp (action chronology). Secondary key: base-record ops before field
56224
+ // ops, which only matters within one action's shared timestamp. Array.prototype.sort is
56225
+ // stable (ES2019), so effects that tie on both keys keep their existing relative order.
56226
+ return [...effects].sort((a, b) => {
56227
+ if (a.timestamp !== b.timestamp) {
56228
+ return a.timestamp - b.timestamp;
56229
+ }
56230
+ const aIsBase = BASE_RECORD_EFFECT_TYPES.has(a.type);
56231
+ const bIsBase = BASE_RECORD_EFFECT_TYPES.has(b.type);
56232
+ if (aIsBase === bIsBase) {
56233
+ return 0;
56234
+ }
56235
+ return aIsBase ? -1 : 1;
56236
+ });
56237
+ }
56387
56238
  class SideEffectStore {
56388
56239
  constructor(durableStore) {
56389
56240
  this.durableStore = durableStore;
@@ -56462,7 +56313,7 @@ class SideEffectStore {
56462
56313
  if (effects === undefined) {
56463
56314
  return [];
56464
56315
  }
56465
- return effects;
56316
+ return orderEffectsForApply(effects);
56466
56317
  }
56467
56318
  getEffectsByTag(tag) {
56468
56319
  // This scan is still O(N) over every effect regardless of the incremental-map
@@ -56959,6 +56810,58 @@ class RecordFieldIncrementHandler {
56959
56810
  return record;
56960
56811
  }
56961
56812
  }
56813
+ class RecordFieldSetHandler {
56814
+ constructor() {
56815
+ this.logger = typeof __nimbus !== 'undefined' &&
56816
+ __nimbus.plugins !== undefined &&
56817
+ __nimbus.plugins.JSLoggerPlugin !== undefined
56818
+ ? __nimbus.plugins.JSLoggerPlugin
56819
+ : undefined;
56820
+ }
56821
+ apply(record, effect) {
56822
+ if (record === undefined) {
56823
+ if (this.logger) {
56824
+ this.logger.logWarning('RecordFieldSetHandler - cannot apply set side effect to non existing record');
56825
+ }
56826
+ return undefined;
56827
+ }
56828
+ // fieldName is an unconstrained string that has round-tripped through the durable
56829
+ // store, so a value like `__proto__` would hit the prototype setter on the plain
56830
+ // `fields`/`serverValues` objects below rather than creating an own field (prototype
56831
+ // pollution). Reject dangerous keys before any dynamic read/write and skip the effect.
56832
+ if (isUnsafeFieldName(effect.fieldName)) {
56833
+ if (this.logger) {
56834
+ this.logger.logError(`RecordFieldSetHandler - unsafe field name ${effect.fieldName}, skip applying side effect`);
56835
+ }
56836
+ return record;
56837
+ }
56838
+ applyDraftNode(record, effect);
56839
+ record.drafts.edited = true;
56840
+ // capture the server value so it can be restored when the draft syncs.
56841
+ // unlike the increment handler, the field may not yet exist on the record
56842
+ // (e.g. IsProduct2Serialized is not part of the draft-created record), in
56843
+ // which case we record `null` to indicate we never had the field.
56844
+ if (record.drafts.created === false) {
56845
+ const serverValue = record.drafts.serverValues[effect.fieldName];
56846
+ if (serverValue === undefined) {
56847
+ const existingField = record.fields[effect.fieldName];
56848
+ record.drafts.serverValues[effect.fieldName] =
56849
+ existingField === undefined ? null : existingField;
56850
+ }
56851
+ }
56852
+ record.fields[effect.fieldName] = {
56853
+ value: effect.value,
56854
+ displayValue: null,
56855
+ };
56856
+ return record;
56857
+ }
56858
+ }
56859
+ // Field names that would target the prototype chain rather than an own property when used
56860
+ // to index a plain object, enabling prototype pollution on the record's field containers.
56861
+ const UNSAFE_FIELD_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
56862
+ function isUnsafeFieldName(fieldName) {
56863
+ return UNSAFE_FIELD_NAMES.has(fieldName);
56864
+ }
56962
56865
  function applyDraftNode(record, effect) {
56963
56866
  if (record.drafts === undefined) {
56964
56867
  record.drafts = {
@@ -56993,6 +56896,7 @@ const handlerMap = {
56993
56896
  'record-update': new UpdateRecordEffectHandler(),
56994
56897
  'record-delete': new DeleteRecordEffectHandler(),
56995
56898
  'record-field-increment': new RecordFieldIncrementHandler(),
56899
+ 'record-field-set': new RecordFieldSetHandler(),
56996
56900
  };
56997
56901
  /**
56998
56902
  * Removes any existing effects on a record and re-applies the given ordered list and returns the record
@@ -57157,14 +57061,181 @@ function isStoreRecordError(storeRecord) {
57157
57061
  }
57158
57062
 
57159
57063
  const instrumentation = getInstrumentation(O11Y_NAMESPACE_LDS_MOBILE);
57064
+ const ProductItem$2 = {
57065
+ Fields: {
57066
+ PRODUCT2_ID: 'Product2Id',
57067
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
57068
+ },
57069
+ };
57070
+ const Product2 = {
57071
+ Fields: {
57072
+ IS_SERIALIZED: 'IsSerialized',
57073
+ },
57074
+ };
57075
+ var CounterResult;
57076
+ (function (CounterResult) {
57077
+ // A side effect was created.
57078
+ CounterResult["Created"] = "Created";
57079
+ // The hook exited early.
57080
+ CounterResult["Exited"] = "Exited";
57081
+ // An unexpected error occurred.
57082
+ CounterResult["Error"] = "Error";
57083
+ })(CounterResult || (CounterResult = {}));
57084
+ function incrementCounter(name, result) {
57085
+ instrumentation.incrementCounter('SFSSideEffect', 1, result === CounterResult.Error, {
57086
+ Name: name,
57087
+ Result: result,
57088
+ });
57089
+ }
57090
+ /**
57091
+ * Reads a string-valued field off a draft-create `fields` map (the raw create body, where
57092
+ * values are the primitives themselves rather than `{ value }` wrappers). Returns `undefined`
57093
+ * when the field is absent or not a string.
57094
+ */
57095
+ function getStringDraftField(fields, fieldName) {
57096
+ const field = fields[fieldName];
57097
+ if (field === undefined || typeof field !== 'string') {
57098
+ return undefined;
57099
+ }
57100
+ return field;
57101
+ }
57102
+ /**
57103
+ * Reads a string-valued field off a durable record's `fields` map (where each value is a
57104
+ * `{ value }` wrapper). Returns `undefined` when the field is absent, null, or not a string.
57105
+ */
57106
+ function getStringRecordField(fields, fieldName) {
57107
+ const field = fields[fieldName];
57108
+ if (field === undefined || field.value === undefined || typeof field.value !== 'string') {
57109
+ return undefined;
57110
+ }
57111
+ return field.value;
57112
+ }
57113
+ /**
57114
+ * Resolves the reference id that points at a related record for a draft edit.
57115
+ *
57116
+ * On a `patch` the body only carries the fields being changed, so the link field may be
57117
+ * absent, present with a new id, or present but explicitly `null` (the link is being cleared).
57118
+ * These three cases are distinguished via `hasOwnProperty` because they demand different
57119
+ * behavior — falling back to the original durable-store id on an explicit `null` would resolve
57120
+ * `IsProduct2Serialized` from a relationship the record no longer has:
57121
+ *
57122
+ * - present + string -> `resolved` with the reassigned id.
57123
+ * - present + null -> `cleared`.
57124
+ * - absent -> fall back to the original record; `resolved` when it carries a usable
57125
+ * string, otherwise `unresolved`.
57126
+ */
57127
+ function resolveReferenceIdForUpdate(patchFields, originalFields, fieldName) {
57128
+ if (Object.prototype.hasOwnProperty.call(patchFields, fieldName)) {
57129
+ const patchValue = patchFields[fieldName];
57130
+ if (patchValue === null) {
57131
+ return { kind: 'cleared' };
57132
+ }
57133
+ const patchId = getStringDraftField(patchFields, fieldName);
57134
+ if (patchId !== undefined) {
57135
+ return { kind: 'resolved', id: patchId, source: 'patch' };
57136
+ }
57137
+ // Present but neither a string nor null (unexpected shape): treat as an absent link
57138
+ // and fall through to the original record rather than guessing.
57139
+ }
57140
+ const originalId = getStringRecordField(originalFields, fieldName);
57141
+ if (originalId !== undefined) {
57142
+ return { kind: 'resolved', id: originalId, source: 'original' };
57143
+ }
57144
+ return { kind: 'unresolved' };
57145
+ }
57146
+ /**
57147
+ * Resolves the server-derived `IsProduct2Serialized` boolean for a draft-created record by
57148
+ * following the related `ProductItem`:
57149
+ *
57150
+ * 1. If the cached `ProductItem` already carries `IsProduct2Serialized`, use it directly
57151
+ * (it mirrors the linked product's serialization, and avoids a second durable read).
57152
+ * 2. Otherwise fall back to the linked `Product2`:
57153
+ * `ProductItem.Product2Id` -> `Product2.IsSerialized`.
57154
+ *
57155
+ * All lookups go through the given `RecordReader` (the durable store, or a per-action memoizer
57156
+ * that dedups reads). Returns `undefined` when a link in the chain is missing so the caller can
57157
+ * exit without an effect and leave the field synthesized (null). Callers pass their own
57158
+ * `counterName` so instrumentation is attributed to the owning hook.
57159
+ */
57160
+ async function resolveIsProduct2SerializedFromProductItem(recordReader, productItemId, counterName) {
57161
+ const productItemRecord = await recordReader.getRecord(buildRecordRepKeyFromId(productItemId));
57162
+ if (productItemRecord === undefined) {
57163
+ incrementCounter(counterName, CounterResult.Exited);
57164
+ return undefined;
57165
+ }
57166
+ // Prefer IsProduct2Serialized straight off the ProductItem when it is cached there;
57167
+ // it saves a second durable read. Fall back to the linked Product2 otherwise.
57168
+ const fromProductItem = getIsProduct2SerializedFromRecordFields(productItemRecord.fields);
57169
+ if (fromProductItem !== undefined) {
57170
+ incrementCounter(counterName, CounterResult.Created);
57171
+ return fromProductItem;
57172
+ }
57173
+ const product2Id = getProduct2IdFromRecordFields(productItemRecord.fields);
57174
+ if (product2Id === undefined) {
57175
+ incrementCounter(counterName, CounterResult.Exited);
57176
+ return undefined;
57177
+ }
57178
+ return resolveIsSerializedFromProduct2Id(recordReader, product2Id, counterName);
57179
+ }
57180
+ /**
57181
+ * Resolves the `Product2.IsSerialized` boolean for a given `Product2Id` by reading the linked
57182
+ * `Product2` record through the given `RecordReader`. Used directly by the `ProductItem` hooks
57183
+ * (which carry `Product2Id` themselves) and as the fallback tail of
57184
+ * `resolveIsProduct2SerializedFromProductItem`.
57185
+ *
57186
+ * Returns `undefined` when the `Product2` record is missing so the caller can exit without an
57187
+ * effect and leave the field synthesized (null). Callers pass their own `counterName` so
57188
+ * instrumentation is attributed to the owning hook.
57189
+ */
57190
+ async function resolveIsSerializedFromProduct2Id(recordReader, product2Id, counterName) {
57191
+ const product2Record = await recordReader.getRecord(buildRecordRepKeyFromId(product2Id));
57192
+ if (product2Record === undefined) {
57193
+ incrementCounter(counterName, CounterResult.Exited);
57194
+ return undefined;
57195
+ }
57196
+ const isSerialized = getIsSerializedFromRecordFields(product2Record.fields);
57197
+ if (isSerialized === undefined) {
57198
+ incrementCounter(counterName, CounterResult.Error);
57199
+ // Keep the message a stable literal and append the variable parts as key=value
57200
+ // pairs, so the log line stays low-cardinality and greppable in Splunk (the raw
57201
+ // record id is a discrete field rather than baked into the message text).
57202
+ instrumentation.error(`SFS - Product2 IsSerialized not found: hook=${counterName}, product2Id=${product2Id}`);
57203
+ return undefined;
57204
+ }
57205
+ incrementCounter(counterName, CounterResult.Created);
57206
+ return isSerialized;
57207
+ }
57208
+ function getProduct2IdFromRecordFields(fields) {
57209
+ const field = fields[ProductItem$2.Fields.PRODUCT2_ID];
57210
+ if (field === undefined || field.value === undefined || typeof field.value !== 'string') {
57211
+ return undefined;
57212
+ }
57213
+ return field.value;
57214
+ }
57215
+ function getIsProduct2SerializedFromRecordFields(fields) {
57216
+ const field = fields[ProductItem$2.Fields.IS_PRODUCT2_SERIALIZED];
57217
+ if (field === undefined || field.value === undefined || typeof field.value !== 'boolean') {
57218
+ return undefined;
57219
+ }
57220
+ return field.value;
57221
+ }
57222
+ function getIsSerializedFromRecordFields(fields) {
57223
+ const field = fields[Product2.Fields.IS_SERIALIZED];
57224
+ if (field === undefined || field.value === undefined || typeof field.value !== 'boolean') {
57225
+ return undefined;
57226
+ }
57227
+ return field.value;
57228
+ }
57229
+
57160
57230
  const ProductConsumed = {
57161
57231
  API_NAME: 'ProductConsumed',
57162
57232
  Fields: {
57163
57233
  PRODUCT_ITEM_ID: 'ProductItemId',
57164
57234
  QUANTITY_CONSUMED: 'QuantityConsumed',
57235
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
57165
57236
  },
57166
57237
  };
57167
- const ProductItem = {
57238
+ const ProductItem$1 = {
57168
57239
  Fields: {
57169
57240
  QUANTITY_ON_HAND: 'QuantityOnHand',
57170
57241
  },
@@ -57175,7 +57246,7 @@ class ProductConsumedCreatedHook {
57175
57246
  this.apiName = ProductConsumed.API_NAME;
57176
57247
  }
57177
57248
  async then(draftActionId, draftActionTimestamp, _draftRecordId, fields) {
57178
- const productItemId = getProductItemId(fields);
57249
+ const productItemId = getStringDraftField(fields, ProductConsumed.Fields.PRODUCT_ITEM_ID);
57179
57250
  const quantityConsumed = getQuantityConsumed(fields);
57180
57251
  if (productItemId === undefined) {
57181
57252
  incrementCounter(ProductConsumedCreatedHook.COUNTER_NAME, CounterResult.Exited);
@@ -57188,7 +57259,7 @@ class ProductConsumedCreatedHook {
57188
57259
  incrementCounter(ProductConsumedCreatedHook.COUNTER_NAME, CounterResult.Created);
57189
57260
  const effect = {
57190
57261
  type: 'record-field-increment',
57191
- fieldName: ProductItem.Fields.QUANTITY_ON_HAND,
57262
+ fieldName: ProductItem$1.Fields.QUANTITY_ON_HAND,
57192
57263
  value: -quantityConsumed,
57193
57264
  key: buildRecordRepKeyFromId(productItemId),
57194
57265
  tag: draftActionId,
@@ -57205,16 +57276,17 @@ class ProductConsumedUpdatedHook {
57205
57276
  this.on = 'patch';
57206
57277
  this.apiName = ProductConsumed.API_NAME;
57207
57278
  }
57208
- async then(draftActionId, draftActionTimestamp, draftRecordId, fields) {
57279
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
57209
57280
  const quantityConsumed = getQuantityConsumed(fields);
57210
57281
  if (quantityConsumed === undefined) {
57211
57282
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
57212
57283
  return [];
57213
57284
  }
57214
- const originalRecord = await this.durableRecordStore.getRecord(buildRecordRepKeyFromId(draftRecordId));
57285
+ const recordReader = reader ?? this.durableRecordStore;
57286
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId(draftRecordId));
57215
57287
  if (originalRecord === undefined) {
57216
57288
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Error);
57217
- instrumentation.error(`SFS - ProductConsumedUpdatedHook - Original record ${draftRecordId} not found`);
57289
+ instrumentation.error(`SFS - ProductConsumedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
57218
57290
  return [];
57219
57291
  }
57220
57292
  const originalProductItemId = getProductItemIdFromRecordFields(originalRecord.fields);
@@ -57225,13 +57297,13 @@ class ProductConsumedUpdatedHook {
57225
57297
  }
57226
57298
  if (originalQuantityConsumed === undefined) {
57227
57299
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Error);
57228
- instrumentation.error(`SFS - ProductConsumedUpdatedHook - Original record ${draftRecordId} QuantityConsumed not found`);
57300
+ instrumentation.error(`SFS - ProductConsumedUpdatedHook original record QuantityConsumed not found: draftRecordId=${draftRecordId}`);
57229
57301
  return [];
57230
57302
  }
57231
57303
  incrementCounter(ProductConsumedUpdatedHook.COUNTER_NAME, CounterResult.Created);
57232
57304
  const effect = {
57233
57305
  type: 'record-field-increment',
57234
- fieldName: ProductItem.Fields.QUANTITY_ON_HAND,
57306
+ fieldName: ProductItem$1.Fields.QUANTITY_ON_HAND,
57235
57307
  value: originalQuantityConsumed - quantityConsumed,
57236
57308
  key: buildRecordRepKeyFromId(originalProductItemId),
57237
57309
  tag: draftActionId,
@@ -57248,11 +57320,12 @@ class ProductConsumedDeletedHook {
57248
57320
  this.on = 'delete';
57249
57321
  this.apiName = ProductConsumed.API_NAME;
57250
57322
  }
57251
- async then(draftActionId, draftActionTimestamp, draftRecordId) {
57252
- const originalRecord = await this.durableRecordStore.getRecord(buildRecordRepKeyFromId(draftRecordId));
57323
+ async then(draftActionId, draftActionTimestamp, draftRecordId, _fields, reader) {
57324
+ const recordReader = reader ?? this.durableRecordStore;
57325
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId(draftRecordId));
57253
57326
  if (originalRecord === undefined) {
57254
57327
  incrementCounter(ProductConsumedDeletedHook.COUNTER_NAME, CounterResult.Error);
57255
- instrumentation.error(`SFS - ProductConsumedDeletedHook - Original record ${draftRecordId} not found`);
57328
+ instrumentation.error(`SFS - ProductConsumedDeletedHook original record not found: draftRecordId=${draftRecordId}`);
57256
57329
  return [];
57257
57330
  }
57258
57331
  const productItemId = getProductItemIdFromRecordFields(originalRecord.fields);
@@ -57263,13 +57336,13 @@ class ProductConsumedDeletedHook {
57263
57336
  }
57264
57337
  if (quantityConsumed === undefined) {
57265
57338
  incrementCounter(ProductConsumedDeletedHook.COUNTER_NAME, CounterResult.Error);
57266
- instrumentation.error(`SFS - ProductConsumedDeletedHook - Original record ${draftRecordId} QuantityConsumed not found`);
57339
+ instrumentation.error(`SFS - ProductConsumedDeletedHook original record QuantityConsumed not found: draftRecordId=${draftRecordId}`);
57267
57340
  return [];
57268
57341
  }
57269
57342
  incrementCounter(ProductConsumedDeletedHook.COUNTER_NAME, CounterResult.Created);
57270
57343
  const effect = {
57271
57344
  type: 'record-field-increment',
57272
- fieldName: ProductItem.Fields.QUANTITY_ON_HAND,
57345
+ fieldName: ProductItem$1.Fields.QUANTITY_ON_HAND,
57273
57346
  value: quantityConsumed,
57274
57347
  key: buildRecordRepKeyFromId(productItemId),
57275
57348
  tag: draftActionId,
@@ -57280,13 +57353,115 @@ class ProductConsumedDeletedHook {
57280
57353
  }
57281
57354
  }
57282
57355
  ProductConsumedDeletedHook.COUNTER_NAME = 'ProductConsumedDeletedHook';
57283
- function getProductItemId(fields) {
57284
- const field = fields[ProductConsumed.Fields.PRODUCT_ITEM_ID];
57285
- if (field === undefined || typeof field !== 'string') {
57286
- return undefined;
57356
+ /**
57357
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-created
57358
+ * `ProductConsumed` record.
57359
+ *
57360
+ * `Product2Id` is not createable on `ProductConsumed`, so the draft only carries the
57361
+ * `ProductItemId`. The value is resolved through the related `ProductItem` record (see
57362
+ * `resolveIsProduct2SerializedFromProductItem`): the ProductItem's own
57363
+ * `IsProduct2Serialized` when cached, otherwise `ProductItem.Product2Id` ->
57364
+ * `Product2.IsSerialized`. When the chain cannot be resolved the hook exits without an
57365
+ * effect and the field is left as synthesized (null).
57366
+ */
57367
+ class ProductConsumedIsSerializedCreatedHook {
57368
+ constructor(durableRecordStore) {
57369
+ this.durableRecordStore = durableRecordStore;
57370
+ this.on = 'post';
57371
+ this.apiName = ProductConsumed.API_NAME;
57372
+ }
57373
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
57374
+ const productItemId = getStringDraftField(fields, ProductConsumed.Fields.PRODUCT_ITEM_ID);
57375
+ if (productItemId === undefined) {
57376
+ incrementCounter(ProductConsumedIsSerializedCreatedHook.COUNTER_NAME, CounterResult.Exited);
57377
+ return [];
57378
+ }
57379
+ const isSerialized = await resolveIsProduct2SerializedFromProductItem(reader ?? this.durableRecordStore, productItemId, ProductConsumedIsSerializedCreatedHook.COUNTER_NAME);
57380
+ if (isSerialized === undefined) {
57381
+ return [];
57382
+ }
57383
+ const effect = {
57384
+ type: 'record-field-set',
57385
+ fieldName: ProductConsumed.Fields.IS_PRODUCT2_SERIALIZED,
57386
+ value: isSerialized,
57387
+ key: buildRecordRepKeyFromId(draftRecordId),
57388
+ tag: draftActionId,
57389
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
57390
+ timestamp: draftActionTimestamp,
57391
+ };
57392
+ return [effect];
57287
57393
  }
57288
- return field;
57289
57394
  }
57395
+ ProductConsumedIsSerializedCreatedHook.COUNTER_NAME = 'ProductConsumedIsSerializedCreatedHook';
57396
+ /**
57397
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-edited
57398
+ * `ProductConsumed` record.
57399
+ *
57400
+ * A `patch` body only carries changed fields, so the related `ProductItemId` may be absent.
57401
+ * The id is resolved by preferring the patch body (the link is being reassigned) and falling
57402
+ * back to the original record in the durable store, then the value is resolved through that
57403
+ * `ProductItem` exactly as on create (see `resolveIsProduct2SerializedFromProductItem`).
57404
+ *
57405
+ * When the patch explicitly clears `ProductItemId` (sets it to `null`) the record no longer
57406
+ * derives from any `ProductItem`, so `IsProduct2Serialized` is cleared (set to `null`) rather
57407
+ * than resolved from the now-stale original link. Likewise, when the patch reassigns
57408
+ * `ProductItemId` to a new id whose chain isn't cached, the stored value reflects the old link
57409
+ * and is stale, so it is cleared. When the link is unchanged and its chain cannot be resolved,
57410
+ * the stored value is still correct so the hook exits without an effect and leaves it unchanged.
57411
+ */
57412
+ class ProductConsumedIsSerializedUpdatedHook {
57413
+ constructor(durableRecordStore) {
57414
+ this.durableRecordStore = durableRecordStore;
57415
+ this.on = 'patch';
57416
+ this.apiName = ProductConsumed.API_NAME;
57417
+ }
57418
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
57419
+ const recordReader = reader ?? this.durableRecordStore;
57420
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId(draftRecordId));
57421
+ if (originalRecord === undefined) {
57422
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Error);
57423
+ instrumentation.error(`SFS - ProductConsumedIsSerializedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
57424
+ return [];
57425
+ }
57426
+ const reference = resolveReferenceIdForUpdate(fields, originalRecord.fields, ProductConsumed.Fields.PRODUCT_ITEM_ID);
57427
+ if (reference.kind === 'unresolved') {
57428
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
57429
+ return [];
57430
+ }
57431
+ let isSerialized;
57432
+ if (reference.kind === 'cleared') {
57433
+ // The link was explicitly cleared: the record derives from no ProductItem, so clear
57434
+ // IsProduct2Serialized rather than resolving it from the stale original link.
57435
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
57436
+ isSerialized = null;
57437
+ }
57438
+ else {
57439
+ isSerialized = await resolveIsProduct2SerializedFromProductItem(recordReader, reference.id, ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME);
57440
+ if (isSerialized === undefined && reference.source === 'patch') {
57441
+ // The link was reassigned to a new ProductItem whose chain isn't cached yet, so
57442
+ // the stored IsProduct2Serialized still reflects the OLD link and is now stale.
57443
+ // Clear it rather than leaving the stale value alongside the new relationship;
57444
+ // the server recomputes the real value on sync.
57445
+ incrementCounter(ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
57446
+ isSerialized = null;
57447
+ }
57448
+ }
57449
+ if (isSerialized === undefined) {
57450
+ return [];
57451
+ }
57452
+ const effect = {
57453
+ type: 'record-field-set',
57454
+ fieldName: ProductConsumed.Fields.IS_PRODUCT2_SERIALIZED,
57455
+ value: isSerialized,
57456
+ key: buildRecordRepKeyFromId(draftRecordId),
57457
+ tag: draftActionId,
57458
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
57459
+ timestamp: draftActionTimestamp,
57460
+ };
57461
+ return [effect];
57462
+ }
57463
+ }
57464
+ ProductConsumedIsSerializedUpdatedHook.COUNTER_NAME = 'ProductConsumedIsSerializedUpdatedHook';
57290
57465
  function getProductItemIdFromRecordFields(fields) {
57291
57466
  const field = fields[ProductConsumed.Fields.PRODUCT_ITEM_ID];
57292
57467
  if (field === undefined || field.value === undefined || typeof field.value !== 'string') {
@@ -57308,29 +57483,265 @@ function getQuantityConsumedFromRecordFields(fields) {
57308
57483
  }
57309
57484
  return field.value;
57310
57485
  }
57311
- var CounterResult;
57312
- (function (CounterResult) {
57313
- // A side effect was created.
57314
- CounterResult["Created"] = "Created";
57315
- // The hook exited early.
57316
- CounterResult["Exited"] = "Exited";
57317
- // An unexpected error occurred.
57318
- CounterResult["Error"] = "Error";
57319
- })(CounterResult || (CounterResult = {}));
57320
- function incrementCounter(name, result) {
57321
- instrumentation.incrementCounter('SFSSideEffect', 1, result === CounterResult.Error, {
57322
- Name: name,
57323
- Result: result,
57324
- });
57486
+
57487
+ const ProductTransfer = {
57488
+ API_NAME: 'ProductTransfer',
57489
+ Fields: {
57490
+ SOURCE_PRODUCT_ITEM_ID: 'SourceProductItemId',
57491
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
57492
+ },
57493
+ };
57494
+ /**
57495
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-created
57496
+ * `ProductTransfer` record.
57497
+ *
57498
+ * The value is resolved through the related `SourceProductItem` record (a `ProductItem`),
57499
+ * mirroring the `ProductConsumed` hook: the ProductItem's own `IsProduct2Serialized` when
57500
+ * cached, otherwise `ProductItem.Product2Id` -> `Product2.IsSerialized` (see
57501
+ * `resolveIsProduct2SerializedFromProductItem`). When the chain cannot be resolved the hook
57502
+ * exits without an effect and the field is left as synthesized (null).
57503
+ */
57504
+ class ProductTransferIsSerializedCreatedHook {
57505
+ constructor(durableRecordStore) {
57506
+ this.durableRecordStore = durableRecordStore;
57507
+ this.on = 'post';
57508
+ this.apiName = ProductTransfer.API_NAME;
57509
+ }
57510
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
57511
+ const sourceProductItemId = getStringDraftField(fields, ProductTransfer.Fields.SOURCE_PRODUCT_ITEM_ID);
57512
+ if (sourceProductItemId === undefined) {
57513
+ incrementCounter(ProductTransferIsSerializedCreatedHook.COUNTER_NAME, CounterResult.Exited);
57514
+ return [];
57515
+ }
57516
+ const isSerialized = await resolveIsProduct2SerializedFromProductItem(reader ?? this.durableRecordStore, sourceProductItemId, ProductTransferIsSerializedCreatedHook.COUNTER_NAME);
57517
+ if (isSerialized === undefined) {
57518
+ return [];
57519
+ }
57520
+ const effect = {
57521
+ type: 'record-field-set',
57522
+ fieldName: ProductTransfer.Fields.IS_PRODUCT2_SERIALIZED,
57523
+ value: isSerialized,
57524
+ key: buildRecordRepKeyFromId(draftRecordId),
57525
+ tag: draftActionId,
57526
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
57527
+ timestamp: draftActionTimestamp,
57528
+ };
57529
+ return [effect];
57530
+ }
57531
+ }
57532
+ ProductTransferIsSerializedCreatedHook.COUNTER_NAME = 'ProductTransferIsSerializedCreatedHook';
57533
+ /**
57534
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-edited
57535
+ * `ProductTransfer` record.
57536
+ *
57537
+ * ProductTransfer can only be edited (not created) on mobile, so this is the hook that fires
57538
+ * in practice. A `patch` body only carries changed fields, so the related `SourceProductItemId`
57539
+ * may be absent; the id is resolved by preferring the patch body (the link is being reassigned)
57540
+ * and falling back to the original record in the durable store, then the value is resolved
57541
+ * through that `ProductItem` exactly as on create (see
57542
+ * `resolveIsProduct2SerializedFromProductItem`).
57543
+ *
57544
+ * When the patch explicitly clears `SourceProductItemId` (sets it to `null`) the record no
57545
+ * longer derives from any `ProductItem`, so `IsProduct2Serialized` is cleared (set to `null`)
57546
+ * rather than resolved from the now-stale original link. Likewise, when the patch reassigns
57547
+ * `SourceProductItemId` to a new id whose chain isn't cached, the stored value reflects the old
57548
+ * link and is stale, so it is cleared. When the link is unchanged and its chain cannot be
57549
+ * resolved, the stored value is still correct so the hook exits without an effect and leaves it
57550
+ * unchanged.
57551
+ */
57552
+ class ProductTransferIsSerializedUpdatedHook {
57553
+ constructor(durableRecordStore) {
57554
+ this.durableRecordStore = durableRecordStore;
57555
+ this.on = 'patch';
57556
+ this.apiName = ProductTransfer.API_NAME;
57557
+ }
57558
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
57559
+ const recordReader = reader ?? this.durableRecordStore;
57560
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId(draftRecordId));
57561
+ if (originalRecord === undefined) {
57562
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Error);
57563
+ instrumentation.error(`SFS - ProductTransferIsSerializedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
57564
+ return [];
57565
+ }
57566
+ const reference = resolveReferenceIdForUpdate(fields, originalRecord.fields, ProductTransfer.Fields.SOURCE_PRODUCT_ITEM_ID);
57567
+ if (reference.kind === 'unresolved') {
57568
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
57569
+ return [];
57570
+ }
57571
+ let isSerialized;
57572
+ if (reference.kind === 'cleared') {
57573
+ // The link was explicitly cleared: the record derives from no ProductItem, so clear
57574
+ // IsProduct2Serialized rather than resolving it from the stale original link.
57575
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
57576
+ isSerialized = null;
57577
+ }
57578
+ else {
57579
+ isSerialized = await resolveIsProduct2SerializedFromProductItem(recordReader, reference.id, ProductTransferIsSerializedUpdatedHook.COUNTER_NAME);
57580
+ if (isSerialized === undefined && reference.source === 'patch') {
57581
+ // The link was reassigned to a new ProductItem whose chain isn't cached yet, so
57582
+ // the stored IsProduct2Serialized still reflects the OLD link and is now stale.
57583
+ // Clear it rather than leaving the stale value alongside the new relationship;
57584
+ // the server recomputes the real value on sync.
57585
+ incrementCounter(ProductTransferIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
57586
+ isSerialized = null;
57587
+ }
57588
+ }
57589
+ if (isSerialized === undefined) {
57590
+ return [];
57591
+ }
57592
+ const effect = {
57593
+ type: 'record-field-set',
57594
+ fieldName: ProductTransfer.Fields.IS_PRODUCT2_SERIALIZED,
57595
+ value: isSerialized,
57596
+ key: buildRecordRepKeyFromId(draftRecordId),
57597
+ tag: draftActionId,
57598
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
57599
+ timestamp: draftActionTimestamp,
57600
+ };
57601
+ return [effect];
57602
+ }
57603
+ }
57604
+ ProductTransferIsSerializedUpdatedHook.COUNTER_NAME = 'ProductTransferIsSerializedUpdatedHook';
57605
+
57606
+ const ProductItem = {
57607
+ API_NAME: 'ProductItem',
57608
+ Fields: {
57609
+ PRODUCT2_ID: 'Product2Id',
57610
+ IS_PRODUCT2_SERIALIZED: 'IsProduct2Serialized',
57611
+ },
57612
+ };
57613
+ /**
57614
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-created
57615
+ * `ProductItem` record.
57616
+ *
57617
+ * Unlike `ProductConsumed`/`ProductTransfer` — which reach the product through a related
57618
+ * `ProductItem` — a `ProductItem` carries `Product2Id` directly, so the value is resolved as
57619
+ * `Product2Id` -> `Product2.IsSerialized` (see `resolveIsSerializedFromProduct2Id`). When the
57620
+ * chain cannot be resolved the hook exits without an effect and the field is left as
57621
+ * synthesized (null).
57622
+ */
57623
+ class ProductItemIsSerializedCreatedHook {
57624
+ constructor(durableRecordStore) {
57625
+ this.durableRecordStore = durableRecordStore;
57626
+ this.on = 'post';
57627
+ this.apiName = ProductItem.API_NAME;
57628
+ }
57629
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
57630
+ const product2Id = getStringDraftField(fields, ProductItem.Fields.PRODUCT2_ID);
57631
+ if (product2Id === undefined) {
57632
+ incrementCounter(ProductItemIsSerializedCreatedHook.COUNTER_NAME, CounterResult.Exited);
57633
+ return [];
57634
+ }
57635
+ const isSerialized = await resolveIsSerializedFromProduct2Id(reader ?? this.durableRecordStore, product2Id, ProductItemIsSerializedCreatedHook.COUNTER_NAME);
57636
+ if (isSerialized === undefined) {
57637
+ return [];
57638
+ }
57639
+ const effect = {
57640
+ type: 'record-field-set',
57641
+ fieldName: ProductItem.Fields.IS_PRODUCT2_SERIALIZED,
57642
+ value: isSerialized,
57643
+ key: buildRecordRepKeyFromId(draftRecordId),
57644
+ tag: draftActionId,
57645
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
57646
+ timestamp: draftActionTimestamp,
57647
+ };
57648
+ return [effect];
57649
+ }
57325
57650
  }
57651
+ ProductItemIsSerializedCreatedHook.COUNTER_NAME = 'ProductItemIsSerializedCreatedHook';
57652
+ /**
57653
+ * Replicates the server-derived `IsProduct2Serialized` boolean on a draft-edited
57654
+ * `ProductItem` record.
57655
+ *
57656
+ * A `patch` body only carries changed fields, so `Product2Id` may be absent; it is resolved by
57657
+ * preferring the patch body (the product is being reassigned) and falling back to the original
57658
+ * record in the durable store, then resolved as `Product2Id` -> `Product2.IsSerialized` (see
57659
+ * `resolveIsSerializedFromProduct2Id`).
57660
+ *
57661
+ * When the patch explicitly clears `Product2Id` (sets it to `null`) the record no longer
57662
+ * derives from any `Product2`, so `IsProduct2Serialized` is cleared (set to `null`) rather than
57663
+ * resolved from the now-stale original link. Likewise, when the patch reassigns `Product2Id` to
57664
+ * a new id whose `Product2` isn't cached, the stored value reflects the old link and is stale,
57665
+ * so it is cleared. When the link is unchanged and its `Product2` cannot be resolved, the stored
57666
+ * value is still correct so the hook exits without an effect and leaves it unchanged.
57667
+ */
57668
+ class ProductItemIsSerializedUpdatedHook {
57669
+ constructor(durableRecordStore) {
57670
+ this.durableRecordStore = durableRecordStore;
57671
+ this.on = 'patch';
57672
+ this.apiName = ProductItem.API_NAME;
57673
+ }
57674
+ async then(draftActionId, draftActionTimestamp, draftRecordId, fields, reader) {
57675
+ const recordReader = reader ?? this.durableRecordStore;
57676
+ const originalRecord = await recordReader.getRecord(buildRecordRepKeyFromId(draftRecordId));
57677
+ if (originalRecord === undefined) {
57678
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Error);
57679
+ instrumentation.error(`SFS - ProductItemIsSerializedUpdatedHook original record not found: draftRecordId=${draftRecordId}`);
57680
+ return [];
57681
+ }
57682
+ const reference = resolveReferenceIdForUpdate(fields, originalRecord.fields, ProductItem.Fields.PRODUCT2_ID);
57683
+ if (reference.kind === 'unresolved') {
57684
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Exited);
57685
+ return [];
57686
+ }
57687
+ let isSerialized;
57688
+ if (reference.kind === 'cleared') {
57689
+ // The link was explicitly cleared: the record derives from no Product2, so clear
57690
+ // IsProduct2Serialized rather than resolving it from the stale original link.
57691
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
57692
+ isSerialized = null;
57693
+ }
57694
+ else {
57695
+ isSerialized = await resolveIsSerializedFromProduct2Id(recordReader, reference.id, ProductItemIsSerializedUpdatedHook.COUNTER_NAME);
57696
+ if (isSerialized === undefined && reference.source === 'patch') {
57697
+ // The link was reassigned to a new Product2 that isn't cached yet, so the stored
57698
+ // IsProduct2Serialized still reflects the OLD link and is now stale. Clear it
57699
+ // rather than leaving the stale value alongside the new relationship; the server
57700
+ // recomputes the real value on sync.
57701
+ incrementCounter(ProductItemIsSerializedUpdatedHook.COUNTER_NAME, CounterResult.Created);
57702
+ isSerialized = null;
57703
+ }
57704
+ }
57705
+ if (isSerialized === undefined) {
57706
+ return [];
57707
+ }
57708
+ const effect = {
57709
+ type: 'record-field-set',
57710
+ fieldName: ProductItem.Fields.IS_PRODUCT2_SERIALIZED,
57711
+ value: isSerialized,
57712
+ key: buildRecordRepKeyFromId(draftRecordId),
57713
+ tag: draftActionId,
57714
+ uniqueId: `${draftActionId}-IsProduct2Serialized`,
57715
+ timestamp: draftActionTimestamp,
57716
+ };
57717
+ return [effect];
57718
+ }
57719
+ }
57720
+ ProductItemIsSerializedUpdatedHook.COUNTER_NAME = 'ProductItemIsSerializedUpdatedHook';
57326
57721
 
57327
57722
  function createSfsSideEffectHooks(durableRecordStore) {
57328
57723
  if (productConsumedSideEffects.isOpen({ fallback: false })) {
57329
- return [
57724
+ const hooks = [
57330
57725
  new ProductConsumedCreatedHook(),
57331
57726
  new ProductConsumedUpdatedHook(durableRecordStore),
57332
57727
  new ProductConsumedDeletedHook(durableRecordStore),
57333
57728
  ];
57729
+ // The IsProduct2Serialized side effects (ProductConsumed, ProductTransfer and
57730
+ // ProductItem) ship on by default and can be disabled independently of the van stock
57731
+ // hooks above via a shared kill switch: the gate is closed (fallback false) by default,
57732
+ // which keeps the effects enabled, and opening the gate turns them off.
57733
+ //
57734
+ // Each object gets both a create (post) and edit (patch) hook so the field is
57735
+ // synthesized on either draft action, keeping the behavior uniform across the objects.
57736
+ if (!isProduct2SerializedSideEffectKillSwitch.isOpen({ fallback: false })) {
57737
+ hooks.push(new ProductConsumedIsSerializedCreatedHook(durableRecordStore));
57738
+ hooks.push(new ProductConsumedIsSerializedUpdatedHook(durableRecordStore));
57739
+ hooks.push(new ProductTransferIsSerializedCreatedHook(durableRecordStore));
57740
+ hooks.push(new ProductTransferIsSerializedUpdatedHook(durableRecordStore));
57741
+ hooks.push(new ProductItemIsSerializedCreatedHook(durableRecordStore));
57742
+ hooks.push(new ProductItemIsSerializedUpdatedHook(durableRecordStore));
57743
+ }
57744
+ return hooks;
57334
57745
  }
57335
57746
  return [];
57336
57747
  }
@@ -59715,7 +60126,7 @@ function buildServiceDescriptor$b(luvio) {
59715
60126
  },
59716
60127
  };
59717
60128
  }
59718
- // version: 1.460.0-f7dc7797dd
60129
+ // version: 1.462.0-50d9a13ba4
59719
60130
 
59720
60131
  /**
59721
60132
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -59741,7 +60152,7 @@ function buildServiceDescriptor$a(notifyRecordUpdateAvailable, getNormalizedLuvi
59741
60152
  },
59742
60153
  };
59743
60154
  }
59744
- // version: 1.460.0-f7dc7797dd
60155
+ // version: 1.462.0-50d9a13ba4
59745
60156
 
59746
60157
  function findExecutableOperation(input) {
59747
60158
  const operations = input.query.definitions.filter(
@@ -62130,9 +62541,6 @@ class NimbusSqliteOneStoreCache {
62130
62541
 
62131
62542
  const PURGE_CACHE_DAYS = 30;
62132
62543
  function initializeOneStore(sqliteStore, luvio) {
62133
- if (!useOneStore.isOpen({ fallback: true })) {
62134
- return;
62135
- }
62136
62544
  // Initialize performance.now() fallback if not available
62137
62545
  if (typeof globalThis.performance === 'undefined') {
62138
62546
  globalThis.performance = { now: () => Date.now() };
@@ -62319,12 +62727,6 @@ function getRuntime({ primeUser = false } = {}) {
62319
62727
  lazyInternalLuvio = internalLuvio;
62320
62728
  lazyObjectInfoService = new ObjectInfoService(getObjectInfo, getObjectInfos, getObjectInfoDirectory, lazyDurableStore);
62321
62729
  const baseEnv = new Environment(store, lazyNetworkAdapter);
62322
- const shouldFilterFieldsOnRevive = (key) => {
62323
- if (reviveOnlyRequestedFields.isOpen({ fallback: true })) {
62324
- return isStoreKeyRecordId(key);
62325
- }
62326
- return false;
62327
- };
62328
62730
  const gqlEnv = makeEnvironmentGraphqlAware(baseEnv);
62329
62731
  const durableEnv = makeDurable(gqlEnv, {
62330
62732
  durableStore: lazyDurableStore,
@@ -62332,7 +62734,12 @@ function getRuntime({ primeUser = false } = {}) {
62332
62734
  // disable luvio deep freeze in headless environments
62333
62735
  disableDeepFreeze: typeof window === 'undefined',
62334
62736
  shouldFlush,
62335
- shouldFilterFieldsOnRevive,
62737
+ // Disabling field filtering on revive for now (previously gated behind
62738
+ // the lmr.reviveOnlyRequestedFields gate). The code path is retained
62739
+ // below should we need to re-enable this behavior in the future:
62740
+ shouldFilterFieldsOnRevive: (_key) => {
62741
+ return false;
62742
+ },
62336
62743
  });
62337
62744
  // draft queue
62338
62745
  lazyDraftQueue = buildLdsDraftQueue(lazyDurableStore);
@@ -62453,4 +62860,4 @@ register({
62453
62860
  });
62454
62861
 
62455
62862
  export { O11Y_NAMESPACE_LDS_MOBILE, getRuntime, ingest$1o as ingestDenormalizedRecordRepresentation, initializeOneStore, registerReportObserver, reportGraphqlQueryParseError };
62456
- // version: 1.460.0-f7dc7797dd
62863
+ // version: 1.462.0-50d9a13ba4