@spoosh/plugin-optimistic 0.4.3 → 0.5.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 CHANGED
@@ -22,7 +22,7 @@ import { Spoosh } from "@spoosh/core";
22
22
  import { optimisticPlugin } from "@spoosh/plugin-optimistic";
23
23
  import { invalidationPlugin } from "@spoosh/plugin-invalidation";
24
24
 
25
- const client = new Spoosh<ApiSchema, Error>("/api").use([
25
+ const spoosh = new Spoosh<ApiSchema, Error>("/api").use([
26
26
  invalidationPlugin(),
27
27
  optimisticPlugin(),
28
28
  ]);
package/dist/index.d.mts CHANGED
@@ -215,61 +215,6 @@ declare module "@spoosh/core" {
215
215
  }
216
216
  }
217
217
 
218
- /**
219
- * Enables optimistic updates for mutations.
220
- *
221
- * Immediately updates cached data before the mutation completes,
222
- * with automatic rollback on error.
223
- *
224
- * When using optimistic updates, invalidation mode defaults to `"none"` to prevent
225
- * unnecessary refetches that would override the optimistic data. You can override
226
- * this by explicitly setting the `invalidate` option with a mode string or array.
227
- *
228
- * @see {@link https://spoosh.dev/docs/react/plugins/optimistic | Optimistic Plugin Documentation}
229
- *
230
- * @returns Optimistic plugin instance
231
- *
232
- * @example
233
- * ```ts
234
- * import { Spoosh } from "@spoosh/core";
235
- *
236
- * const client = new Spoosh<ApiSchema, Error>("/api")
237
- * .use([
238
- * // ... other plugins
239
- * optimisticPlugin(),
240
- * ]);
241
- *
242
- * // Methods can be chained in any order
243
- * trigger({
244
- * optimistic: (api) => api("posts")
245
- * .GET()
246
- * .UPDATE_CACHE(posts => posts.filter(p => p.id !== deletedId)),
247
- * });
248
- * ```
249
- *
250
- * @example
251
- * ```ts
252
- * // With WHERE filter and disable rollback
253
- * trigger({
254
- * optimistic: (api) => api("posts")
255
- * .GET()
256
- * .NO_ROLLBACK()
257
- * .WHERE(entry => entry.query.page === 1)
258
- * .UPDATE_CACHE(posts => [newPost, ...posts]),
259
- * });
260
- * ```
261
- *
262
- * @example
263
- * ```ts
264
- * // Apply after success with typed response
265
- * trigger({
266
- * optimistic: (api) => api("posts")
267
- * .GET()
268
- * .ON_SUCCESS()
269
- * .UPDATE_CACHE((posts, newPost) => [...posts, newPost]),
270
- * });
271
- * ```
272
- */
273
218
  declare function optimisticPlugin(): SpooshPlugin<{
274
219
  readOptions: OptimisticReadOptions;
275
220
  writeOptions: OptimisticWriteOptions;
package/dist/index.d.ts CHANGED
@@ -215,61 +215,6 @@ declare module "@spoosh/core" {
215
215
  }
216
216
  }
217
217
 
218
- /**
219
- * Enables optimistic updates for mutations.
220
- *
221
- * Immediately updates cached data before the mutation completes,
222
- * with automatic rollback on error.
223
- *
224
- * When using optimistic updates, invalidation mode defaults to `"none"` to prevent
225
- * unnecessary refetches that would override the optimistic data. You can override
226
- * this by explicitly setting the `invalidate` option with a mode string or array.
227
- *
228
- * @see {@link https://spoosh.dev/docs/react/plugins/optimistic | Optimistic Plugin Documentation}
229
- *
230
- * @returns Optimistic plugin instance
231
- *
232
- * @example
233
- * ```ts
234
- * import { Spoosh } from "@spoosh/core";
235
- *
236
- * const client = new Spoosh<ApiSchema, Error>("/api")
237
- * .use([
238
- * // ... other plugins
239
- * optimisticPlugin(),
240
- * ]);
241
- *
242
- * // Methods can be chained in any order
243
- * trigger({
244
- * optimistic: (api) => api("posts")
245
- * .GET()
246
- * .UPDATE_CACHE(posts => posts.filter(p => p.id !== deletedId)),
247
- * });
248
- * ```
249
- *
250
- * @example
251
- * ```ts
252
- * // With WHERE filter and disable rollback
253
- * trigger({
254
- * optimistic: (api) => api("posts")
255
- * .GET()
256
- * .NO_ROLLBACK()
257
- * .WHERE(entry => entry.query.page === 1)
258
- * .UPDATE_CACHE(posts => [newPost, ...posts]),
259
- * });
260
- * ```
261
- *
262
- * @example
263
- * ```ts
264
- * // Apply after success with typed response
265
- * trigger({
266
- * optimistic: (api) => api("posts")
267
- * .GET()
268
- * .ON_SUCCESS()
269
- * .UPDATE_CACHE((posts, newPost) => [...posts, newPost]),
270
- * });
271
- * ```
272
- */
273
218
  declare function optimisticPlugin(): SpooshPlugin<{
274
219
  readOptions: OptimisticReadOptions;
275
220
  writeOptions: OptimisticWriteOptions;
package/dist/index.js CHANGED
@@ -90,7 +90,7 @@ function resolveOptimisticTargets(context) {
90
90
  const targets = Array.isArray(result) ? result : [result];
91
91
  return targets;
92
92
  }
93
- function applyOptimisticUpdate(stateManager, target) {
93
+ function applyOptimisticUpdate(stateManager, target, t) {
94
94
  if (!target.updater) return [];
95
95
  const tags = extractTagsFromPath(target.path);
96
96
  const targetSelfTag = getExactMatchPath(tags);
@@ -113,19 +113,24 @@ function applyOptimisticUpdate(stateManager, target) {
113
113
  if (entry.state.data === void 0) {
114
114
  continue;
115
115
  }
116
- snapshots.push({ key, previousData: entry.state.data });
116
+ const afterData = target.updater(entry.state.data, void 0);
117
+ snapshots.push({ key, previousData: entry.state.data, afterData });
117
118
  stateManager.setCache(key, {
118
119
  previousData: entry.state.data,
119
120
  state: {
120
121
  ...entry.state,
121
- data: target.updater(entry.state.data, void 0)
122
+ data: afterData
122
123
  }
123
124
  });
124
125
  stateManager.setMeta(key, { isOptimistic: true });
126
+ t?.log("Marked as optimistic", {
127
+ color: "info",
128
+ info: [{ value: { isOptimistic: true } }]
129
+ });
125
130
  }
126
131
  return snapshots;
127
132
  }
128
- function confirmOptimistic(stateManager, snapshots) {
133
+ function confirmOptimistic(stateManager, snapshots, t) {
129
134
  for (const { key } of snapshots) {
130
135
  const entry = stateManager.getCache(key);
131
136
  if (entry) {
@@ -133,10 +138,14 @@ function confirmOptimistic(stateManager, snapshots) {
133
138
  previousData: void 0
134
139
  });
135
140
  stateManager.setMeta(key, { isOptimistic: false });
141
+ t?.log("Optimistic confirmed", {
142
+ color: "success",
143
+ info: [{ value: { isOptimistic: false } }]
144
+ });
136
145
  }
137
146
  }
138
147
  }
139
- function rollbackOptimistic(stateManager, snapshots) {
148
+ function rollbackOptimistic(stateManager, snapshots, t) {
140
149
  for (const { key, previousData } of snapshots) {
141
150
  const entry = stateManager.getCache(key);
142
151
  if (entry) {
@@ -148,33 +157,66 @@ function rollbackOptimistic(stateManager, snapshots) {
148
157
  }
149
158
  });
150
159
  stateManager.setMeta(key, { isOptimistic: false });
160
+ t?.log("Optimistic rolled back", {
161
+ color: "warning",
162
+ info: [{ value: { isOptimistic: false } }]
163
+ });
151
164
  }
152
165
  }
153
166
  }
167
+ function buildSnapshotDiff(snapshots, mode = "apply") {
168
+ const first = snapshots[0];
169
+ const label = mode === "apply" ? "Optimistic update to cache" : mode === "rollback" ? "Rollback optimistic changes" : "Apply onSuccess updates";
170
+ if (snapshots.length === 1) {
171
+ return mode === "apply" || mode === "onSuccess" ? { before: first.previousData, after: first.afterData, label } : { before: first.afterData, after: first.previousData, label };
172
+ }
173
+ return mode === "apply" || mode === "onSuccess" ? {
174
+ before: snapshots.map((s) => ({ key: s.key, data: s.previousData })),
175
+ after: snapshots.map((s) => ({ key: s.key, data: s.afterData })),
176
+ label
177
+ } : {
178
+ before: snapshots.map((s) => ({ key: s.key, data: s.afterData })),
179
+ after: snapshots.map((s) => ({ key: s.key, data: s.previousData })),
180
+ label
181
+ };
182
+ }
183
+ var PLUGIN_NAME = "spoosh:optimistic";
154
184
  function optimisticPlugin() {
155
185
  return {
156
- name: "spoosh:optimistic",
186
+ name: PLUGIN_NAME,
157
187
  operations: ["write"],
158
188
  dependencies: ["spoosh:invalidation"],
159
189
  middleware: async (context, next) => {
190
+ const t = context.tracer?.(PLUGIN_NAME);
160
191
  const { stateManager } = context;
161
192
  const targets = resolveOptimisticTargets(context);
162
- if (targets.length > 0) {
163
- context.plugins.get("spoosh:invalidation")?.setDefaultMode("none");
193
+ if (targets.length === 0) {
194
+ t?.skip("No optimistic targets");
195
+ return next();
164
196
  }
165
- const immediateTargets = targets.filter((t) => t.timing !== "onSuccess");
197
+ context.plugins.get("spoosh:invalidation")?.setDefaultMode("none");
198
+ const immediateTargets = targets.filter((t2) => t2.timing !== "onSuccess");
166
199
  const allSnapshots = [];
167
200
  for (const target of immediateTargets) {
168
- const snapshots = applyOptimisticUpdate(stateManager, target);
201
+ const snapshots = applyOptimisticUpdate(stateManager, target, t);
169
202
  allSnapshots.push(...snapshots);
170
203
  }
204
+ if (allSnapshots.length > 0) {
205
+ t?.log(`Applied ${allSnapshots.length} immediate update(s)`, {
206
+ diff: buildSnapshotDiff(allSnapshots)
207
+ });
208
+ }
171
209
  const response = await next();
172
210
  if (response.error) {
173
211
  const shouldRollback = targets.some(
174
- (t) => t.rollbackOnError && t.timing !== "onSuccess"
212
+ (t2) => t2.rollbackOnError && t2.timing !== "onSuccess"
175
213
  );
176
214
  if (shouldRollback && allSnapshots.length > 0) {
177
- rollbackOptimistic(stateManager, allSnapshots);
215
+ rollbackOptimistic(stateManager, allSnapshots, t);
216
+ t?.log(`Rolled back ${allSnapshots.length} update(s)`, {
217
+ color: "warning",
218
+ diff: buildSnapshotDiff(allSnapshots, "rollback")
219
+ });
178
220
  }
179
221
  for (const target of targets) {
180
222
  if (target.onError) {
@@ -183,11 +225,12 @@ function optimisticPlugin() {
183
225
  }
184
226
  } else {
185
227
  if (allSnapshots.length > 0) {
186
- confirmOptimistic(stateManager, allSnapshots);
228
+ confirmOptimistic(stateManager, allSnapshots, t);
187
229
  }
188
230
  const onSuccessTargets = targets.filter(
189
- (t) => t.timing === "onSuccess"
231
+ (target) => target.timing === "onSuccess"
190
232
  );
233
+ const onSuccessSnapshots = [];
191
234
  for (const target of onSuccessTargets) {
192
235
  if (!target.updater) continue;
193
236
  const tags = extractTagsFromPath(target.path);
@@ -210,14 +253,26 @@ function optimisticPlugin() {
210
253
  if (entry.state.data === void 0) {
211
254
  continue;
212
255
  }
256
+ const afterData = target.updater(entry.state.data, response.data);
257
+ onSuccessSnapshots.push({
258
+ key,
259
+ previousData: entry.state.data,
260
+ afterData
261
+ });
213
262
  stateManager.setCache(key, {
214
263
  state: {
215
264
  ...entry.state,
216
- data: target.updater(entry.state.data, response.data)
265
+ data: afterData
217
266
  }
218
267
  });
219
268
  }
220
269
  }
270
+ if (onSuccessSnapshots.length > 0) {
271
+ t?.log(`Applied ${onSuccessSnapshots.length} onSuccess update(s)`, {
272
+ color: "success",
273
+ diff: buildSnapshotDiff(onSuccessSnapshots, "onSuccess")
274
+ });
275
+ }
221
276
  }
222
277
  return response;
223
278
  }
package/dist/index.mjs CHANGED
@@ -64,7 +64,7 @@ function resolveOptimisticTargets(context) {
64
64
  const targets = Array.isArray(result) ? result : [result];
65
65
  return targets;
66
66
  }
67
- function applyOptimisticUpdate(stateManager, target) {
67
+ function applyOptimisticUpdate(stateManager, target, t) {
68
68
  if (!target.updater) return [];
69
69
  const tags = extractTagsFromPath(target.path);
70
70
  const targetSelfTag = getExactMatchPath(tags);
@@ -87,19 +87,24 @@ function applyOptimisticUpdate(stateManager, target) {
87
87
  if (entry.state.data === void 0) {
88
88
  continue;
89
89
  }
90
- snapshots.push({ key, previousData: entry.state.data });
90
+ const afterData = target.updater(entry.state.data, void 0);
91
+ snapshots.push({ key, previousData: entry.state.data, afterData });
91
92
  stateManager.setCache(key, {
92
93
  previousData: entry.state.data,
93
94
  state: {
94
95
  ...entry.state,
95
- data: target.updater(entry.state.data, void 0)
96
+ data: afterData
96
97
  }
97
98
  });
98
99
  stateManager.setMeta(key, { isOptimistic: true });
100
+ t?.log("Marked as optimistic", {
101
+ color: "info",
102
+ info: [{ value: { isOptimistic: true } }]
103
+ });
99
104
  }
100
105
  return snapshots;
101
106
  }
102
- function confirmOptimistic(stateManager, snapshots) {
107
+ function confirmOptimistic(stateManager, snapshots, t) {
103
108
  for (const { key } of snapshots) {
104
109
  const entry = stateManager.getCache(key);
105
110
  if (entry) {
@@ -107,10 +112,14 @@ function confirmOptimistic(stateManager, snapshots) {
107
112
  previousData: void 0
108
113
  });
109
114
  stateManager.setMeta(key, { isOptimistic: false });
115
+ t?.log("Optimistic confirmed", {
116
+ color: "success",
117
+ info: [{ value: { isOptimistic: false } }]
118
+ });
110
119
  }
111
120
  }
112
121
  }
113
- function rollbackOptimistic(stateManager, snapshots) {
122
+ function rollbackOptimistic(stateManager, snapshots, t) {
114
123
  for (const { key, previousData } of snapshots) {
115
124
  const entry = stateManager.getCache(key);
116
125
  if (entry) {
@@ -122,33 +131,66 @@ function rollbackOptimistic(stateManager, snapshots) {
122
131
  }
123
132
  });
124
133
  stateManager.setMeta(key, { isOptimistic: false });
134
+ t?.log("Optimistic rolled back", {
135
+ color: "warning",
136
+ info: [{ value: { isOptimistic: false } }]
137
+ });
125
138
  }
126
139
  }
127
140
  }
141
+ function buildSnapshotDiff(snapshots, mode = "apply") {
142
+ const first = snapshots[0];
143
+ const label = mode === "apply" ? "Optimistic update to cache" : mode === "rollback" ? "Rollback optimistic changes" : "Apply onSuccess updates";
144
+ if (snapshots.length === 1) {
145
+ return mode === "apply" || mode === "onSuccess" ? { before: first.previousData, after: first.afterData, label } : { before: first.afterData, after: first.previousData, label };
146
+ }
147
+ return mode === "apply" || mode === "onSuccess" ? {
148
+ before: snapshots.map((s) => ({ key: s.key, data: s.previousData })),
149
+ after: snapshots.map((s) => ({ key: s.key, data: s.afterData })),
150
+ label
151
+ } : {
152
+ before: snapshots.map((s) => ({ key: s.key, data: s.afterData })),
153
+ after: snapshots.map((s) => ({ key: s.key, data: s.previousData })),
154
+ label
155
+ };
156
+ }
157
+ var PLUGIN_NAME = "spoosh:optimistic";
128
158
  function optimisticPlugin() {
129
159
  return {
130
- name: "spoosh:optimistic",
160
+ name: PLUGIN_NAME,
131
161
  operations: ["write"],
132
162
  dependencies: ["spoosh:invalidation"],
133
163
  middleware: async (context, next) => {
164
+ const t = context.tracer?.(PLUGIN_NAME);
134
165
  const { stateManager } = context;
135
166
  const targets = resolveOptimisticTargets(context);
136
- if (targets.length > 0) {
137
- context.plugins.get("spoosh:invalidation")?.setDefaultMode("none");
167
+ if (targets.length === 0) {
168
+ t?.skip("No optimistic targets");
169
+ return next();
138
170
  }
139
- const immediateTargets = targets.filter((t) => t.timing !== "onSuccess");
171
+ context.plugins.get("spoosh:invalidation")?.setDefaultMode("none");
172
+ const immediateTargets = targets.filter((t2) => t2.timing !== "onSuccess");
140
173
  const allSnapshots = [];
141
174
  for (const target of immediateTargets) {
142
- const snapshots = applyOptimisticUpdate(stateManager, target);
175
+ const snapshots = applyOptimisticUpdate(stateManager, target, t);
143
176
  allSnapshots.push(...snapshots);
144
177
  }
178
+ if (allSnapshots.length > 0) {
179
+ t?.log(`Applied ${allSnapshots.length} immediate update(s)`, {
180
+ diff: buildSnapshotDiff(allSnapshots)
181
+ });
182
+ }
145
183
  const response = await next();
146
184
  if (response.error) {
147
185
  const shouldRollback = targets.some(
148
- (t) => t.rollbackOnError && t.timing !== "onSuccess"
186
+ (t2) => t2.rollbackOnError && t2.timing !== "onSuccess"
149
187
  );
150
188
  if (shouldRollback && allSnapshots.length > 0) {
151
- rollbackOptimistic(stateManager, allSnapshots);
189
+ rollbackOptimistic(stateManager, allSnapshots, t);
190
+ t?.log(`Rolled back ${allSnapshots.length} update(s)`, {
191
+ color: "warning",
192
+ diff: buildSnapshotDiff(allSnapshots, "rollback")
193
+ });
152
194
  }
153
195
  for (const target of targets) {
154
196
  if (target.onError) {
@@ -157,11 +199,12 @@ function optimisticPlugin() {
157
199
  }
158
200
  } else {
159
201
  if (allSnapshots.length > 0) {
160
- confirmOptimistic(stateManager, allSnapshots);
202
+ confirmOptimistic(stateManager, allSnapshots, t);
161
203
  }
162
204
  const onSuccessTargets = targets.filter(
163
- (t) => t.timing === "onSuccess"
205
+ (target) => target.timing === "onSuccess"
164
206
  );
207
+ const onSuccessSnapshots = [];
165
208
  for (const target of onSuccessTargets) {
166
209
  if (!target.updater) continue;
167
210
  const tags = extractTagsFromPath(target.path);
@@ -184,14 +227,26 @@ function optimisticPlugin() {
184
227
  if (entry.state.data === void 0) {
185
228
  continue;
186
229
  }
230
+ const afterData = target.updater(entry.state.data, response.data);
231
+ onSuccessSnapshots.push({
232
+ key,
233
+ previousData: entry.state.data,
234
+ afterData
235
+ });
187
236
  stateManager.setCache(key, {
188
237
  state: {
189
238
  ...entry.state,
190
- data: target.updater(entry.state.data, response.data)
239
+ data: afterData
191
240
  }
192
241
  });
193
242
  }
194
243
  }
244
+ if (onSuccessSnapshots.length > 0) {
245
+ t?.log(`Applied ${onSuccessSnapshots.length} onSuccess update(s)`, {
246
+ color: "success",
247
+ diff: buildSnapshotDiff(onSuccessSnapshots, "onSuccess")
248
+ });
249
+ }
195
250
  }
196
251
  return response;
197
252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/plugin-optimistic",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Optimistic updates plugin for Spoosh - instant UI updates with automatic rollback",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,13 +33,13 @@
33
33
  }
34
34
  },
35
35
  "peerDependencies": {
36
- "@spoosh/core": ">=0.11.1",
36
+ "@spoosh/core": ">=0.13.0",
37
37
  "@spoosh/plugin-invalidation": ">=0.4.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@spoosh/core": "0.11.1",
41
- "@spoosh/test-utils": "0.1.6",
42
- "@spoosh/plugin-invalidation": "0.5.6"
40
+ "@spoosh/core": "0.13.0",
41
+ "@spoosh/test-utils": "0.2.0",
42
+ "@spoosh/plugin-invalidation": "0.6.0"
43
43
  },
44
44
  "scripts": {
45
45
  "dev": "tsup --watch",