@spoosh/plugin-optimistic 0.7.2 → 0.8.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/README.md +40 -39
- package/dist/index.d.mts +103 -103
- package/dist/index.d.ts +103 -103
- package/dist/index.js +157 -164
- package/dist/index.mjs +155 -162
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -3,40 +3,14 @@ import {
|
|
|
3
3
|
createSpooshPlugin
|
|
4
4
|
} from "@spoosh/core";
|
|
5
5
|
import "@spoosh/plugin-invalidation";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
...state,
|
|
9
|
-
WHERE(predicate) {
|
|
10
|
-
return createBuilder({ ...state, where: predicate });
|
|
11
|
-
},
|
|
12
|
-
UPDATE_CACHE(updater) {
|
|
13
|
-
return createBuilder({ ...state, updater });
|
|
14
|
-
},
|
|
15
|
-
ON_SUCCESS() {
|
|
16
|
-
return createBuilder({ ...state, timing: "onSuccess" });
|
|
17
|
-
},
|
|
18
|
-
NO_ROLLBACK() {
|
|
19
|
-
return createBuilder({ ...state, rollbackOnError: false });
|
|
20
|
-
},
|
|
21
|
-
ON_ERROR(callback) {
|
|
22
|
-
return createBuilder({ ...state, onError: callback });
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function createOptimisticProxy() {
|
|
27
|
-
const createMethodsProxy = (path) => ({
|
|
28
|
-
GET: () => createBuilder({
|
|
29
|
-
path,
|
|
30
|
-
method: "GET",
|
|
31
|
-
timing: "immediate",
|
|
32
|
-
rollbackOnError: true
|
|
33
|
-
})
|
|
34
|
-
});
|
|
35
|
-
return ((path) => createMethodsProxy(path));
|
|
36
|
-
}
|
|
6
|
+
|
|
7
|
+
// src/utils/path.ts
|
|
37
8
|
function isParameterSegment(segment) {
|
|
38
9
|
return segment.startsWith(":");
|
|
39
10
|
}
|
|
11
|
+
function hasPatternParams(path) {
|
|
12
|
+
return path.split("/").some(isParameterSegment);
|
|
13
|
+
}
|
|
40
14
|
function pathMatchesPattern(actualPath, pattern) {
|
|
41
15
|
const actualSegments = actualPath.split("/").filter(Boolean);
|
|
42
16
|
const patternSegments = pattern.split("/").filter(Boolean);
|
|
@@ -64,9 +38,9 @@ function pathMatchesPattern(actualPath, pattern) {
|
|
|
64
38
|
}
|
|
65
39
|
return { matches: true, params, paramMapping };
|
|
66
40
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
41
|
+
|
|
42
|
+
// src/utils/cache-key.ts
|
|
43
|
+
import { generateSelfTagFromKey } from "@spoosh/core";
|
|
70
44
|
function extractPathFromKey(key) {
|
|
71
45
|
try {
|
|
72
46
|
const parsed = JSON.parse(key);
|
|
@@ -74,11 +48,39 @@ function extractPathFromKey(key) {
|
|
|
74
48
|
if (typeof path === "string") {
|
|
75
49
|
return path;
|
|
76
50
|
}
|
|
51
|
+
if (Array.isArray(path)) {
|
|
52
|
+
return path.join("/");
|
|
53
|
+
}
|
|
77
54
|
return null;
|
|
78
55
|
} catch {
|
|
79
56
|
return null;
|
|
80
57
|
}
|
|
81
58
|
}
|
|
59
|
+
function formatCacheKeyForTrace(key) {
|
|
60
|
+
const resolvedPath = generateSelfTagFromKey(key);
|
|
61
|
+
if (!resolvedPath) return "unknown";
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(key);
|
|
64
|
+
const opts = parsed.options ?? parsed.pageRequest;
|
|
65
|
+
const query = opts?.query;
|
|
66
|
+
if (!query || Object.keys(query).length === 0) {
|
|
67
|
+
return resolvedPath;
|
|
68
|
+
}
|
|
69
|
+
const queryString = Object.entries(query).map(
|
|
70
|
+
([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`
|
|
71
|
+
).join("&");
|
|
72
|
+
return `${resolvedPath}?${queryString}`;
|
|
73
|
+
} catch {
|
|
74
|
+
return resolvedPath;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function stringifyParams(params) {
|
|
78
|
+
const result = {};
|
|
79
|
+
for (const [key, value] of Object.entries(params)) {
|
|
80
|
+
result[key] = String(value);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
82
84
|
function extractOptionsFromKey(key) {
|
|
83
85
|
try {
|
|
84
86
|
const parsed = JSON.parse(key);
|
|
@@ -89,7 +91,7 @@ function extractOptionsFromKey(key) {
|
|
|
89
91
|
result.query = opts.query;
|
|
90
92
|
}
|
|
91
93
|
if (opts.params) {
|
|
92
|
-
result.params = opts.params;
|
|
94
|
+
result.params = stringifyParams(opts.params);
|
|
93
95
|
}
|
|
94
96
|
return Object.keys(result).length > 0 ? result : null;
|
|
95
97
|
} catch {
|
|
@@ -101,30 +103,75 @@ function mapParamsToTargetNames(actualParams, paramMapping) {
|
|
|
101
103
|
const result = {};
|
|
102
104
|
for (const [targetName, actualName] of Object.entries(paramMapping)) {
|
|
103
105
|
if (actualName in actualParams) {
|
|
104
|
-
result[targetName] = actualParams[actualName];
|
|
106
|
+
result[targetName] = String(actualParams[actualName]);
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
for (const [key, value] of Object.entries(actualParams)) {
|
|
108
110
|
if (!Object.values(paramMapping).includes(key)) {
|
|
109
|
-
result[key] = value;
|
|
111
|
+
result[key] = String(value);
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
return result;
|
|
113
115
|
}
|
|
116
|
+
|
|
117
|
+
// src/builder/index.ts
|
|
118
|
+
function createBuilder(state, isConfirmed = false) {
|
|
119
|
+
return {
|
|
120
|
+
...state,
|
|
121
|
+
filter(predicate) {
|
|
122
|
+
return createBuilder({ ...state, filter: predicate }, isConfirmed);
|
|
123
|
+
},
|
|
124
|
+
set(updater) {
|
|
125
|
+
if (isConfirmed) {
|
|
126
|
+
return createBuilder(
|
|
127
|
+
{
|
|
128
|
+
...state,
|
|
129
|
+
confirmedUpdater: updater
|
|
130
|
+
},
|
|
131
|
+
isConfirmed
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return createBuilder(
|
|
135
|
+
{
|
|
136
|
+
...state,
|
|
137
|
+
immediateUpdater: updater
|
|
138
|
+
},
|
|
139
|
+
isConfirmed
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
confirmed() {
|
|
143
|
+
return createBuilder(state, true);
|
|
144
|
+
},
|
|
145
|
+
disableRollback() {
|
|
146
|
+
return createBuilder({ ...state, rollbackOnError: false }, isConfirmed);
|
|
147
|
+
},
|
|
148
|
+
onError(callback) {
|
|
149
|
+
return createBuilder({ ...state, onError: callback }, isConfirmed);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function createCacheProxy() {
|
|
154
|
+
return ((path) => createBuilder({
|
|
155
|
+
path,
|
|
156
|
+
rollbackOnError: true
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/plugin.ts
|
|
114
161
|
function resolveOptimisticTargets(context) {
|
|
115
162
|
const pluginOptions = context.pluginOptions;
|
|
116
163
|
if (!pluginOptions?.optimistic) return [];
|
|
117
|
-
const
|
|
118
|
-
const result = pluginOptions.optimistic(
|
|
164
|
+
const cacheProxy = createCacheProxy();
|
|
165
|
+
const result = pluginOptions.optimistic(cacheProxy);
|
|
119
166
|
const targets = Array.isArray(result) ? result : [result];
|
|
120
167
|
return targets;
|
|
121
168
|
}
|
|
122
|
-
function getMatchingEntries(stateManager, targetPath
|
|
169
|
+
function getMatchingEntries(stateManager, targetPath) {
|
|
123
170
|
const results = [];
|
|
171
|
+
const allEntries = stateManager.getAllCacheEntries();
|
|
124
172
|
if (hasPatternParams(targetPath)) {
|
|
125
|
-
const allEntries = stateManager.getAllCacheEntries();
|
|
126
173
|
for (const { key, entry } of allEntries) {
|
|
127
|
-
if (!key.includes(`"method":"
|
|
174
|
+
if (!key.includes(`"method":"GET"`)) continue;
|
|
128
175
|
const actualPath = extractPathFromKey(key);
|
|
129
176
|
if (!actualPath) continue;
|
|
130
177
|
const { matches, params, paramMapping } = pathMatchesPattern(
|
|
@@ -136,36 +183,26 @@ function getMatchingEntries(stateManager, targetPath, targetMethod) {
|
|
|
136
183
|
}
|
|
137
184
|
}
|
|
138
185
|
} else {
|
|
139
|
-
const allEntries = stateManager.getAllCacheEntries();
|
|
140
186
|
for (const { key, entry } of allEntries) {
|
|
141
|
-
if (!key.includes(`"method":"
|
|
187
|
+
if (!key.includes(`"method":"GET"`)) continue;
|
|
142
188
|
const actualPath = extractPathFromKey(key);
|
|
143
189
|
if (!actualPath) continue;
|
|
144
190
|
if (actualPath === targetPath) {
|
|
145
191
|
results.push({ key, entry, extractedParams: {}, paramMapping: {} });
|
|
146
|
-
} else if (hasPatternParams(actualPath)) {
|
|
147
|
-
const { matches, params, paramMapping } = pathMatchesPattern(
|
|
148
|
-
targetPath,
|
|
149
|
-
actualPath
|
|
150
|
-
);
|
|
151
|
-
if (matches) {
|
|
152
|
-
results.push({ key, entry, extractedParams: params, paramMapping });
|
|
153
|
-
}
|
|
154
192
|
}
|
|
155
193
|
}
|
|
156
194
|
}
|
|
157
195
|
return results;
|
|
158
196
|
}
|
|
159
|
-
function
|
|
160
|
-
if (!target.updater) return [];
|
|
197
|
+
function applyUpdate(stateManager, target, updater, response, t) {
|
|
161
198
|
const snapshots = [];
|
|
162
|
-
const matchingEntries = getMatchingEntries(
|
|
163
|
-
|
|
164
|
-
target.path
|
|
165
|
-
|
|
166
|
-
|
|
199
|
+
const matchingEntries = getMatchingEntries(stateManager, target.path);
|
|
200
|
+
if (matchingEntries.length === 0) {
|
|
201
|
+
t?.skip(`Skipped ${target.path} (no cache entry)`);
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
167
204
|
for (const { key, entry, extractedParams, paramMapping } of matchingEntries) {
|
|
168
|
-
if (target.
|
|
205
|
+
if (target.filter) {
|
|
169
206
|
const options = extractOptionsFromKey(key) ?? {};
|
|
170
207
|
const mappedParams = mapParamsToTargetNames(
|
|
171
208
|
options.params,
|
|
@@ -178,14 +215,23 @@ function applyOptimisticUpdate(stateManager, target, t) {
|
|
|
178
215
|
...mappedParams
|
|
179
216
|
}
|
|
180
217
|
};
|
|
181
|
-
|
|
218
|
+
try {
|
|
219
|
+
if (!target.filter(mergedOptions)) {
|
|
220
|
+
t?.skip(
|
|
221
|
+
`Skipped ${formatCacheKeyForTrace(key)} (filter not matched)`
|
|
222
|
+
);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
t?.skip(`Skipped ${formatCacheKeyForTrace(key)} (filter error)`);
|
|
182
227
|
continue;
|
|
183
228
|
}
|
|
184
229
|
}
|
|
185
230
|
if (entry?.state.data === void 0) {
|
|
231
|
+
t?.skip(`Skipped ${formatCacheKeyForTrace(key)} (no cached data)`);
|
|
186
232
|
continue;
|
|
187
233
|
}
|
|
188
|
-
const afterData =
|
|
234
|
+
const afterData = updater(entry.state.data, response);
|
|
189
235
|
snapshots.push({ key, previousData: entry.state.data, afterData });
|
|
190
236
|
stateManager.setCache(key, {
|
|
191
237
|
previousData: entry.state.data,
|
|
@@ -195,14 +241,10 @@ function applyOptimisticUpdate(stateManager, target, t) {
|
|
|
195
241
|
}
|
|
196
242
|
});
|
|
197
243
|
stateManager.setMeta(key, { isOptimistic: true });
|
|
198
|
-
t?.log("Marked as optimistic", {
|
|
199
|
-
color: "info",
|
|
200
|
-
info: [{ value: { isOptimistic: true } }]
|
|
201
|
-
});
|
|
202
244
|
}
|
|
203
245
|
return snapshots;
|
|
204
246
|
}
|
|
205
|
-
function confirmOptimistic(stateManager, snapshots
|
|
247
|
+
function confirmOptimistic(stateManager, snapshots) {
|
|
206
248
|
for (const { key } of snapshots) {
|
|
207
249
|
const entry = stateManager.getCache(key);
|
|
208
250
|
if (entry) {
|
|
@@ -210,14 +252,10 @@ function confirmOptimistic(stateManager, snapshots, t) {
|
|
|
210
252
|
previousData: void 0
|
|
211
253
|
});
|
|
212
254
|
stateManager.setMeta(key, { isOptimistic: false });
|
|
213
|
-
t?.log("Optimistic confirmed", {
|
|
214
|
-
color: "success",
|
|
215
|
-
info: [{ value: { isOptimistic: false } }]
|
|
216
|
-
});
|
|
217
255
|
}
|
|
218
256
|
}
|
|
219
257
|
}
|
|
220
|
-
function rollbackOptimistic(stateManager, snapshots
|
|
258
|
+
function rollbackOptimistic(stateManager, snapshots) {
|
|
221
259
|
for (const { key, previousData } of snapshots) {
|
|
222
260
|
const entry = stateManager.getCache(key);
|
|
223
261
|
if (entry) {
|
|
@@ -229,28 +267,12 @@ function rollbackOptimistic(stateManager, snapshots, t) {
|
|
|
229
267
|
}
|
|
230
268
|
});
|
|
231
269
|
stateManager.setMeta(key, { isOptimistic: false });
|
|
232
|
-
t?.log("Optimistic rolled back", {
|
|
233
|
-
color: "warning",
|
|
234
|
-
info: [{ value: { isOptimistic: false } }]
|
|
235
|
-
});
|
|
236
270
|
}
|
|
237
271
|
}
|
|
238
272
|
}
|
|
239
|
-
function
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
if (snapshots.length === 1) {
|
|
243
|
-
return mode === "apply" || mode === "onSuccess" ? { before: first.previousData, after: first.afterData, label } : { before: first.afterData, after: first.previousData, label };
|
|
244
|
-
}
|
|
245
|
-
return mode === "apply" || mode === "onSuccess" ? {
|
|
246
|
-
before: snapshots.map((s) => ({ key: s.key, data: s.previousData })),
|
|
247
|
-
after: snapshots.map((s) => ({ key: s.key, data: s.afterData })),
|
|
248
|
-
label
|
|
249
|
-
} : {
|
|
250
|
-
before: snapshots.map((s) => ({ key: s.key, data: s.afterData })),
|
|
251
|
-
after: snapshots.map((s) => ({ key: s.key, data: s.previousData })),
|
|
252
|
-
label
|
|
253
|
-
};
|
|
273
|
+
function buildSingleDiff(snapshot, mode = "apply") {
|
|
274
|
+
const label = mode === "apply" ? "Optimistic update" : mode === "rollback" ? "Rollback optimistic" : "Confirmed update";
|
|
275
|
+
return mode === "apply" || mode === "confirmed" ? { before: snapshot.previousData, after: snapshot.afterData, label } : { before: snapshot.afterData, after: snapshot.previousData, label };
|
|
254
276
|
}
|
|
255
277
|
var PLUGIN_NAME = "spoosh:optimistic";
|
|
256
278
|
function optimisticPlugin() {
|
|
@@ -267,28 +289,37 @@ function optimisticPlugin() {
|
|
|
267
289
|
return next();
|
|
268
290
|
}
|
|
269
291
|
context.plugins.get("spoosh:invalidation")?.setDefaultMode("none");
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
const snapshots =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
const allImmediateSnapshots = [];
|
|
293
|
+
for (const target of targets) {
|
|
294
|
+
if (!target.immediateUpdater) continue;
|
|
295
|
+
const snapshots = applyUpdate(
|
|
296
|
+
stateManager,
|
|
297
|
+
target,
|
|
298
|
+
target.immediateUpdater,
|
|
299
|
+
void 0,
|
|
300
|
+
t
|
|
301
|
+
);
|
|
302
|
+
for (const snapshot of snapshots) {
|
|
303
|
+
t?.log(
|
|
304
|
+
`Applied optimistic update to ${formatCacheKeyForTrace(snapshot.key)}`,
|
|
305
|
+
{ diff: buildSingleDiff(snapshot) }
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
allImmediateSnapshots.push(...snapshots);
|
|
280
309
|
}
|
|
281
310
|
const response = await next();
|
|
282
311
|
if (response.error) {
|
|
283
312
|
const shouldRollback = targets.some(
|
|
284
|
-
(
|
|
313
|
+
(target) => target.rollbackOnError && target.immediateUpdater
|
|
285
314
|
);
|
|
286
|
-
if (shouldRollback &&
|
|
287
|
-
rollbackOptimistic(stateManager,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
315
|
+
if (shouldRollback && allImmediateSnapshots.length > 0) {
|
|
316
|
+
rollbackOptimistic(stateManager, allImmediateSnapshots);
|
|
317
|
+
for (const snapshot of allImmediateSnapshots) {
|
|
318
|
+
t?.log(`Rolled back ${formatCacheKeyForTrace(snapshot.key)}`, {
|
|
319
|
+
color: "warning",
|
|
320
|
+
diff: buildSingleDiff(snapshot, "rollback")
|
|
321
|
+
});
|
|
322
|
+
}
|
|
292
323
|
}
|
|
293
324
|
for (const target of targets) {
|
|
294
325
|
if (target.onError) {
|
|
@@ -296,66 +327,28 @@ function optimisticPlugin() {
|
|
|
296
327
|
}
|
|
297
328
|
}
|
|
298
329
|
} else {
|
|
299
|
-
if (
|
|
300
|
-
confirmOptimistic(stateManager,
|
|
330
|
+
if (allImmediateSnapshots.length > 0) {
|
|
331
|
+
confirmOptimistic(stateManager, allImmediateSnapshots);
|
|
301
332
|
}
|
|
302
|
-
const
|
|
303
|
-
(target)
|
|
304
|
-
|
|
305
|
-
const onSuccessSnapshots = [];
|
|
306
|
-
for (const target of onSuccessTargets) {
|
|
307
|
-
if (!target.updater) continue;
|
|
308
|
-
const matchingEntries = getMatchingEntries(
|
|
333
|
+
for (const target of targets) {
|
|
334
|
+
if (!target.confirmedUpdater) continue;
|
|
335
|
+
const snapshots = applyUpdate(
|
|
309
336
|
stateManager,
|
|
310
|
-
target
|
|
311
|
-
target.
|
|
337
|
+
target,
|
|
338
|
+
target.confirmedUpdater,
|
|
339
|
+
response.data,
|
|
340
|
+
t
|
|
312
341
|
);
|
|
313
|
-
for (const {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (target.where) {
|
|
320
|
-
const options = extractOptionsFromKey(key) ?? {};
|
|
321
|
-
const mappedParams = mapParamsToTargetNames(
|
|
322
|
-
options.params,
|
|
323
|
-
paramMapping
|
|
324
|
-
);
|
|
325
|
-
const mergedOptions = {
|
|
326
|
-
...options,
|
|
327
|
-
params: {
|
|
328
|
-
...extractedParams,
|
|
329
|
-
...mappedParams
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
if (!target.where(mergedOptions)) {
|
|
333
|
-
continue;
|
|
342
|
+
for (const snapshot of snapshots) {
|
|
343
|
+
t?.log(
|
|
344
|
+
`Applied confirmed update to ${formatCacheKeyForTrace(snapshot.key)}`,
|
|
345
|
+
{
|
|
346
|
+
color: "success",
|
|
347
|
+
diff: buildSingleDiff(snapshot, "confirmed")
|
|
334
348
|
}
|
|
335
|
-
|
|
336
|
-
if (entry?.state.data === void 0) {
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
const afterData = target.updater(entry.state.data, response.data);
|
|
340
|
-
onSuccessSnapshots.push({
|
|
341
|
-
key,
|
|
342
|
-
previousData: entry.state.data,
|
|
343
|
-
afterData
|
|
344
|
-
});
|
|
345
|
-
stateManager.setCache(key, {
|
|
346
|
-
state: {
|
|
347
|
-
...entry.state,
|
|
348
|
-
data: afterData
|
|
349
|
-
}
|
|
350
|
-
});
|
|
349
|
+
);
|
|
351
350
|
}
|
|
352
351
|
}
|
|
353
|
-
if (onSuccessSnapshots.length > 0) {
|
|
354
|
-
t?.log(`Applied ${onSuccessSnapshots.length} onSuccess update(s)`, {
|
|
355
|
-
color: "success",
|
|
356
|
-
diff: buildSnapshotDiff(onSuccessSnapshots, "onSuccess")
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
352
|
}
|
|
360
353
|
return response;
|
|
361
354
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spoosh/plugin-optimistic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Optimistic updates plugin for Spoosh - instant UI updates with automatic rollback",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@spoosh/core": ">=0.
|
|
36
|
+
"@spoosh/core": ">=0.17.0",
|
|
37
37
|
"@spoosh/plugin-invalidation": ">=0.7.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@spoosh/core": "0.
|
|
41
|
-
"@spoosh/plugin-invalidation": "0.
|
|
40
|
+
"@spoosh/core": "0.17.0",
|
|
41
|
+
"@spoosh/plugin-invalidation": "0.10.0",
|
|
42
42
|
"@spoosh/test-utils": "0.3.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|