@luvio/environments 0.160.2 → 0.160.3

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.
@@ -22,7 +22,7 @@ const RedirectDurableSegment = 'REDIRECT_KEYS';
22
22
  const MessagingDurableSegment = 'MESSAGING';
23
23
  const MessageNotifyStoreUpdateAvailable = 'notifyStoreUpdateAvailable';
24
24
 
25
- const { keys, create, assign, freeze } = Object;
25
+ const { keys, create, assign, freeze, isFrozen } = Object;
26
26
 
27
27
  //Durable store error instrumentation key
28
28
  const DURABLE_STORE_ERROR = 'durable-store-error';
@@ -109,6 +109,74 @@ function publishDurableStoreEntries(durableRecords, put, publishMetadata) {
109
109
  }
110
110
  return { revivedKeys, hadUnexpectedShape };
111
111
  }
112
+ /**
113
+ * Extracts field filtering configuration from the selection.
114
+ */
115
+ function extractFieldFilteringConfig(select, recordId, baseEnvironment) {
116
+ const rootRecordKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(recordId));
117
+ let topLevelFields;
118
+ let nestedFieldRequirements;
119
+ if (select &&
120
+ select.node.kind === 'Fragment' &&
121
+ 'selections' in select.node &&
122
+ select.node.selections) {
123
+ topLevelFields = extractRequestedFieldNames(select.node.selections);
124
+ nestedFieldRequirements = extractNestedFieldRequirements(select.node.selections);
125
+ }
126
+ // Merge all nested field requirements into a single set
127
+ let nestedFields;
128
+ if (nestedFieldRequirements && nestedFieldRequirements.size > 0) {
129
+ nestedFields = new Set();
130
+ for (const fieldSet of nestedFieldRequirements.values()) {
131
+ for (const field of fieldSet) {
132
+ nestedFields.add(field);
133
+ }
134
+ }
135
+ }
136
+ return { rootRecordKey, topLevelFields, nestedFields };
137
+ }
138
+ /**
139
+ * Categorizes keys into different fetch strategies based on filtering requirements.
140
+ */
141
+ function categorizeKeysForL2Fetch(keysToRevive, config, baseEnvironment, shouldFilterFields) {
142
+ const unfilteredKeys = [];
143
+ const rootKeysWithTopLevelFields = [];
144
+ const nestedKeysWithNestedFields = [];
145
+ const canFilter = config.topLevelFields !== undefined && config.topLevelFields.size > 0;
146
+ for (let i = 0, len = keysToRevive.length; i < len; i += 1) {
147
+ const canonicalKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(keysToRevive[i]));
148
+ if (!shouldFilterFields(canonicalKey) || !canFilter) {
149
+ unfilteredKeys.push(canonicalKey);
150
+ continue;
151
+ }
152
+ const isRootRecord = canonicalKey === config.rootRecordKey;
153
+ if (isRootRecord) {
154
+ rootKeysWithTopLevelFields.push(canonicalKey);
155
+ }
156
+ else if (config.nestedFields !== undefined && config.nestedFields.size > 0) {
157
+ nestedKeysWithNestedFields.push(canonicalKey);
158
+ }
159
+ else {
160
+ unfilteredKeys.push(canonicalKey);
161
+ }
162
+ }
163
+ return { unfilteredKeys, rootKeysWithTopLevelFields, nestedKeysWithNestedFields };
164
+ }
165
+ /**
166
+ * Builds L2 fetch promises for different key categories.
167
+ */
168
+ function buildL2FetchPromises(categorizedKeys, config, durableStore) {
169
+ const promises = [
170
+ durableStore.getEntries(categorizedKeys.unfilteredKeys, DefaultDurableSegment),
171
+ ];
172
+ if (config.topLevelFields && categorizedKeys.rootKeysWithTopLevelFields.length > 0) {
173
+ promises.push(durableStore.getEntriesWithSpecificFields(categorizedKeys.rootKeysWithTopLevelFields, config.topLevelFields, DefaultDurableSegment));
174
+ }
175
+ if (config.nestedFields && categorizedKeys.nestedKeysWithNestedFields.length > 0) {
176
+ promises.push(durableStore.getEntriesWithSpecificFields(categorizedKeys.nestedKeysWithNestedFields, config.nestedFields, DefaultDurableSegment));
177
+ }
178
+ return promises;
179
+ }
112
180
  /**
113
181
  * This method returns a Promise to a snapshot that is revived from L2 cache. If
114
182
  * L2 does not have the entries necessary to fulfill the snapshot then this method
@@ -144,53 +212,41 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
144
212
  const keysToRevive = keysToReviveSet.keysAsArray();
145
213
  const start = Date.now();
146
214
  const { l2Trips } = reviveMetrics;
147
- // Extract requested fields first to determine if filtering is possible
148
- let requestedFields;
149
- if (select.node.kind === 'Fragment' && 'selections' in select.node && select.node.selections) {
150
- requestedFields = extractRequestedFieldNames(select.node.selections);
151
- }
152
- const canonicalKeys = [];
153
- const filteredCanonicalKeys = [];
154
- for (let i = 0, len = keysToRevive.length; i < len; i += 1) {
155
- const canonicalKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(keysToRevive[i]));
156
- // Only filter if we have fields to filter and shouldFilterFields returns true
157
- if (requestedFields !== undefined &&
158
- requestedFields.size > 0 &&
159
- shouldFilterFields(canonicalKey)) {
160
- filteredCanonicalKeys.push(canonicalKey);
161
- }
162
- else {
163
- canonicalKeys.push(canonicalKey);
164
- }
165
- }
166
- const fetchPromises = [
167
- durableStore.getEntries(canonicalKeys, DefaultDurableSegment),
168
- ];
169
- if (requestedFields !== undefined &&
170
- requestedFields.size > 0 &&
171
- filteredCanonicalKeys.length > 0) {
172
- fetchPromises.push(durableStore.getEntriesWithSpecificFields(filteredCanonicalKeys, requestedFields, DefaultDurableSegment));
173
- }
174
- return Promise.all(fetchPromises).then(([durableRecords, filteredDurableRecords]) => {
215
+ // Extract field filtering requirements from the selection
216
+ const fieldFilteringConfig = extractFieldFilteringConfig(select, recordId, baseEnvironment);
217
+ // Categorize keys by how they should be fetched from L2
218
+ const categorizedKeys = categorizeKeysForL2Fetch(keysToRevive, fieldFilteringConfig, baseEnvironment, shouldFilterFields);
219
+ // Build fetch promises for each category
220
+ const fetchPromises = buildL2FetchPromises(categorizedKeys, fieldFilteringConfig, durableStore);
221
+ return Promise.all(fetchPromises).then(([durableRecords, filteredDurableRecords, nestedFilteredDurableRecords]) => {
222
+ const totalKeysRequested = categorizedKeys.unfilteredKeys.length +
223
+ categorizedKeys.rootKeysWithTopLevelFields.length +
224
+ categorizedKeys.nestedKeysWithNestedFields.length;
175
225
  l2Trips.push({
176
226
  duration: Date.now() - start,
177
- keysRequestedCount: canonicalKeys.length + filteredCanonicalKeys.length,
227
+ keysRequestedCount: totalKeysRequested,
178
228
  });
179
- // Process both normal and filtered records in a single pass
229
+ // Process all three categories of records
180
230
  const revivedKeys = new StoreKeySet();
181
231
  let hadUnexpectedShape = false;
182
- // Process normal records
232
+ // Process normal records (all fields)
183
233
  if (durableRecords !== undefined) {
184
234
  const normalResult = publishDurableStoreEntries(durableRecords, baseEnvironment.storePut.bind(baseEnvironment), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
185
235
  revivedKeys.merge(normalResult.revivedKeys);
186
236
  hadUnexpectedShape = hadUnexpectedShape || normalResult.hadUnexpectedShape;
187
237
  }
188
- // Process filtered records with merging
238
+ // Process filtered records (root with top-level fields) with merging
189
239
  if (filteredDurableRecords !== undefined) {
190
240
  const filteredResult = publishDurableStoreEntries(filteredDurableRecords, createMergeFilteredPut((key) => baseEnvironment.store.readEntry(key), baseEnvironment.storePut.bind(baseEnvironment)), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
191
241
  revivedKeys.merge(filteredResult.revivedKeys);
192
242
  hadUnexpectedShape = hadUnexpectedShape || filteredResult.hadUnexpectedShape;
193
243
  }
244
+ // Process nested filtered records with merging
245
+ if (nestedFilteredDurableRecords !== undefined) {
246
+ const nestedFilteredResult = publishDurableStoreEntries(nestedFilteredDurableRecords, createMergeFilteredPut((key) => baseEnvironment.store.readEntry(key), baseEnvironment.storePut.bind(baseEnvironment)), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
247
+ revivedKeys.merge(nestedFilteredResult.revivedKeys);
248
+ hadUnexpectedShape = hadUnexpectedShape || nestedFilteredResult.hadUnexpectedShape;
249
+ }
194
250
  // if the data coming back from DS had an unexpected shape then just
195
251
  // return the L1 snapshot
196
252
  if (hadUnexpectedShape === true) {
@@ -236,6 +292,54 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
236
292
  return { snapshot: unavailableSnapshot, metrics: reviveMetrics };
237
293
  });
238
294
  }
295
+ /**
296
+ * Filters out null fields from a fields object (null indicates field doesn't exist in L2).
297
+ * Returns a new object without null values, or the original if no filtering needed.
298
+ */
299
+ function filterNullFields(fields) {
300
+ const keys$1 = keys(fields);
301
+ let hasNull = false;
302
+ // Check if any nulls exist before allocating new object
303
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
304
+ if (fields[keys$1[i]] === null) {
305
+ hasNull = true;
306
+ break;
307
+ }
308
+ }
309
+ if (!hasNull) {
310
+ return fields;
311
+ }
312
+ const cleaned = {};
313
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
314
+ const key = keys$1[i];
315
+ if (fields[key] !== null) {
316
+ cleaned[key] = fields[key];
317
+ }
318
+ }
319
+ return cleaned;
320
+ }
321
+ /**
322
+ * Merges new fields into existing fields object, skipping null values.
323
+ * Creates a new object to avoid mutations.
324
+ */
325
+ function mergeFieldsObjects(existing, incoming) {
326
+ const merged = { ...existing };
327
+ const keys$1 = keys(incoming);
328
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
329
+ const key = keys$1[i];
330
+ // Skip null values - they indicate the field doesn't exist in L2
331
+ if (incoming[key] !== null) {
332
+ merged[key] = incoming[key];
333
+ }
334
+ }
335
+ return merged;
336
+ }
337
+ /**
338
+ * Type guard to check if value is a non-null object.
339
+ */
340
+ function isObject(value) {
341
+ return typeof value === 'object' && value !== null;
342
+ }
239
343
  /**
240
344
  * Creates a put function that merges filtered fields with existing L1 records instead of replacing them.
241
345
  * This is used when reviving filtered fields from L2 to preserve existing data in L1.
@@ -243,28 +347,36 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
243
347
  function createMergeFilteredPut(readEntry, storePut) {
244
348
  return (key, filteredData) => {
245
349
  const existingRecord = readEntry(key);
246
- if (existingRecord !== undefined &&
247
- existingRecord !== null &&
248
- typeof filteredData === 'object' &&
249
- filteredData !== null &&
250
- typeof existingRecord === 'object') {
251
- const filteredFields = filteredData;
252
- const existingObj = existingRecord;
253
- // Check if object is frozen (can happen after deepFreeze)
254
- // If frozen, create a shallow copy to merge fields into
255
- let targetObj = existingObj;
256
- if (Object.isFrozen(existingObj)) {
257
- targetObj = { ...existingObj };
258
- }
259
- const keys = Object.keys(filteredFields);
260
- for (let i = 0, len = keys.length; i < len; i += 1) {
261
- const fieldKey = keys[i];
262
- targetObj[fieldKey] = filteredFields[fieldKey];
350
+ // Merge with existing record if both are objects
351
+ if (isObject(existingRecord) && isObject(filteredData)) {
352
+ // Create target object (copy if frozen to avoid mutation errors)
353
+ const targetObj = isFrozen(existingRecord)
354
+ ? { ...existingRecord }
355
+ : existingRecord;
356
+ const keys$1 = keys(filteredData);
357
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
358
+ const fieldKey = keys$1[i];
359
+ const incomingValue = filteredData[fieldKey];
360
+ // Special handling for 'fields' property to merge rather than replace
361
+ if (fieldKey === 'fields' &&
362
+ isObject(incomingValue) &&
363
+ isObject(targetObj[fieldKey])) {
364
+ targetObj[fieldKey] = mergeFieldsObjects(targetObj[fieldKey], incomingValue);
365
+ }
366
+ else {
367
+ targetObj[fieldKey] = incomingValue;
368
+ }
263
369
  }
264
370
  storePut(key, targetObj);
265
371
  }
266
372
  else {
267
- // No existing record, just put the filtered data
373
+ // No existing record - clean null fields before storing
374
+ if (isObject(filteredData) && 'fields' in filteredData) {
375
+ const fields = filteredData.fields;
376
+ if (isObject(fields)) {
377
+ filteredData.fields = filterNullFields(fields);
378
+ }
379
+ }
268
380
  storePut(key, filteredData);
269
381
  }
270
382
  };
@@ -278,16 +390,101 @@ function extractRequestedFieldNames(selections) {
278
390
  return undefined;
279
391
  }
280
392
  // Find the 'fields' ObjectSelection
281
- const fieldsSelection = selections.find((sel) => sel.kind === 'Object' && sel.name === 'fields');
282
- if (!fieldsSelection || !fieldsSelection.selections) {
393
+ let fieldsSelection;
394
+ for (let i = 0, len = selections.length; i < len; i += 1) {
395
+ const sel = selections[i];
396
+ if (sel.kind === 'Object' && sel.name === 'fields') {
397
+ fieldsSelection = sel;
398
+ break;
399
+ }
400
+ }
401
+ if (!fieldsSelection ||
402
+ !fieldsSelection.selections ||
403
+ fieldsSelection.selections.length === 0) {
283
404
  return undefined;
284
405
  }
285
406
  // Extract all field names from the fields selections
407
+ const fieldSelections = fieldsSelection.selections;
286
408
  const fieldNames = new Set();
287
- for (const fieldSel of fieldsSelection.selections) {
288
- fieldNames.add(fieldSel.name);
409
+ for (let i = 0, len = fieldSelections.length; i < len; i += 1) {
410
+ fieldNames.add(fieldSelections[i].name);
411
+ }
412
+ return fieldNames;
413
+ }
414
+ /**
415
+ * Extracts nested field requirements for spanning fields.
416
+ * For spanning fields like Case.CreatedBy.Name, we need to extract what fields
417
+ * are requested from the nested record (User in this case).
418
+ * The structure is: fields { CreatedBy { value (Link with fragment) { fields { Name } } } }
419
+ */
420
+ function extractNestedFieldRequirements(selections) {
421
+ if (!selections) {
422
+ return undefined;
423
+ }
424
+ // Find the 'fields' ObjectSelection
425
+ let fieldsSelection;
426
+ for (let i = 0, len = selections.length; i < len; i += 1) {
427
+ const sel = selections[i];
428
+ if (sel.kind === 'Object' && sel.name === 'fields') {
429
+ fieldsSelection = sel;
430
+ break;
431
+ }
432
+ }
433
+ if (!fieldsSelection || !fieldsSelection.selections) {
434
+ return undefined;
435
+ }
436
+ let nestedFieldsMap;
437
+ // Look for ObjectSelections within fields (these are spanning fields)
438
+ const fieldSelections = fieldsSelection.selections;
439
+ for (let i = 0, len = fieldSelections.length; i < len; i += 1) {
440
+ const fieldSel = fieldSelections[i];
441
+ if (fieldSel.kind !== 'Object') {
442
+ continue;
443
+ }
444
+ const objSel = fieldSel;
445
+ if (!objSel.selections) {
446
+ continue;
447
+ }
448
+ // Look for the 'value' Link selection
449
+ let valueLinkSelection;
450
+ for (let j = 0, jlen = objSel.selections.length; j < jlen; j += 1) {
451
+ const sel = objSel.selections[j];
452
+ if (sel.kind === 'Link' && sel.name === 'value') {
453
+ valueLinkSelection = sel;
454
+ break;
455
+ }
456
+ }
457
+ if (!valueLinkSelection || !('fragment' in valueLinkSelection)) {
458
+ continue;
459
+ }
460
+ const fragment = valueLinkSelection.fragment;
461
+ if (!fragment || !fragment.selections) {
462
+ continue;
463
+ }
464
+ // Look for the 'fields' selection within the fragment
465
+ let nestedFieldsSelection;
466
+ for (let j = 0, jlen = fragment.selections.length; j < jlen; j += 1) {
467
+ const sel = fragment.selections[j];
468
+ if (sel.kind === 'Object' && sel.name === 'fields') {
469
+ nestedFieldsSelection = sel;
470
+ break;
471
+ }
472
+ }
473
+ if (nestedFieldsSelection && nestedFieldsSelection.selections) {
474
+ const nestedFields = new Set();
475
+ for (const nestedFieldSel of nestedFieldsSelection.selections) {
476
+ nestedFields.add(nestedFieldSel.name);
477
+ }
478
+ if (nestedFields.size > 0) {
479
+ // Lazy initialize map only if we have nested fields
480
+ if (!nestedFieldsMap) {
481
+ nestedFieldsMap = new Map();
482
+ }
483
+ nestedFieldsMap.set(fieldSel.name, nestedFields);
484
+ }
485
+ }
289
486
  }
290
- return fieldNames.size > 0 ? fieldNames : undefined;
487
+ return nestedFieldsMap;
291
488
  }
292
489
 
293
490
  const TTL_DURABLE_SEGMENT = 'TTL_DURABLE_SEGMENT';
@@ -15,7 +15,7 @@ declare const keys: {
15
15
  [idx: string]: object | U | null | undefined;
16
16
  }, U extends string | number | bigint | boolean | symbol>(o: T_1): Readonly<T_1>;
17
17
  <T_2>(o: T_2): Readonly<T_2>;
18
- };
18
+ }, isFrozen: (o: any) => boolean;
19
19
  declare const hasOwnProperty: (v: PropertyKey) => boolean;
20
20
  declare const isArray: (arg: any) => arg is any[];
21
- export { create as ObjectCreate, keys as ObjectKeys, assign as ObjectAssign, freeze as ObjectFreeze, hasOwnProperty as ObjectPrototypeHasOwnProperty, isArray as ArrayIsArray, };
21
+ export { create as ObjectCreate, keys as ObjectKeys, assign as ObjectAssign, freeze as ObjectFreeze, isFrozen as ObjectIsFrozen, hasOwnProperty as ObjectPrototypeHasOwnProperty, isArray as ArrayIsArray, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luvio/environments",
3
- "version": "0.160.2",
3
+ "version": "0.160.3",
4
4
  "description": "Luvio Environments",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,7 @@
27
27
  "watch": "yarn build --watch"
28
28
  },
29
29
  "dependencies": {
30
- "@luvio/engine": "^0.160.2"
30
+ "@luvio/engine": "^0.160.3"
31
31
  },
32
32
  "volta": {
33
33
  "extends": "../../../package.json"
@@ -36,9 +36,9 @@
36
36
  {
37
37
  "path": "./dist/environments.js",
38
38
  "maxSize": {
39
- "none": "55.0 kB",
40
- "min": "15.5 kB",
41
- "compressed": "10.5 kB"
39
+ "none": "62.0 kB",
40
+ "min": "18.0 kB",
41
+ "compressed": "11.5 kB"
42
42
  }
43
43
  }
44
44
  ],