@spoosh/plugin-optimistic 0.7.2 → 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/README.md CHANGED
@@ -32,40 +32,41 @@ const { trigger } = useWrite((api) => api("posts/:id").DELETE());
32
32
  trigger({
33
33
  params: { id },
34
34
  // Optimistic delete - instantly remove item from list
35
- optimistic: (api) =>
36
- api("posts")
37
- .GET()
38
- .UPDATE_CACHE((posts) => posts.filter((p) => p.id !== id)),
35
+ optimistic: (cache) =>
36
+ cache("posts").set((posts) => posts.filter((p) => p.id !== id)),
39
37
  });
40
38
 
41
- // Optimistic update with response data (onSuccess timing)
39
+ // Confirmed update - update cache after successful response
42
40
  trigger({
43
- optimistic: (api) =>
44
- api("posts")
45
- .GET()
46
- .ON_SUCCESS()
47
- .UPDATE_CACHE((posts, newPost) => [newPost!, ...posts]),
41
+ optimistic: (cache) =>
42
+ cache("posts")
43
+ .confirmed()
44
+ .set((posts, newPost) => [newPost, ...posts]),
45
+ });
46
+
47
+ // Both immediate and confirmed updates
48
+ trigger({
49
+ optimistic: (cache) =>
50
+ cache("posts")
51
+ .set((posts) => [...posts, { id: -1, title: "Saving..." }])
52
+ .confirmed()
53
+ .set((posts, newPost) => posts.map((p) => (p.id === -1 ? newPost : p))),
48
54
  });
49
55
 
50
56
  // Multiple targets
51
57
  trigger({
52
- optimistic: (api) => [
53
- api("posts")
54
- .GET()
55
- .UPDATE_CACHE((posts) => posts.filter((p) => p.id !== id)),
56
- api("stats")
57
- .GET()
58
- .UPDATE_CACHE((stats) => ({ ...stats, count: stats.count - 1 })),
58
+ optimistic: (cache) => [
59
+ cache("posts").set((posts) => posts.filter((p) => p.id !== id)),
60
+ cache("stats").set((stats) => ({ ...stats, count: stats.count - 1 })),
59
61
  ],
60
62
  });
61
63
 
62
64
  // Filter by request params
63
65
  trigger({
64
- optimistic: (api) =>
65
- api("posts")
66
- .GET()
67
- .WHERE((request) => request.query?.page === 1)
68
- .UPDATE_CACHE((posts, newPost) => [newPost!, ...posts]),
66
+ optimistic: (cache) =>
67
+ cache("posts/:id")
68
+ .filter((entry) => entry.params.id === "1")
69
+ .set((post) => ({ ...post, title: "Updated" })),
69
70
  });
70
71
  ```
71
72
 
@@ -73,22 +74,21 @@ trigger({
73
74
 
74
75
  ### Per-Request Options
75
76
 
76
- | Option | Type | Description |
77
- | ------------ | ------------------------------- | ------------------------------------- |
78
- | `optimistic` | `(api) => builder \| builder[]` | Callback to define optimistic updates |
77
+ | Option | Type | Description |
78
+ | ------------ | --------------------------------- | ------------------------------------- |
79
+ | `optimistic` | `(cache) => builder \| builder[]` | Callback to define optimistic updates |
79
80
 
80
- ### Builder Methods (DSL)
81
+ ### Builder Methods
81
82
 
82
83
  Chain methods to configure optimistic updates:
83
84
 
84
- | Method | Description |
85
- | ------------------- | ----------------------------------------- |
86
- | `.GET()` | Select the GET endpoint to update |
87
- | `.WHERE(fn)` | Filter which cache entries to update |
88
- | `.UPDATE_CACHE(fn)` | Update cache immediately (default timing) |
89
- | `.ON_SUCCESS()` | Switch to onSuccess timing mode |
90
- | `.NO_ROLLBACK()` | Disable automatic rollback on error |
91
- | `.ON_ERROR(fn)` | Error callback |
85
+ | Method | Description |
86
+ | -------------------- | ----------------------------------------------- |
87
+ | `.filter(fn)` | Filter which cache entries to update |
88
+ | `.set(fn)` | Update cache (immediate before `.confirmed()`) |
89
+ | `.confirmed()` | Switch to confirmed mode (update after success) |
90
+ | `.disableRollback()` | Disable automatic rollback on error |
91
+ | `.onError(fn)` | Error callback |
92
92
 
93
93
  ### Result
94
94
 
@@ -96,9 +96,10 @@ Chain methods to configure optimistic updates:
96
96
  | -------------- | --------- | --------------------------------------------------- |
97
97
  | `isOptimistic` | `boolean` | `true` if current data is from an optimistic update |
98
98
 
99
- ### Timing Modes
99
+ ### Update Modes
100
100
 
101
- | Usage | Description |
102
- | -------------------------------- | ---------------------------------------------------------------------------------------------------- |
103
- | `.UPDATE_CACHE(fn)` | **Immediate** - Update cache instantly before request completes. Rollback on error. |
104
- | `.ON_SUCCESS().UPDATE_CACHE(fn)` | **On Success** - Wait for successful response, then update cache. `fn` receives response as 2nd arg. |
101
+ | Usage | Description |
102
+ | ------------------------------ | --------------------------------------------------------------------------------------------------- |
103
+ | `.set(fn)` | **Immediate** - Update cache instantly before request completes. Rollback on error. |
104
+ | `.confirmed().set(fn)` | **Confirmed** - Wait for successful response, then update cache. `fn` receives response as 2nd arg. |
105
+ | `.set(fn).confirmed().set(fn)` | **Both** - Immediate update, then replace with confirmed data on success. |
package/dist/index.d.mts CHANGED
@@ -14,112 +14,113 @@ type ExtractQuery<T> = T extends {
14
14
  query?: infer Q;
15
15
  } ? Q : never;
16
16
  /**
17
- * Internal optimistic target data.
18
- * @internal
17
+ * Filter options for filtering cache entries.
19
18
  */
20
- type OptimisticTarget = {
21
- path: string;
22
- method: string;
23
- where?: (options: unknown) => boolean;
24
- updater?: (data: unknown, response?: unknown) => unknown;
25
- timing: "immediate" | "onSuccess";
26
- rollbackOnError: boolean;
27
- onError?: (error: unknown) => void;
28
- };
29
- /**
30
- * WHERE options for filtering cache entries.
31
- */
32
- type WhereOptions<TMethodConfig, TUserPath extends string> = HasParams<TUserPath> extends true ? HasQuery<TMethodConfig> extends true ? {
33
- params: Record<ExtractParamNames<TUserPath>, string | number>;
19
+ type FilterOptions<TMethodConfig, TUserPath extends string> = HasParams<TUserPath> extends true ? HasQuery<TMethodConfig> extends true ? {
20
+ params: Record<ExtractParamNames<TUserPath>, string>;
34
21
  query: ExtractQuery<TMethodConfig>;
35
22
  } : {
36
- params: Record<ExtractParamNames<TUserPath>, string | number>;
23
+ params: Record<ExtractParamNames<TUserPath>, string>;
37
24
  } : HasQuery<TMethodConfig> extends true ? {
38
25
  query: ExtractQuery<TMethodConfig>;
39
26
  } : never;
40
27
  /**
41
- * Conditionally include a method if it hasn't been used yet.
42
- */
43
- type IfNotUsed<TMethod extends string, TUsed extends string, TType> = TMethod extends TUsed ? never : TType;
44
- /**
45
- * Brand for completed builders (UPDATE_CACHE was called).
28
+ * Brand for completed builders (at least one set() was called).
46
29
  * @internal
47
30
  */
48
31
  declare const COMPLETED_BRAND: unique symbol;
49
32
  /**
50
- * Chainable builder after GET().
51
- * Methods can be chained in any order, but each method can only be called once.
52
- * Internal properties are hidden from autocomplete.
33
+ * Chainable builder for cache operations.
34
+ * Uses conditional intersections to completely hide unavailable methods from IDE suggestions.
53
35
  *
54
- * @typeParam TTiming - Tracks whether ON_SUCCESS was called to determine UPDATE_CACHE signature
55
- * @typeParam TUsed - Tracks which methods have been called to prevent duplicate calls
56
- * @typeParam TCompleted - Tracks whether UPDATE_CACHE was called (required for valid builder)
36
+ * @typeParam TData - The data type of the cache entry
37
+ * @typeParam TMethodConfig - The method configuration from schema
38
+ * @typeParam TUserPath - The user's path string
39
+ * @typeParam TResponse - The mutation response type
40
+ * @typeParam TError - The error type
41
+ * @typeParam TConfirmed - Whether we're in confirmed mode
42
+ * @typeParam THasImmediate - Whether immediate set() was called
43
+ * @typeParam THasConfirmed - Whether confirmed set() was called
44
+ * @typeParam THasFilter - Whether filter() was called
45
+ * @typeParam THasDisableRollback - Whether disableRollback() was called
46
+ * @typeParam THasOnError - Whether onError() was called
57
47
  */
58
- type OptimisticBuilder<TData = unknown, TMethodConfig = unknown, TUserPath extends string = string, TResponse = unknown, TError = unknown, TTiming extends "immediate" | "onSuccess" = "immediate", TUsed extends string = never, TCompleted extends boolean = false> = (TCompleted extends true ? {
48
+ type CacheBuilder<TData = unknown, TMethodConfig = unknown, TUserPath extends string = string, TResponse = unknown, TError = unknown, TConfirmed extends boolean = false, THasImmediate extends boolean = false, THasConfirmed extends boolean = false, THasFilter extends boolean = false, THasDisableRollback extends boolean = false, THasOnError extends boolean = false> = (THasImmediate extends true ? {
59
49
  readonly [COMPLETED_BRAND]: true;
60
- } : unknown) & {
50
+ } : THasConfirmed extends true ? {
51
+ readonly [COMPLETED_BRAND]: true;
52
+ } : unknown) & (THasFilter extends true ? unknown : THasImmediate extends true ? unknown : THasConfirmed extends true ? unknown : FilterOptions<TMethodConfig, TUserPath> extends never ? unknown : {
61
53
  /**
62
54
  * Filter which cache entries to update based on query/params.
55
+ * Must be called before any `set()`.
63
56
  *
64
57
  * @param predicate - Function that receives cache entry info and returns true to match
65
58
  *
66
59
  * @example
67
60
  * ```ts
68
- * .WHERE(entry => entry.query.page === 1)
61
+ * .filter(entry => entry.params.id === "1")
69
62
  * ```
70
63
  */
71
- WHERE: IfNotUsed<"WHERE", TUsed, WhereOptions<TMethodConfig, TUserPath> extends never ? never : (predicate: (entry: Simplify<WhereOptions<TMethodConfig, TUserPath>>) => boolean) => OptimisticBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TTiming, TUsed | "WHERE", TCompleted>>;
64
+ filter: (predicate: (entry: Simplify<FilterOptions<TMethodConfig, TUserPath>>) => boolean) => CacheBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TConfirmed, THasImmediate, THasConfirmed, true, THasDisableRollback, THasOnError>;
65
+ }) & (TConfirmed extends true ? THasConfirmed extends true ? unknown : {
72
66
  /**
73
- * Specify how to update the cached data optimistically.
74
- * This method is required - an optimistic update must have an updater function.
67
+ * Set the cache data (confirmed mode).
68
+ * Receives current data and the mutation response.
75
69
  *
76
- * For immediate updates (default): receives only the current data.
77
- * For ON_SUCCESS: receives current data and the mutation response.
70
+ * @param updater - Function that receives current data and response, returns updated data
71
+ */
72
+ set: (updater: (data: TData, response: TResponse) => TData) => CacheBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TConfirmed, THasImmediate, true, THasFilter, THasDisableRollback, THasOnError>;
73
+ } : THasImmediate extends true ? unknown : {
74
+ /**
75
+ * Set the cache data (immediate mode).
76
+ * Receives only the current data.
78
77
  *
79
- * @param updater - Function that receives current data (and response if ON_SUCCESS), returns updated data
78
+ * @param updater - Function that receives current data, returns updated data
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * .set(data => ({ ...data, pending: true }))
83
+ * ```
80
84
  */
81
- UPDATE_CACHE: IfNotUsed<"UPDATE_CACHE", TUsed, TTiming extends "onSuccess" ? (updater: (data: TData, response: TResponse) => TData) => OptimisticBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TTiming, TUsed | "UPDATE_CACHE", true> : (updater: (data: TData) => TData) => OptimisticBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TTiming, TUsed | "UPDATE_CACHE", true>>;
85
+ set: (updater: (data: TData) => TData) => CacheBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TConfirmed, true, THasConfirmed, THasFilter, false, false>;
86
+ }) & (TConfirmed extends true ? unknown : {
82
87
  /**
83
- * Apply optimistic update only after mutation succeeds.
84
- * By default, updates are applied immediately before mutation completes.
85
- * When using ON_SUCCESS, UPDATE_CACHE receives the mutation response as second argument.
88
+ * Switch to confirmed mode. The next set() will be applied after mutation succeeds.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * .set(data => ({ ...data, pending: true })) // immediate
93
+ * .confirmed()
94
+ * .set((data, response) => response) // after success
95
+ * ```
86
96
  */
87
- ON_SUCCESS: IfNotUsed<"ON_SUCCESS", TUsed, () => OptimisticBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, "onSuccess", TUsed | "ON_SUCCESS", TCompleted>>;
97
+ confirmed: () => CacheBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, true, THasImmediate, THasConfirmed, THasFilter, THasDisableRollback, THasOnError>;
98
+ }) & (THasImmediate extends true ? THasDisableRollback extends true ? unknown : {
88
99
  /**
89
100
  * Disable automatic rollback when mutation fails.
90
101
  * By default, optimistic updates are rolled back on error.
91
102
  */
92
- NO_ROLLBACK: IfNotUsed<"NO_ROLLBACK", TUsed, () => OptimisticBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TTiming, TUsed | "NO_ROLLBACK", TCompleted>>;
103
+ disableRollback: () => CacheBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TConfirmed, THasImmediate, THasConfirmed, THasFilter, true, THasOnError>;
104
+ } : unknown) & (THasImmediate extends true ? THasOnError extends true ? unknown : {
93
105
  /**
94
106
  * Callback when mutation fails.
95
107
  */
96
- ON_ERROR: IfNotUsed<"ON_ERROR", TUsed, (callback: (error: TError) => void) => OptimisticBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TTiming, TUsed | "ON_ERROR", TCompleted>>;
97
- };
108
+ onError: (callback: (error: TError) => void) => CacheBuilder<TData, TMethodConfig, TUserPath, TResponse, TError, TConfirmed, THasImmediate, THasConfirmed, THasFilter, THasDisableRollback, true>;
109
+ } : unknown);
98
110
  /**
99
- * Path methods proxy for optimistic API - only GET.
100
- * Resolves literal paths (e.g., "posts/1") to schema keys (e.g., "posts/:id") using FindMatchingKey.
101
- * Uses TPath for param extraction to preserve user's param names.
111
+ * Cache selector that resolves paths to their schema definitions.
102
112
  */
103
- type OptimisticPathMethods<TSchema, TPath extends string, TResponse, TError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? TSchema[TKey] extends infer TRoute ? "GET" extends keyof TRoute ? TRoute["GET"] extends infer TGetConfig ? {
104
- GET: () => OptimisticBuilder<ExtractData<TGetConfig>, TGetConfig, TPath, TResponse, TError, "immediate", never, false>;
105
- } : never : never : never : never : never;
113
+ type CacheSelector<TSchema, TPath extends string, TResponse, TError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? TSchema[TKey] extends infer TRoute ? "GET" extends keyof TRoute ? TRoute["GET"] extends infer TGetConfig ? CacheBuilder<ExtractData<TGetConfig>, TGetConfig, TPath, TResponse, TError, false, false, false, false, false, false> : never : never : never : never : never;
106
114
  /**
107
- * Helper type for creating the optimistic API proxy.
115
+ * Helper type for creating the cache selector.
108
116
  * Accepts both schema-defined paths (e.g., "posts/:id") and literal paths (e.g., "posts/1").
109
- * Uses union with (string & {}) to allow any string while preserving autocomplete.
110
117
  */
111
- type OptimisticApiHelper<TSchema, TResponse = unknown, TError = unknown> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => OptimisticPathMethods<TSchema, TPath, TResponse, TError>;
118
+ type CacheHelper<TSchema, TResponse = unknown, TError = unknown> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => CacheSelector<TSchema, TPath, TResponse, TError>;
112
119
  /**
113
- * A generic OptimisticTarget that accepts any data/response types.
114
- * Used for the return type of the callback.
115
- */
116
- type AnyOptimisticTarget = OptimisticTarget;
117
- /**
118
- * A completed builder that has UPDATE_CACHE called.
119
- * Uses the brand to ensure UPDATE_CACHE was called.
120
+ * A completed builder that has at least one set() called.
120
121
  * @internal
121
122
  */
122
- type CompletedOptimisticBuilder = {
123
+ type CompletedCacheBuilder = {
123
124
  readonly [COMPLETED_BRAND]: true;
124
125
  };
125
126
  /**
@@ -127,41 +128,52 @@ type CompletedOptimisticBuilder = {
127
128
  *
128
129
  * @example
129
130
  * ```ts
130
- * // Single target - immediate update (no response)
131
- * optimistic: (api) => api("posts")
132
- * .GET()
133
- * .UPDATE_CACHE(posts => posts.filter(p => p.id !== deletedId))
131
+ * // Optimistic only
132
+ * optimistic: (cache) => cache("posts")
133
+ * .set(posts => posts.filter(p => p.id !== deletedId))
134
134
  * ```
135
135
  *
136
136
  * @example
137
137
  * ```ts
138
- * // With WHERE filter and options
139
- * optimistic: (api) => api("posts")
140
- * .GET()
141
- * .WHERE(entry => entry.query.page === 1)
142
- * .NO_ROLLBACK()
143
- * .UPDATE_CACHE(posts => [...posts, newPost])
138
+ * // Confirmed only (post-success)
139
+ * optimistic: (cache) => cache("posts")
140
+ * .confirmed()
141
+ * .set((posts, newPost) => [...posts, newPost])
144
142
  * ```
145
143
  *
146
144
  * @example
147
145
  * ```ts
148
- * // Apply update after mutation succeeds (with typed response)
149
- * optimistic: (api) => api("posts")
150
- * .GET()
151
- * .ON_SUCCESS()
152
- * .UPDATE_CACHE((posts, response) => [...posts, response])
146
+ * // Both optimistic and confirmed
147
+ * optimistic: (cache) => cache("posts/:id")
148
+ * .filter(e => e.params.id === "1")
149
+ * .set(post => ({ ...post, pending: true }))
150
+ * .confirmed()
151
+ * .set((post, response) => response)
153
152
  * ```
154
153
  *
155
154
  * @example
156
155
  * ```ts
157
156
  * // Multiple targets
158
- * optimistic: (api) => [
159
- * api("posts").GET().UPDATE_CACHE(posts => posts.filter(p => p.id !== id)),
160
- * api("stats").GET().UPDATE_CACHE(stats => ({ ...stats, count: stats.count - 1 })),
157
+ * optimistic: (cache) => [
158
+ * cache("posts").set(posts => posts.filter(p => p.id !== id)),
159
+ * cache("stats").set(stats => ({ ...stats, count: stats.count - 1 })),
161
160
  * ]
162
161
  * ```
163
162
  */
164
- type OptimisticCallbackFn<TSchema = unknown, TResponse = unknown, TError = unknown> = (api: OptimisticApiHelper<TSchema, TResponse, TError>) => CompletedOptimisticBuilder | CompletedOptimisticBuilder[];
163
+ type OptimisticCallbackFn<TSchema = unknown, TResponse = unknown, TError = unknown> = (cache: CacheHelper<TSchema, TResponse, TError>) => CompletedCacheBuilder | CompletedCacheBuilder[];
164
+
165
+ /**
166
+ * Internal optimistic target data.
167
+ * @internal
168
+ */
169
+ type OptimisticTarget = {
170
+ path: string;
171
+ filter?: (options: unknown) => boolean;
172
+ immediateUpdater?: (data: unknown) => unknown;
173
+ confirmedUpdater?: (data: unknown, response: unknown) => unknown;
174
+ rollbackOnError: boolean;
175
+ onError?: (error: unknown) => void;
176
+ };
165
177
  type OptimisticPluginConfig = object;
166
178
  type OptimisticWriteOptions = object;
167
179
  interface OptimisticWriteTriggerOptions<TSchema = unknown, TResponse = unknown, TError = unknown> {
@@ -170,34 +182,22 @@ interface OptimisticWriteTriggerOptions<TSchema = unknown, TResponse = unknown,
170
182
  *
171
183
  * @example
172
184
  * ```ts
173
- * // Immediate update (default) - no response available
174
- * trigger({
175
- * optimistic: (api) => api("posts")
176
- * .GET()
177
- * .UPDATE_CACHE(posts => posts.filter(p => p.id !== deletedId)),
178
- * });
179
- * ```
180
- *
181
- * @example
182
- * ```ts
183
- * // With WHERE filter and disable rollback
185
+ * // Optimistic update
184
186
  * trigger({
185
- * optimistic: (api) => api("posts")
186
- * .GET()
187
- * .NO_ROLLBACK()
188
- * .WHERE(entry => entry.query.page === 1)
189
- * .UPDATE_CACHE(posts => [newPost, ...posts]),
187
+ * optimistic: (cache) => cache("posts")
188
+ * .set(posts => posts.filter(p => p.id !== deletedId)),
190
189
  * });
191
190
  * ```
192
191
  *
193
192
  * @example
194
193
  * ```ts
195
- * // Apply after success - response is available
194
+ * // With filter and confirmed update
196
195
  * trigger({
197
- * optimistic: (api) => api("posts")
198
- * .GET()
199
- * .ON_SUCCESS()
200
- * .UPDATE_CACHE((posts, newPost) => [...posts, newPost]),
196
+ * optimistic: (cache) => cache("posts/:id")
197
+ * .filter(e => e.params.id === "1")
198
+ * .set(post => ({ ...post, pending: true }))
199
+ * .confirmed()
200
+ * .set((post, response) => response),
201
201
  * });
202
202
  * ```
203
203
  */
@@ -224,4 +224,4 @@ declare function optimisticPlugin(): _spoosh_core.SpooshPlugin<{
224
224
  writeResult: OptimisticWriteResult;
225
225
  }>;
226
226
 
227
- export { type AnyOptimisticTarget, type OptimisticApiHelper, type OptimisticBuilder, type OptimisticCallbackFn, type OptimisticPagesOptions, type OptimisticPluginConfig, type OptimisticReadOptions, type OptimisticReadResult, type OptimisticTarget, type OptimisticWriteOptions, type OptimisticWriteResult, type OptimisticWriteTriggerOptions, optimisticPlugin };
227
+ export { type CacheBuilder, type CacheHelper, type OptimisticCallbackFn, type OptimisticPagesOptions, type OptimisticPluginConfig, type OptimisticReadOptions, type OptimisticReadResult, type OptimisticTarget, type OptimisticWriteOptions, type OptimisticWriteResult, type OptimisticWriteTriggerOptions, optimisticPlugin };