@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/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 import_core = require("@spoosh/core");
28
+ var import_core2 = require("@spoosh/core");
29
29
  var import_plugin_invalidation = require("@spoosh/plugin-invalidation");
30
- function createBuilder(state) {
31
- return {
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
- function hasPatternParams(path) {
92
- return path.split("/").some(isParameterSegment);
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 apiProxy = createOptimisticProxy();
142
- const result = pluginOptions.optimistic(apiProxy);
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, targetMethod) {
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":"${targetMethod}"`)) continue;
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":"${targetMethod}"`)) continue;
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 applyOptimisticUpdate(stateManager, target, t) {
184
- if (!target.updater) return [];
221
+ function applyUpdate(stateManager, target, updater, response, t) {
185
222
  const snapshots = [];
186
- const matchingEntries = getMatchingEntries(
187
- stateManager,
188
- target.path,
189
- target.method
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.where) {
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
- if (!target.where(mergedOptions)) {
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 = target.updater(entry.state.data, void 0);
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, t) {
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, t) {
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 buildSnapshotDiff(snapshots, mode = "apply") {
264
- const first = snapshots[0];
265
- const label = mode === "apply" ? "Optimistic update to cache" : mode === "rollback" ? "Rollback optimistic changes" : "Apply onSuccess updates";
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, import_core.createSpooshPlugin)({
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 immediateTargets = targets.filter((t2) => t2.timing !== "onSuccess");
295
- const allSnapshots = [];
296
- for (const target of immediateTargets) {
297
- const snapshots = applyOptimisticUpdate(stateManager, target, t);
298
- allSnapshots.push(...snapshots);
299
- }
300
- if (allSnapshots.length > 0) {
301
- t?.log(`Applied ${allSnapshots.length} immediate update(s)`, {
302
- diff: buildSnapshotDiff(allSnapshots)
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
- (t2) => t2.rollbackOnError && t2.timing !== "onSuccess"
337
+ (target) => target.rollbackOnError && target.immediateUpdater
309
338
  );
310
- if (shouldRollback && allSnapshots.length > 0) {
311
- rollbackOptimistic(stateManager, allSnapshots, t);
312
- t?.log(`Rolled back ${allSnapshots.length} update(s)`, {
313
- color: "warning",
314
- diff: buildSnapshotDiff(allSnapshots, "rollback")
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 (allSnapshots.length > 0) {
324
- confirmOptimistic(stateManager, allSnapshots, t);
354
+ if (allImmediateSnapshots.length > 0) {
355
+ confirmOptimistic(stateManager, allImmediateSnapshots);
325
356
  }
326
- const onSuccessTargets = targets.filter(
327
- (target) => target.timing === "onSuccess"
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.path,
335
- target.method
361
+ target,
362
+ target.confirmedUpdater,
363
+ response.data,
364
+ t
336
365
  );
337
- for (const {
338
- key,
339
- entry,
340
- extractedParams,
341
- paramMapping
342
- } of matchingEntries) {
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
  }