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