@luvio/environments 0.158.7 → 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.
package/dist/environments.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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": "
|
|
40
|
-
"min": "15.
|
|
41
|
-
"compressed": "10.
|
|
39
|
+
"none": "55.0 kB",
|
|
40
|
+
"min": "15.5 kB",
|
|
41
|
+
"compressed": "10.5 kB"
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
],
|