@spoosh/plugin-optimistic 0.6.0 → 0.6.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.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { FindMatchingKey, HasParams, ExtractParamNames, Simplify, ExtractData, SpooshPlugin } from '@spoosh/core';
1
+ import { ReadPaths, FindMatchingKey, HasParams, ExtractParamNames, Simplify, ExtractData, SpooshPlugin } from '@spoosh/core';
2
2
 
3
3
  /**
4
4
  * Check if query exists in the method config.
@@ -94,22 +94,20 @@ type OptimisticBuilder<TData = unknown, TMethodConfig = unknown, TUserPath exten
94
94
  */
95
95
  ON_ERROR: IfNotUsed<"ON_ERROR", TUsed, (callback: (error: unknown) => void) => OptimisticBuilder<TData, TMethodConfig, TUserPath, TResponse, TTiming, TUsed | "ON_ERROR", TCompleted>>;
96
96
  };
97
- /**
98
- * Extract paths that have GET methods.
99
- */
100
- type ReadPaths<TSchema> = {
101
- [K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
102
- }[keyof TSchema & string];
103
97
  /**
104
98
  * Path methods proxy for optimistic API - only GET.
99
+ * Resolves literal paths (e.g., "posts/1") to schema keys (e.g., "posts/:id") using FindMatchingKey.
100
+ * Uses TPath for param extraction to preserve user's param names.
105
101
  */
106
102
  type OptimisticPathMethods<TSchema, TPath extends string, TResponse> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? TSchema[TKey] extends infer TRoute ? "GET" extends keyof TRoute ? TRoute["GET"] extends infer TGetConfig ? {
107
103
  GET: () => OptimisticBuilder<ExtractData<TGetConfig>, TGetConfig, TPath, TResponse, "immediate", never, false>;
108
104
  } : never : never : never : never : never;
109
105
  /**
110
106
  * Helper type for creating the optimistic API proxy.
107
+ * Accepts both schema-defined paths (e.g., "posts/:id") and literal paths (e.g., "posts/1").
108
+ * Uses union with (string & {}) to allow any string while preserving autocomplete.
111
109
  */
112
- type OptimisticApiHelper<TSchema, TResponse = unknown> = <TPath extends ReadPaths<TSchema>>(path: TPath) => OptimisticPathMethods<TSchema, TPath, TResponse>;
110
+ type OptimisticApiHelper<TSchema, TResponse = unknown> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => OptimisticPathMethods<TSchema, TPath, TResponse>;
113
111
  /**
114
112
  * A generic OptimisticTarget that accepts any data/response types.
115
113
  * Used for the return type of the callback.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FindMatchingKey, HasParams, ExtractParamNames, Simplify, ExtractData, SpooshPlugin } from '@spoosh/core';
1
+ import { ReadPaths, FindMatchingKey, HasParams, ExtractParamNames, Simplify, ExtractData, SpooshPlugin } from '@spoosh/core';
2
2
 
3
3
  /**
4
4
  * Check if query exists in the method config.
@@ -94,22 +94,20 @@ type OptimisticBuilder<TData = unknown, TMethodConfig = unknown, TUserPath exten
94
94
  */
95
95
  ON_ERROR: IfNotUsed<"ON_ERROR", TUsed, (callback: (error: unknown) => void) => OptimisticBuilder<TData, TMethodConfig, TUserPath, TResponse, TTiming, TUsed | "ON_ERROR", TCompleted>>;
96
96
  };
97
- /**
98
- * Extract paths that have GET methods.
99
- */
100
- type ReadPaths<TSchema> = {
101
- [K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
102
- }[keyof TSchema & string];
103
97
  /**
104
98
  * Path methods proxy for optimistic API - only GET.
99
+ * Resolves literal paths (e.g., "posts/1") to schema keys (e.g., "posts/:id") using FindMatchingKey.
100
+ * Uses TPath for param extraction to preserve user's param names.
105
101
  */
106
102
  type OptimisticPathMethods<TSchema, TPath extends string, TResponse> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? TSchema[TKey] extends infer TRoute ? "GET" extends keyof TRoute ? TRoute["GET"] extends infer TGetConfig ? {
107
103
  GET: () => OptimisticBuilder<ExtractData<TGetConfig>, TGetConfig, TPath, TResponse, "immediate", never, false>;
108
104
  } : never : never : never : never : never;
109
105
  /**
110
106
  * Helper type for creating the optimistic API proxy.
107
+ * Accepts both schema-defined paths (e.g., "posts/:id") and literal paths (e.g., "posts/1").
108
+ * Uses union with (string & {}) to allow any string while preserving autocomplete.
111
109
  */
112
- type OptimisticApiHelper<TSchema, TResponse = unknown> = <TPath extends ReadPaths<TSchema>>(path: TPath) => OptimisticPathMethods<TSchema, TPath, TResponse>;
110
+ type OptimisticApiHelper<TSchema, TResponse = unknown> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => OptimisticPathMethods<TSchema, TPath, TResponse>;
113
111
  /**
114
112
  * A generic OptimisticTarget that accepts any data/response types.
115
113
  * Used for the return type of the callback.
package/dist/index.js CHANGED
@@ -25,7 +25,6 @@ __export(src_exports, {
25
25
  module.exports = __toCommonJS(src_exports);
26
26
 
27
27
  // src/plugin.ts
28
- var import_core = require("@spoosh/core");
29
28
  var import_plugin_invalidation = require("@spoosh/plugin-invalidation");
30
29
  function createBuilder(state) {
31
30
  return {
@@ -58,12 +57,50 @@ function createOptimisticProxy() {
58
57
  });
59
58
  return ((path) => createMethodsProxy(path));
60
59
  }
61
- function extractTagsFromPath(path) {
62
- const pathSegments = path.split("/").filter(Boolean);
63
- return (0, import_core.generateTags)(pathSegments);
60
+ function isParameterSegment(segment) {
61
+ return segment.startsWith(":");
64
62
  }
65
- function getExactMatchPath(tags) {
66
- return tags.length > 0 ? tags[tags.length - 1] : void 0;
63
+ function pathMatchesPattern(actualPath, pattern) {
64
+ const actualSegments = actualPath.split("/").filter(Boolean);
65
+ const patternSegments = pattern.split("/").filter(Boolean);
66
+ if (actualSegments.length !== patternSegments.length) {
67
+ return { matches: false, params: {}, paramMapping: {} };
68
+ }
69
+ const params = {};
70
+ const paramMapping = {};
71
+ for (let i = 0; i < patternSegments.length; i++) {
72
+ const patternSeg = patternSegments[i];
73
+ const actualSeg = actualSegments[i];
74
+ if (isParameterSegment(patternSeg)) {
75
+ const targetParamName = patternSeg.slice(1);
76
+ if (isParameterSegment(actualSeg)) {
77
+ const actualParamName = actualSeg.slice(1);
78
+ paramMapping[targetParamName] = actualParamName;
79
+ continue;
80
+ }
81
+ params[targetParamName] = actualSeg;
82
+ } else if (isParameterSegment(actualSeg)) {
83
+ continue;
84
+ } else if (patternSeg !== actualSeg) {
85
+ return { matches: false, params: {}, paramMapping: {} };
86
+ }
87
+ }
88
+ return { matches: true, params, paramMapping };
89
+ }
90
+ function hasPatternParams(path) {
91
+ return path.split("/").some(isParameterSegment);
92
+ }
93
+ function extractPathFromKey(key) {
94
+ try {
95
+ const parsed = JSON.parse(key);
96
+ const path = parsed.path;
97
+ if (typeof path === "string") {
98
+ return path;
99
+ }
100
+ return null;
101
+ } catch {
102
+ return null;
103
+ }
67
104
  }
68
105
  function extractOptionsFromKey(key) {
69
106
  try {
@@ -82,6 +119,21 @@ function extractOptionsFromKey(key) {
82
119
  return null;
83
120
  }
84
121
  }
122
+ function mapParamsToTargetNames(actualParams, paramMapping) {
123
+ if (!actualParams) return {};
124
+ const result = {};
125
+ for (const [targetName, actualName] of Object.entries(paramMapping)) {
126
+ if (actualName in actualParams) {
127
+ result[targetName] = actualParams[actualName];
128
+ }
129
+ }
130
+ for (const [key, value] of Object.entries(actualParams)) {
131
+ if (!Object.values(paramMapping).includes(key)) {
132
+ result[key] = value;
133
+ }
134
+ }
135
+ return result;
136
+ }
85
137
  function resolveOptimisticTargets(context) {
86
138
  const pluginOptions = context.pluginOptions;
87
139
  if (!pluginOptions?.optimistic) return [];
@@ -90,27 +142,72 @@ function resolveOptimisticTargets(context) {
90
142
  const targets = Array.isArray(result) ? result : [result];
91
143
  return targets;
92
144
  }
145
+ function getMatchingEntries(stateManager, targetPath, targetMethod) {
146
+ const results = [];
147
+ if (hasPatternParams(targetPath)) {
148
+ const allEntries = stateManager.getAllCacheEntries();
149
+ for (const { key, entry } of allEntries) {
150
+ if (key.includes('"type":"infinite-tracker"')) continue;
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('"type":"infinite-tracker"')) continue;
166
+ if (!key.includes(`"method":"${targetMethod}"`)) continue;
167
+ const actualPath = extractPathFromKey(key);
168
+ if (!actualPath) continue;
169
+ if (actualPath === targetPath) {
170
+ results.push({ key, entry, extractedParams: {}, paramMapping: {} });
171
+ } else if (hasPatternParams(actualPath)) {
172
+ const { matches, params, paramMapping } = pathMatchesPattern(
173
+ targetPath,
174
+ actualPath
175
+ );
176
+ if (matches) {
177
+ results.push({ key, entry, extractedParams: params, paramMapping });
178
+ }
179
+ }
180
+ }
181
+ }
182
+ return results;
183
+ }
93
184
  function applyOptimisticUpdate(stateManager, target, t) {
94
185
  if (!target.updater) return [];
95
- const tags = extractTagsFromPath(target.path);
96
- const targetSelfTag = getExactMatchPath(tags);
97
- if (!targetSelfTag) return [];
98
186
  const snapshots = [];
99
- const entries = stateManager.getCacheEntriesBySelfTag(targetSelfTag);
100
- for (const { key, entry } of entries) {
101
- if (key.includes('"type":"infinite-tracker"')) {
102
- continue;
103
- }
104
- if (!key.includes(`"method":"${target.method}"`)) {
105
- continue;
106
- }
187
+ const matchingEntries = getMatchingEntries(
188
+ stateManager,
189
+ target.path,
190
+ target.method
191
+ );
192
+ for (const { key, entry, extractedParams, paramMapping } of matchingEntries) {
107
193
  if (target.where) {
108
- const options = extractOptionsFromKey(key);
109
- if (!options || !target.where(options)) {
194
+ const options = extractOptionsFromKey(key) ?? {};
195
+ const mappedParams = mapParamsToTargetNames(
196
+ options.params,
197
+ paramMapping
198
+ );
199
+ const mergedOptions = {
200
+ ...options,
201
+ params: {
202
+ ...extractedParams,
203
+ ...mappedParams
204
+ }
205
+ };
206
+ if (!target.where(mergedOptions)) {
110
207
  continue;
111
208
  }
112
209
  }
113
- if (entry.state.data === void 0) {
210
+ if (entry?.state.data === void 0) {
114
211
  continue;
115
212
  }
116
213
  const afterData = target.updater(entry.state.data, void 0);
@@ -233,24 +330,35 @@ function optimisticPlugin() {
233
330
  const onSuccessSnapshots = [];
234
331
  for (const target of onSuccessTargets) {
235
332
  if (!target.updater) continue;
236
- const tags = extractTagsFromPath(target.path);
237
- const targetSelfTag = getExactMatchPath(tags);
238
- if (!targetSelfTag) continue;
239
- const entries = stateManager.getCacheEntriesBySelfTag(targetSelfTag);
240
- for (const { key, entry } of entries) {
241
- if (key.includes('"type":"infinite-tracker"')) {
242
- continue;
243
- }
244
- if (!key.includes(`"method":"${target.method}"`)) {
245
- continue;
246
- }
333
+ const matchingEntries = getMatchingEntries(
334
+ stateManager,
335
+ target.path,
336
+ target.method
337
+ );
338
+ for (const {
339
+ key,
340
+ entry,
341
+ extractedParams,
342
+ paramMapping
343
+ } of matchingEntries) {
247
344
  if (target.where) {
248
- const options = extractOptionsFromKey(key);
249
- if (!options || !target.where(options)) {
345
+ const options = extractOptionsFromKey(key) ?? {};
346
+ const mappedParams = mapParamsToTargetNames(
347
+ options.params,
348
+ paramMapping
349
+ );
350
+ const mergedOptions = {
351
+ ...options,
352
+ params: {
353
+ ...extractedParams,
354
+ ...mappedParams
355
+ }
356
+ };
357
+ if (!target.where(mergedOptions)) {
250
358
  continue;
251
359
  }
252
360
  }
253
- if (entry.state.data === void 0) {
361
+ if (entry?.state.data === void 0) {
254
362
  continue;
255
363
  }
256
364
  const afterData = target.updater(entry.state.data, response.data);
package/dist/index.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  // src/plugin.ts
2
- import { generateTags } from "@spoosh/core";
3
2
  import "@spoosh/plugin-invalidation";
4
3
  function createBuilder(state) {
5
4
  return {
@@ -32,12 +31,50 @@ function createOptimisticProxy() {
32
31
  });
33
32
  return ((path) => createMethodsProxy(path));
34
33
  }
35
- function extractTagsFromPath(path) {
36
- const pathSegments = path.split("/").filter(Boolean);
37
- return generateTags(pathSegments);
34
+ function isParameterSegment(segment) {
35
+ return segment.startsWith(":");
38
36
  }
39
- function getExactMatchPath(tags) {
40
- return tags.length > 0 ? tags[tags.length - 1] : void 0;
37
+ function pathMatchesPattern(actualPath, pattern) {
38
+ const actualSegments = actualPath.split("/").filter(Boolean);
39
+ const patternSegments = pattern.split("/").filter(Boolean);
40
+ if (actualSegments.length !== patternSegments.length) {
41
+ return { matches: false, params: {}, paramMapping: {} };
42
+ }
43
+ const params = {};
44
+ const paramMapping = {};
45
+ for (let i = 0; i < patternSegments.length; i++) {
46
+ const patternSeg = patternSegments[i];
47
+ const actualSeg = actualSegments[i];
48
+ if (isParameterSegment(patternSeg)) {
49
+ const targetParamName = patternSeg.slice(1);
50
+ if (isParameterSegment(actualSeg)) {
51
+ const actualParamName = actualSeg.slice(1);
52
+ paramMapping[targetParamName] = actualParamName;
53
+ continue;
54
+ }
55
+ params[targetParamName] = actualSeg;
56
+ } else if (isParameterSegment(actualSeg)) {
57
+ continue;
58
+ } else if (patternSeg !== actualSeg) {
59
+ return { matches: false, params: {}, paramMapping: {} };
60
+ }
61
+ }
62
+ return { matches: true, params, paramMapping };
63
+ }
64
+ function hasPatternParams(path) {
65
+ return path.split("/").some(isParameterSegment);
66
+ }
67
+ function extractPathFromKey(key) {
68
+ try {
69
+ const parsed = JSON.parse(key);
70
+ const path = parsed.path;
71
+ if (typeof path === "string") {
72
+ return path;
73
+ }
74
+ return null;
75
+ } catch {
76
+ return null;
77
+ }
41
78
  }
42
79
  function extractOptionsFromKey(key) {
43
80
  try {
@@ -56,6 +93,21 @@ function extractOptionsFromKey(key) {
56
93
  return null;
57
94
  }
58
95
  }
96
+ function mapParamsToTargetNames(actualParams, paramMapping) {
97
+ if (!actualParams) return {};
98
+ const result = {};
99
+ for (const [targetName, actualName] of Object.entries(paramMapping)) {
100
+ if (actualName in actualParams) {
101
+ result[targetName] = actualParams[actualName];
102
+ }
103
+ }
104
+ for (const [key, value] of Object.entries(actualParams)) {
105
+ if (!Object.values(paramMapping).includes(key)) {
106
+ result[key] = value;
107
+ }
108
+ }
109
+ return result;
110
+ }
59
111
  function resolveOptimisticTargets(context) {
60
112
  const pluginOptions = context.pluginOptions;
61
113
  if (!pluginOptions?.optimistic) return [];
@@ -64,27 +116,72 @@ function resolveOptimisticTargets(context) {
64
116
  const targets = Array.isArray(result) ? result : [result];
65
117
  return targets;
66
118
  }
119
+ function getMatchingEntries(stateManager, targetPath, targetMethod) {
120
+ const results = [];
121
+ if (hasPatternParams(targetPath)) {
122
+ const allEntries = stateManager.getAllCacheEntries();
123
+ for (const { key, entry } of allEntries) {
124
+ if (key.includes('"type":"infinite-tracker"')) continue;
125
+ if (!key.includes(`"method":"${targetMethod}"`)) continue;
126
+ const actualPath = extractPathFromKey(key);
127
+ if (!actualPath) continue;
128
+ const { matches, params, paramMapping } = pathMatchesPattern(
129
+ actualPath,
130
+ targetPath
131
+ );
132
+ if (matches) {
133
+ results.push({ key, entry, extractedParams: params, paramMapping });
134
+ }
135
+ }
136
+ } else {
137
+ const allEntries = stateManager.getAllCacheEntries();
138
+ for (const { key, entry } of allEntries) {
139
+ if (key.includes('"type":"infinite-tracker"')) continue;
140
+ if (!key.includes(`"method":"${targetMethod}"`)) continue;
141
+ const actualPath = extractPathFromKey(key);
142
+ if (!actualPath) continue;
143
+ if (actualPath === targetPath) {
144
+ results.push({ key, entry, extractedParams: {}, paramMapping: {} });
145
+ } else if (hasPatternParams(actualPath)) {
146
+ const { matches, params, paramMapping } = pathMatchesPattern(
147
+ targetPath,
148
+ actualPath
149
+ );
150
+ if (matches) {
151
+ results.push({ key, entry, extractedParams: params, paramMapping });
152
+ }
153
+ }
154
+ }
155
+ }
156
+ return results;
157
+ }
67
158
  function applyOptimisticUpdate(stateManager, target, t) {
68
159
  if (!target.updater) return [];
69
- const tags = extractTagsFromPath(target.path);
70
- const targetSelfTag = getExactMatchPath(tags);
71
- if (!targetSelfTag) return [];
72
160
  const snapshots = [];
73
- const entries = stateManager.getCacheEntriesBySelfTag(targetSelfTag);
74
- for (const { key, entry } of entries) {
75
- if (key.includes('"type":"infinite-tracker"')) {
76
- continue;
77
- }
78
- if (!key.includes(`"method":"${target.method}"`)) {
79
- continue;
80
- }
161
+ const matchingEntries = getMatchingEntries(
162
+ stateManager,
163
+ target.path,
164
+ target.method
165
+ );
166
+ for (const { key, entry, extractedParams, paramMapping } of matchingEntries) {
81
167
  if (target.where) {
82
- const options = extractOptionsFromKey(key);
83
- if (!options || !target.where(options)) {
168
+ const options = extractOptionsFromKey(key) ?? {};
169
+ const mappedParams = mapParamsToTargetNames(
170
+ options.params,
171
+ paramMapping
172
+ );
173
+ const mergedOptions = {
174
+ ...options,
175
+ params: {
176
+ ...extractedParams,
177
+ ...mappedParams
178
+ }
179
+ };
180
+ if (!target.where(mergedOptions)) {
84
181
  continue;
85
182
  }
86
183
  }
87
- if (entry.state.data === void 0) {
184
+ if (entry?.state.data === void 0) {
88
185
  continue;
89
186
  }
90
187
  const afterData = target.updater(entry.state.data, void 0);
@@ -207,24 +304,35 @@ function optimisticPlugin() {
207
304
  const onSuccessSnapshots = [];
208
305
  for (const target of onSuccessTargets) {
209
306
  if (!target.updater) continue;
210
- const tags = extractTagsFromPath(target.path);
211
- const targetSelfTag = getExactMatchPath(tags);
212
- if (!targetSelfTag) continue;
213
- const entries = stateManager.getCacheEntriesBySelfTag(targetSelfTag);
214
- for (const { key, entry } of entries) {
215
- if (key.includes('"type":"infinite-tracker"')) {
216
- continue;
217
- }
218
- if (!key.includes(`"method":"${target.method}"`)) {
219
- continue;
220
- }
307
+ const matchingEntries = getMatchingEntries(
308
+ stateManager,
309
+ target.path,
310
+ target.method
311
+ );
312
+ for (const {
313
+ key,
314
+ entry,
315
+ extractedParams,
316
+ paramMapping
317
+ } of matchingEntries) {
221
318
  if (target.where) {
222
- const options = extractOptionsFromKey(key);
223
- if (!options || !target.where(options)) {
319
+ const options = extractOptionsFromKey(key) ?? {};
320
+ const mappedParams = mapParamsToTargetNames(
321
+ options.params,
322
+ paramMapping
323
+ );
324
+ const mergedOptions = {
325
+ ...options,
326
+ params: {
327
+ ...extractedParams,
328
+ ...mappedParams
329
+ }
330
+ };
331
+ if (!target.where(mergedOptions)) {
224
332
  continue;
225
333
  }
226
334
  }
227
- if (entry.state.data === void 0) {
335
+ if (entry?.state.data === void 0) {
228
336
  continue;
229
337
  }
230
338
  const afterData = target.updater(entry.state.data, response.data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/plugin-optimistic",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Optimistic updates plugin for Spoosh - instant UI updates with automatic rollback",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,8 +37,8 @@
37
37
  "@spoosh/plugin-invalidation": ">=0.7.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@spoosh/core": "0.13.1",
41
- "@spoosh/plugin-invalidation": "0.7.0",
40
+ "@spoosh/core": "0.14.1",
41
+ "@spoosh/plugin-invalidation": "0.8.0",
42
42
  "@spoosh/test-utils": "0.2.0"
43
43
  },
44
44
  "scripts": {