@luvio/environments 0.158.6 → 0.159.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.
@@ -115,7 +115,7 @@ function publishDurableStoreEntries(durableRecords, put, publishMetadata) {
115
115
  * will refresh the snapshot from network, and then run the results from network
116
116
  * through L2 ingestion, returning the subsequent revived snapshot.
117
117
  */
118
- function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, durableStoreErrorHandler, buildL1Snapshot, revivingStore, reviveMetrics = { l2Trips: [] }) {
118
+ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, durableStoreErrorHandler, buildL1Snapshot, revivingStore, reviveMetrics = { l2Trips: [] }, shouldFilterFields) {
119
119
  const { recordId, select, missingLinks, seenRecords, state } = unavailableSnapshot;
120
120
  // L2 can only revive Unfulfilled snapshots that have a selector since they have the
121
121
  // info needed to revive (like missingLinks) and rebuild. Otherwise return L1 snapshot.
@@ -142,19 +142,55 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
142
142
  }
143
143
  keysToReviveSet.merge(missingLinks);
144
144
  const keysToRevive = keysToReviveSet.keysAsArray();
145
- const canonicalKeys = keysToRevive.map((x) => serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(x)));
146
145
  const start = Date.now();
147
146
  const { l2Trips } = reviveMetrics;
148
- return durableStore.getEntries(canonicalKeys, DefaultDurableSegment).then((durableRecords) => {
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]) => {
149
175
  l2Trips.push({
150
176
  duration: Date.now() - start,
151
- keysRequestedCount: canonicalKeys.length,
177
+ keysRequestedCount: canonicalKeys.length + filteredCanonicalKeys.length,
152
178
  });
153
- const { revivedKeys, hadUnexpectedShape } = publishDurableStoreEntries(durableRecords,
154
- // TODO [W-10072584]: instead of implicitly using L1 we should take in
155
- // publish and publishMetadata funcs, so callers can decide where to
156
- // revive to (like they pass in how to do the buildL1Snapshot)
157
- baseEnvironment.storePut.bind(baseEnvironment), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
179
+ // Process both normal and filtered records in a single pass
180
+ const revivedKeys = new StoreKeySet();
181
+ let hadUnexpectedShape = false;
182
+ // Process normal records
183
+ if (durableRecords !== undefined) {
184
+ const normalResult = publishDurableStoreEntries(durableRecords, baseEnvironment.storePut.bind(baseEnvironment), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
185
+ revivedKeys.merge(normalResult.revivedKeys);
186
+ hadUnexpectedShape = hadUnexpectedShape || normalResult.hadUnexpectedShape;
187
+ }
188
+ // Process filtered records with merging
189
+ if (filteredDurableRecords !== undefined) {
190
+ const filteredResult = publishDurableStoreEntries(filteredDurableRecords, createMergeFilteredPut((key) => baseEnvironment.store.readEntry(key), baseEnvironment.storePut.bind(baseEnvironment)), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
191
+ revivedKeys.merge(filteredResult.revivedKeys);
192
+ hadUnexpectedShape = hadUnexpectedShape || filteredResult.hadUnexpectedShape;
193
+ }
158
194
  // if the data coming back from DS had an unexpected shape then just
159
195
  // return the L1 snapshot
160
196
  if (hadUnexpectedShape === true) {
@@ -189,7 +225,7 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
189
225
  for (let i = 0, len = newKeys.length; i < len; i++) {
190
226
  const newSnapshotSeenKey = newKeys[i];
191
227
  if (!alreadyRequestedOrRevivedSet.has(newSnapshotSeenKey)) {
192
- return reviveSnapshot(baseEnvironment, durableStore, snapshot, durableStoreErrorHandler, buildL1Snapshot, revivingStore, reviveMetrics);
228
+ return reviveSnapshot(baseEnvironment, durableStore, snapshot, durableStoreErrorHandler, buildL1Snapshot, revivingStore, reviveMetrics, shouldFilterFields);
193
229
  }
194
230
  }
195
231
  }
@@ -200,6 +236,59 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
200
236
  return { snapshot: unavailableSnapshot, metrics: reviveMetrics };
201
237
  });
202
238
  }
239
+ /**
240
+ * Creates a put function that merges filtered fields with existing L1 records instead of replacing them.
241
+ * This is used when reviving filtered fields from L2 to preserve existing data in L1.
242
+ */
243
+ function createMergeFilteredPut(readEntry, storePut) {
244
+ return (key, filteredData) => {
245
+ 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];
263
+ }
264
+ storePut(key, targetObj);
265
+ }
266
+ else {
267
+ // No existing record, just put the filtered data
268
+ storePut(key, filteredData);
269
+ }
270
+ };
271
+ }
272
+ /**
273
+ * Extracts the requested field names from the selections of a 'fields' ObjectSelection.
274
+ * Returns undefined if no 'fields' selection is found or if it doesn't have selections.
275
+ */
276
+ function extractRequestedFieldNames(selections) {
277
+ if (!selections) {
278
+ return undefined;
279
+ }
280
+ // Find the 'fields' ObjectSelection
281
+ const fieldsSelection = selections.find((sel) => sel.kind === 'Object' && sel.name === 'fields');
282
+ if (!fieldsSelection || !fieldsSelection.selections) {
283
+ return undefined;
284
+ }
285
+ // Extract all field names from the fields selections
286
+ const fieldNames = new Set();
287
+ for (const fieldSel of fieldsSelection.selections) {
288
+ fieldNames.add(fieldSel.name);
289
+ }
290
+ return fieldNames.size > 0 ? fieldNames : undefined;
291
+ }
203
292
 
204
293
  const TTL_DURABLE_SEGMENT = 'TTL_DURABLE_SEGMENT';
205
294
  const TTL_DEFAULT_KEY = 'TTL_DEFAULT_KEY';
@@ -519,7 +608,7 @@ function isUnfulfilledSnapshot(cachedSnapshotResult) {
519
608
  * @param durableStore A DurableStore implementation
520
609
  * @param instrumentation An instrumentation function implementation
521
610
  */
522
- function makeDurable(environment, { durableStore, instrumentation, useRevivingStore, shouldFlush, enableDurableMetadataRefresh = false, disableDeepFreeze = false, }) {
611
+ function makeDurable(environment, { durableStore, instrumentation, useRevivingStore, shouldFlush, enableDurableMetadataRefresh = false, disableDeepFreeze = false, shouldFilterFieldsOnRevive, }) {
523
612
  // runtimes can choose to disable deepFreeze, e.g. headless mobile runtime
524
613
  setBypassDeepFreeze(disableDeepFreeze);
525
614
  let stagingStore = null;
@@ -1061,7 +1150,7 @@ function makeDurable(environment, { durableStore, instrumentation, useRevivingSt
1061
1150
  const result = buildL1Snapshot();
1062
1151
  stagingStore = tempStore;
1063
1152
  return result;
1064
- }, revivingStore).finally(() => {
1153
+ }, revivingStore, { l2Trips: [] }, shouldFilterFieldsOnRevive !== null && shouldFilterFieldsOnRevive !== void 0 ? shouldFilterFieldsOnRevive : (() => false)).finally(() => {
1065
1154
  });
1066
1155
  };
1067
1156
  const expirePossibleStaleRecords = async function (keys$1, config, refresh) {
@@ -102,6 +102,41 @@ export interface DurableStore {
102
102
  * @returns {Promise<DurableStoreEntries | undefined>}
103
103
  */
104
104
  getEntries<T>(entryIds: string[], segment: string): Promise<DurableStoreEntries<T> | undefined>;
105
+ /**
106
+ * Given a list of cache entryIds and a set of field names, this method asynchronously
107
+ * returns DurableStoreEntries with only the specified fields included in the data.
108
+ *
109
+ * This overload allows for selective field retrieval, which can improve performance
110
+ * when only a subset of fields is needed from large entries. The fields set specifies
111
+ * which top-level properties should be included in the returned data objects.
112
+ *
113
+ * If the segment isn't found this will return undefined.
114
+ *
115
+ * If the segment is found this will return a map of results, even if not all entries
116
+ * are present in the DS. Each entry's data will contain only the fields specified in
117
+ * the fields parameter.
118
+ *
119
+ * Only the store segment specified is queried.
120
+ *
121
+ * @param {string[]} entryIds - The list of entry IDs to retrieve
122
+ * @param {Set<string>} fields - A set of field names to include in the returned data.
123
+ * Only these top-level fields will be present in each
124
+ * entry's data object. If empty, no data fields are returned.
125
+ * @param {string} segment - The durable store segment to query
126
+ * @returns {Promise<DurableStoreEntries | undefined>} A promise resolving to entries
127
+ * with filtered data, or undefined
128
+ * if the segment is not found
129
+ *
130
+ * @example
131
+ * // Retrieve only 'id' and 'name' fields from user entries
132
+ * const entries = await durableStore.getEntries(
133
+ * ['user1', 'user2'],
134
+ * new Set(['id', 'name']),
135
+ * 'DEFAULT'
136
+ * );
137
+ * // entries['user1'].data will only contain { id: '...', name: '...' }
138
+ */
139
+ getEntriesWithSpecificFields<T>(entryIds: string[], fields: Set<string>, segment: string): Promise<DurableStoreEntries<T> | undefined>;
105
140
  /**
106
141
  * Given alist of cache entryId's this method will asynchronously return only the DurableStoreMetadata
107
142
  * of DurableStoreEntries
@@ -7,6 +7,7 @@ type ReviveResponse = {
7
7
  revivedKeys: StoreKeySet<string | NormalizedKeyMetadata>;
8
8
  hadUnexpectedShape: boolean;
9
9
  };
10
+ type ShouldReviveFilterFields = (recordId: string) => boolean;
10
11
  /**
11
12
  * Takes a set of entries from DurableStore and publishes them via the passed in funcs.
12
13
  * This respects expiration and checks for valid DurableStore data shapes. This should
@@ -35,5 +36,5 @@ export interface ReviveResult<D, V> {
35
36
  * will refresh the snapshot from network, and then run the results from network
36
37
  * through L2 ingestion, returning the subsequent revived snapshot.
37
38
  */
38
- export declare function reviveSnapshot<D, V = unknown>(baseEnvironment: Environment, durableStore: DurableStore, unavailableSnapshot: UnfulfilledSnapshot<D, V>, durableStoreErrorHandler: DurableStoreRejectionHandler, buildL1Snapshot: () => Snapshot<D, V>, revivingStore: RevivingStore | undefined, reviveMetrics?: ReviveMetrics): Promise<ReviveResult<D, V>>;
39
+ export declare function reviveSnapshot<D, V = unknown>(baseEnvironment: Environment, durableStore: DurableStore, unavailableSnapshot: UnfulfilledSnapshot<D, V>, durableStoreErrorHandler: DurableStoreRejectionHandler, buildL1Snapshot: () => Snapshot<D, V>, revivingStore: RevivingStore | undefined, reviveMetrics: ReviveMetrics | undefined, shouldFilterFields: ShouldReviveFilterFields): Promise<ReviveResult<D, V>>;
39
40
  export {};
@@ -44,6 +44,7 @@ interface MakeDurableOptions {
44
44
  useRevivingStore?: boolean;
45
45
  disableDeepFreeze?: boolean;
46
46
  shouldFlush?: ShouldFlushFn;
47
+ shouldFilterFieldsOnRevive?: (key: string) => boolean;
47
48
  }
48
49
  /**
49
50
  * Configures the environment to persist data into a durable store and attempt to resolve
@@ -54,5 +55,5 @@ interface MakeDurableOptions {
54
55
  * @param durableStore A DurableStore implementation
55
56
  * @param instrumentation An instrumentation function implementation
56
57
  */
57
- export declare function makeDurable(environment: Environment, { durableStore, instrumentation, useRevivingStore, shouldFlush, enableDurableMetadataRefresh, disableDeepFreeze, }: MakeDurableOptions): DurableEnvironment;
58
+ export declare function makeDurable(environment: Environment, { durableStore, instrumentation, useRevivingStore, shouldFlush, enableDurableMetadataRefresh, disableDeepFreeze, shouldFilterFieldsOnRevive, }: MakeDurableOptions): DurableEnvironment;
58
59
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luvio/environments",
3
- "version": "0.158.6",
3
+ "version": "0.159.0",
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.158.6"
30
+ "@luvio/engine": "^0.159.0"
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": "50.0 kB",
40
- "min": "15.0 kB",
41
- "compressed": "10.0 kB"
39
+ "none": "55.0 kB",
40
+ "min": "15.5 kB",
41
+ "compressed": "10.5 kB"
42
42
  }
43
43
  }
44
44
  ],