@spoosh/plugin-optimistic 0.7.2 → 0.8.1

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