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