@spoosh/plugin-invalidation 0.9.1 → 0.11.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 +87 -107
- package/dist/index.d.mts +60 -33
- package/dist/index.d.ts +60 -33
- package/dist/index.js +72 -50
- package/dist/index.mjs +74 -51
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @spoosh/plugin-invalidation
|
|
2
2
|
|
|
3
|
-
Cache invalidation plugin for Spoosh - auto-invalidates related queries after mutations.
|
|
3
|
+
Cache invalidation plugin for Spoosh - auto-invalidates related queries after mutations using wildcard patterns.
|
|
4
4
|
|
|
5
5
|
**[Documentation](https://spoosh.dev/docs/react/plugins/invalidation)** · **Requirements:** TypeScript >= 5.0 · **Peer Dependencies:** `@spoosh/core`
|
|
6
6
|
|
|
@@ -12,29 +12,28 @@ npm install @spoosh/plugin-invalidation
|
|
|
12
12
|
|
|
13
13
|
## How It Works
|
|
14
14
|
|
|
15
|
-
Tags are automatically generated from the API path
|
|
15
|
+
Tags are automatically generated from the API path:
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// → tags: ["users"]
|
|
18
|
+
useRead((api) => api("posts").GET());
|
|
19
|
+
// → tag: "posts"
|
|
21
20
|
|
|
22
|
-
useRead((api) => api("
|
|
23
|
-
// →
|
|
21
|
+
useRead((api) => api("posts/:id").GET({ params: { id: 123 } }));
|
|
22
|
+
// → tag: "posts/123"
|
|
24
23
|
|
|
25
|
-
useRead((api) => api("
|
|
26
|
-
// →
|
|
24
|
+
useRead((api) => api("posts/:id/comments").GET({ params: { id: 123 } }));
|
|
25
|
+
// → tag: "posts/123/comments"
|
|
27
26
|
```
|
|
28
27
|
|
|
29
|
-
When a mutation succeeds, related queries are automatically invalidated:
|
|
28
|
+
When a mutation succeeds, related queries are automatically invalidated using wildcard patterns:
|
|
30
29
|
|
|
31
30
|
```typescript
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await trigger({ params: { id: 123 }, body: { title: "New Post" } });
|
|
31
|
+
const { trigger } = useWrite((api) => api("posts/:id/comments").POST());
|
|
32
|
+
await trigger({ params: { id: 123 }, body: { text: "Hello" } });
|
|
35
33
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
34
|
+
// Default behavior (autoInvalidate: true):
|
|
35
|
+
// Invalidates: ["posts", "posts/*"]
|
|
36
|
+
// ✓ Matches: "posts", "posts/123", "posts/123/comments", etc.
|
|
38
37
|
```
|
|
39
38
|
|
|
40
39
|
## Usage
|
|
@@ -49,172 +48,153 @@ const { trigger } = useWrite((api) => api("posts").POST());
|
|
|
49
48
|
await trigger({ body: { title: "New Post" } });
|
|
50
49
|
```
|
|
51
50
|
|
|
52
|
-
##
|
|
51
|
+
## Pattern Matching
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
invalidationPlugin({ defaultMode: "self" });
|
|
60
|
-
|
|
61
|
-
// Disable auto-invalidation by default (manual only)
|
|
62
|
-
invalidationPlugin({ defaultMode: "none" });
|
|
63
|
-
```
|
|
53
|
+
| Pattern | Matches | Does NOT Match |
|
|
54
|
+
| ---------------------- | --------------------------------- | ---------------------- |
|
|
55
|
+
| `"posts"` | `"posts"` (exact) | `"posts/1"`, `"users"` |
|
|
56
|
+
| `"posts/*"` | `"posts/1"`, `"posts/1/comments"` | `"posts"` (parent) |
|
|
57
|
+
| `["posts", "posts/*"]` | `"posts"` AND all children | - |
|
|
64
58
|
|
|
65
59
|
## Per-Request Invalidation
|
|
66
60
|
|
|
67
61
|
```typescript
|
|
68
|
-
//
|
|
62
|
+
// Exact match only
|
|
69
63
|
await trigger({
|
|
70
64
|
body: { title: "New Post" },
|
|
71
|
-
invalidate: "
|
|
65
|
+
invalidate: "posts",
|
|
72
66
|
});
|
|
73
67
|
|
|
68
|
+
// Children only (not the parent)
|
|
74
69
|
await trigger({
|
|
75
70
|
body: { title: "New Post" },
|
|
76
|
-
invalidate: "
|
|
71
|
+
invalidate: "posts/*",
|
|
77
72
|
});
|
|
78
73
|
|
|
74
|
+
// Parent AND all children
|
|
79
75
|
await trigger({
|
|
80
76
|
body: { title: "New Post" },
|
|
81
|
-
invalidate: "
|
|
77
|
+
invalidate: ["posts", "posts/*"],
|
|
82
78
|
});
|
|
83
79
|
|
|
84
|
-
//
|
|
80
|
+
// Multiple patterns
|
|
85
81
|
await trigger({
|
|
86
82
|
body: { title: "New Post" },
|
|
87
|
-
invalidate: "posts",
|
|
83
|
+
invalidate: ["posts", "users/*", "dashboard"],
|
|
88
84
|
});
|
|
89
85
|
|
|
90
|
-
//
|
|
86
|
+
// Disable invalidation for this mutation
|
|
91
87
|
await trigger({
|
|
92
88
|
body: { title: "New Post" },
|
|
93
|
-
invalidate:
|
|
94
|
-
// → Default mode: 'none' (only explicit tags are invalidated)
|
|
89
|
+
invalidate: false,
|
|
95
90
|
});
|
|
96
91
|
|
|
97
|
-
//
|
|
92
|
+
// Global refetch - triggers ALL queries to refetch
|
|
98
93
|
await trigger({
|
|
99
94
|
body: { title: "New Post" },
|
|
100
|
-
invalidate:
|
|
101
|
-
// → 'all' mode + explicit tags
|
|
95
|
+
invalidate: "*",
|
|
102
96
|
});
|
|
97
|
+
```
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
body: { title: "New Post" },
|
|
106
|
-
invalidate: ["posts", "self", "users"],
|
|
107
|
-
// → 'self' mode + explicit tags
|
|
108
|
-
});
|
|
99
|
+
## Options
|
|
109
100
|
|
|
110
|
-
|
|
111
|
-
body: { title: "New Post" },
|
|
112
|
-
invalidate: ["dashboard", "stats", "all"],
|
|
113
|
-
// → 'all' mode + explicit tags (mode can be anywhere)
|
|
114
|
-
});
|
|
101
|
+
### Plugin Config
|
|
115
102
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
103
|
+
| Option | Type | Default | Description |
|
|
104
|
+
| ---------------- | ---------- | ------- | ---------------------------------------------- |
|
|
105
|
+
| `autoInvalidate` | `boolean` | `true` | Auto-generate invalidation patterns from path |
|
|
106
|
+
| `groups` | `string[]` | `[]` | Path prefixes that use deeper segment matching |
|
|
121
107
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
108
|
+
```typescript
|
|
109
|
+
// Default: auto-invalidate using [firstSegment, firstSegment/*]
|
|
110
|
+
invalidationPlugin(); // same as { autoInvalidate: true }
|
|
111
|
+
|
|
112
|
+
// Disable auto-invalidation (manual only)
|
|
113
|
+
invalidationPlugin({ autoInvalidate: false });
|
|
114
|
+
|
|
115
|
+
// Groups: use deeper segment matching for grouped endpoints
|
|
116
|
+
invalidationPlugin({
|
|
117
|
+
groups: ["admin", "api/v1"],
|
|
126
118
|
});
|
|
127
119
|
```
|
|
128
120
|
|
|
129
|
-
|
|
121
|
+
### Groups Configuration
|
|
130
122
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
| Option | Type | Default | Description |
|
|
134
|
-
| ------------- | --------------------------- | ------- | --------------------------------------------------- |
|
|
135
|
-
| `defaultMode` | `"all" \| "self" \| "none"` | `"all"` | Default invalidation mode when option not specified |
|
|
123
|
+
Use `groups` when you have path prefixes that should be treated as a namespace:
|
|
136
124
|
|
|
137
|
-
|
|
125
|
+
```typescript
|
|
126
|
+
invalidationPlugin({
|
|
127
|
+
groups: ["admin", "api/v1"],
|
|
128
|
+
});
|
|
138
129
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
| `invalidate` | `"all" \| "self" \| "none" \| "*" \| string \| string[]` | Mode (`"all"`, `"self"`, `"none"`), wildcard (`"*"` for global refetch), single tag, or array of tags with optional mode keyword |
|
|
130
|
+
// Without groups:
|
|
131
|
+
// POST admin/posts → invalidates ["admin", "admin/*"]
|
|
142
132
|
|
|
143
|
-
|
|
133
|
+
// With groups: ["admin"]:
|
|
134
|
+
// POST admin/posts → invalidates ["admin/posts", "admin/posts/*"]
|
|
135
|
+
// POST admin/users → invalidates ["admin/users", "admin/users/*"]
|
|
136
|
+
// POST admin → invalidates ["admin", "admin/*"]
|
|
144
137
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
| `"self"` | Only invalidate the exact endpoint tag | `users/123/posts` → `users/123/posts` |
|
|
149
|
-
| `"none"` | Disable auto-invalidation (manual only) | No automatic invalidation |
|
|
150
|
-
| `"*"` | Global refetch - triggers all queries | All active queries refetch |
|
|
138
|
+
// With groups: ["api/v1"]:
|
|
139
|
+
// POST api/v1/users → invalidates ["api/v1/users", "api/v1/users/*"]
|
|
140
|
+
```
|
|
151
141
|
|
|
152
|
-
###
|
|
142
|
+
### Per-Request Options
|
|
153
143
|
|
|
154
|
-
|
|
144
|
+
| Option | Type | Description |
|
|
145
|
+
| ------------ | ------------------------------------ | ------------------------------------------------------------------------- |
|
|
146
|
+
| `invalidate` | `string \| string[] \| false \| "*"` | Pattern(s) to invalidate, `false` to disable, or `"*"` for global refetch |
|
|
155
147
|
|
|
156
|
-
|
|
148
|
+
## Default Behavior
|
|
157
149
|
|
|
158
|
-
|
|
150
|
+
When `autoInvalidate: true` (default) and no `invalidate` option is provided:
|
|
159
151
|
|
|
160
152
|
```typescript
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
// If path is users/123/posts → invalidates: users, users/123, users/123/posts
|
|
153
|
+
// POST /posts/123/comments
|
|
154
|
+
// → Invalidates: ["posts", "posts/*"]
|
|
164
155
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
//
|
|
156
|
+
// The first path segment is used to generate patterns:
|
|
157
|
+
// - "posts" - exact match for the root
|
|
158
|
+
// - "posts/*" - all children under posts
|
|
168
159
|
```
|
|
169
160
|
|
|
170
161
|
## Instance API
|
|
171
162
|
|
|
172
|
-
The plugin exposes `invalidate` for
|
|
163
|
+
The plugin exposes `invalidate` for manual cache invalidation:
|
|
173
164
|
|
|
174
165
|
```typescript
|
|
175
166
|
import { create } from "@spoosh/react";
|
|
176
167
|
|
|
177
168
|
const { useRead, invalidate } = create(spoosh);
|
|
178
169
|
|
|
179
|
-
//
|
|
180
|
-
invalidate(
|
|
170
|
+
// Single pattern
|
|
171
|
+
invalidate("posts");
|
|
181
172
|
|
|
182
|
-
//
|
|
183
|
-
invalidate("users");
|
|
173
|
+
// Multiple patterns
|
|
174
|
+
invalidate(["posts", "users/*"]);
|
|
184
175
|
|
|
185
|
-
// Global refetch
|
|
176
|
+
// Global refetch
|
|
186
177
|
invalidate("*");
|
|
187
178
|
|
|
188
|
-
// Useful for external events
|
|
189
|
-
socket.on("
|
|
190
|
-
invalidate(
|
|
179
|
+
// Useful for external events
|
|
180
|
+
socket.on("posts-updated", () => {
|
|
181
|
+
invalidate(["posts", "posts/*"]);
|
|
191
182
|
});
|
|
192
183
|
|
|
193
|
-
// WebSocket: trigger global refetch
|
|
194
184
|
socket.on("full-sync", () => {
|
|
195
185
|
invalidate("*");
|
|
196
186
|
});
|
|
197
187
|
```
|
|
198
188
|
|
|
199
|
-
| Method | Description |
|
|
200
|
-
| ------------ | ---------------------------------------------------------------------- |
|
|
201
|
-
| `invalidate` | Manually invalidate cache entries by tags, or use `"*"` to refetch all |
|
|
202
|
-
|
|
203
189
|
## Combining with Cache Plugin
|
|
204
190
|
|
|
205
|
-
For scenarios like logout
|
|
191
|
+
For scenarios like logout, combine with `clearCache` from `@spoosh/plugin-cache`:
|
|
206
192
|
|
|
207
193
|
```typescript
|
|
208
194
|
const { trigger } = useWrite((api) => api("auth/logout").POST());
|
|
209
195
|
|
|
210
|
-
// Clear cache + trigger all queries to refetch
|
|
211
196
|
await trigger({
|
|
212
|
-
clearCache: true, //
|
|
213
|
-
invalidate: "*", //
|
|
197
|
+
clearCache: true, // Clear all cached data
|
|
198
|
+
invalidate: "*", // Trigger all queries to refetch
|
|
214
199
|
});
|
|
215
200
|
```
|
|
216
|
-
|
|
217
|
-
This ensures both:
|
|
218
|
-
|
|
219
|
-
1. All cached data is cleared (no stale data from previous session)
|
|
220
|
-
2. All active queries refetch with fresh data
|
package/dist/index.d.mts
CHANGED
|
@@ -1,31 +1,57 @@
|
|
|
1
1
|
import * as _spoosh_core from '@spoosh/core';
|
|
2
2
|
|
|
3
|
-
type InvalidationMode = "all" | "self" | "none";
|
|
4
3
|
/**
|
|
5
4
|
* Extract paths that have GET methods (eligible for invalidation)
|
|
5
|
+
* Excludes "/" root path to avoid "//*" pattern in autocomplete
|
|
6
6
|
*/
|
|
7
7
|
type ReadPaths<TSchema> = {
|
|
8
|
-
[K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
|
|
8
|
+
[K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K extends "/" | "" ? never : K : never;
|
|
9
9
|
}[keyof TSchema & string];
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* - String: mode only ('all' | 'self' | 'none')
|
|
13
|
-
* - Array: tags only OR [mode keyword mixed with tags]
|
|
14
|
-
* - If array contains 'all' or 'self' at ANY position, it's treated as mode + tags
|
|
15
|
-
* - Otherwise, it's tags only with mode defaulting to 'none'
|
|
16
|
-
* - 'none' keyword should NOT be used in arrays (use string 'none' instead)
|
|
11
|
+
* Support both exact paths and wildcard patterns in autocomplete
|
|
17
12
|
*/
|
|
18
|
-
type
|
|
13
|
+
type InvalidatePattern<TSchema> = ReadPaths<TSchema> | `${ReadPaths<TSchema>}/*`;
|
|
14
|
+
/**
|
|
15
|
+
* Unified invalidate option with wildcard pattern support
|
|
16
|
+
* - `"posts"` - Exact match only
|
|
17
|
+
* - `"posts/*"` - Children only (posts/1, posts/1/comments) - NOT posts itself
|
|
18
|
+
* - `["posts", "posts/*"]` - posts AND all children
|
|
19
|
+
* - `"*"` - Global refetch
|
|
20
|
+
* - `false` - Disable invalidation
|
|
21
|
+
* - Any custom string is also allowed for custom tags
|
|
22
|
+
*/
|
|
23
|
+
type InvalidateOption<TSchema = unknown> = "*" | false | InvalidatePattern<TSchema> | (string & {}) | (InvalidatePattern<TSchema> | (string & {}))[];
|
|
19
24
|
interface InvalidationPluginConfig {
|
|
20
25
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
26
|
+
* Enable automatic invalidation for mutations.
|
|
27
|
+
* When true, mutations automatically invalidate `[firstSegment, firstSegment/*]`.
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
autoInvalidate?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Path groups that should use deeper segment matching for invalidation.
|
|
33
|
+
* Useful for grouped endpoints like `admin/posts`, `api/v1/users`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* invalidationPlugin({
|
|
38
|
+
* groups: ["admin", "api/v1"]
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* // Without groups:
|
|
42
|
+
* // POST admin/posts → invalidates ["admin", "admin/*"]
|
|
43
|
+
*
|
|
44
|
+
* // With groups: ["admin"]:
|
|
45
|
+
* // POST admin/posts → invalidates ["admin/posts", "admin/posts/*"]
|
|
46
|
+
* // POST admin/users → invalidates ["admin/users", "admin/users/*"]
|
|
47
|
+
* // POST admin → invalidates ["admin", "admin/*"]
|
|
48
|
+
* ```
|
|
23
49
|
*/
|
|
24
|
-
|
|
50
|
+
groups?: string[];
|
|
25
51
|
}
|
|
26
52
|
type InvalidationWriteOptions = object;
|
|
27
53
|
interface InvalidationWriteTriggerOptions<TSchema = unknown> {
|
|
28
|
-
/** Unified invalidation configuration */
|
|
54
|
+
/** Unified invalidation configuration with wildcard pattern support */
|
|
29
55
|
invalidate?: InvalidateOption<TSchema>;
|
|
30
56
|
}
|
|
31
57
|
type InvalidationReadOptions = object;
|
|
@@ -33,43 +59,43 @@ type InvalidationPagesOptions = object;
|
|
|
33
59
|
type InvalidationReadResult = object;
|
|
34
60
|
type InvalidationWriteResult = object;
|
|
35
61
|
interface InvalidationQueueTriggerOptions<TSchema = unknown> {
|
|
36
|
-
/** Unified invalidation configuration */
|
|
62
|
+
/** Unified invalidation configuration with wildcard pattern support */
|
|
37
63
|
invalidate?: InvalidateOption<TSchema>;
|
|
38
64
|
}
|
|
39
65
|
type InvalidationQueueResult = object;
|
|
40
66
|
/**
|
|
41
|
-
* Manual invalidation
|
|
67
|
+
* Manual invalidation with pattern support
|
|
42
68
|
*/
|
|
43
69
|
type InvalidateFn<TSchema> = {
|
|
44
70
|
(tag: "*"): void;
|
|
45
|
-
(
|
|
46
|
-
(
|
|
71
|
+
(pattern: InvalidatePattern<TSchema> | (string & {})): void;
|
|
72
|
+
(patterns: (InvalidatePattern<TSchema> | (string & {}))[]): void;
|
|
47
73
|
};
|
|
48
74
|
interface InvalidationInstanceApi {
|
|
49
|
-
/** Manually invalidate cache entries by
|
|
75
|
+
/** Manually invalidate cache entries by patterns. Useful for external events like WebSocket messages. */
|
|
50
76
|
invalidate: InvalidateFn<unknown>;
|
|
51
77
|
}
|
|
52
|
-
interface
|
|
53
|
-
/**
|
|
54
|
-
|
|
78
|
+
interface InvalidationPluginInternal {
|
|
79
|
+
/** Disable auto-invalidation for this request. Used by optimistic plugin. */
|
|
80
|
+
disableAutoInvalidate: () => void;
|
|
55
81
|
}
|
|
56
82
|
declare module "@spoosh/core" {
|
|
57
|
-
interface PluginExportsRegistry {
|
|
58
|
-
"spoosh:invalidation": InvalidationPluginExports;
|
|
59
|
-
}
|
|
60
83
|
interface PluginResolvers<TContext> {
|
|
61
84
|
invalidate: InvalidateOption<TContext["schema"]> | undefined;
|
|
62
85
|
}
|
|
63
|
-
interface
|
|
86
|
+
interface ApiResolvers<TSchema> {
|
|
64
87
|
invalidate: InvalidateFn<TSchema>;
|
|
65
88
|
}
|
|
89
|
+
interface PluginInternalRegistry {
|
|
90
|
+
"spoosh:invalidation": InvalidationPluginInternal;
|
|
91
|
+
}
|
|
66
92
|
}
|
|
67
93
|
|
|
68
94
|
/**
|
|
69
95
|
* Enables automatic cache invalidation after mutations.
|
|
70
96
|
*
|
|
71
97
|
* Marks related cache entries as stale and triggers refetches
|
|
72
|
-
* based on
|
|
98
|
+
* based on wildcard patterns or explicit invalidation targets.
|
|
73
99
|
*
|
|
74
100
|
* @param config - Plugin configuration
|
|
75
101
|
*
|
|
@@ -81,24 +107,24 @@ declare module "@spoosh/core" {
|
|
|
81
107
|
*
|
|
82
108
|
* const spoosh = new Spoosh<ApiSchema, Error>("/api")
|
|
83
109
|
* .use([
|
|
84
|
-
* invalidationPlugin({
|
|
110
|
+
* invalidationPlugin({ autoInvalidate: true }),
|
|
85
111
|
* ]);
|
|
86
112
|
*
|
|
87
113
|
* // Per-mutation invalidation
|
|
88
114
|
* trigger({
|
|
89
|
-
* invalidate: "
|
|
115
|
+
* invalidate: "posts", // Exact match only
|
|
90
116
|
* });
|
|
91
117
|
*
|
|
92
118
|
* trigger({
|
|
93
|
-
* invalidate: "posts", //
|
|
119
|
+
* invalidate: "posts/*", // Children only (posts/1, posts/1/comments)
|
|
94
120
|
* });
|
|
95
121
|
*
|
|
96
122
|
* trigger({
|
|
97
|
-
* invalidate: ["posts", "
|
|
123
|
+
* invalidate: ["posts", "posts/*"], // posts AND all children
|
|
98
124
|
* });
|
|
99
125
|
*
|
|
100
126
|
* trigger({
|
|
101
|
-
* invalidate:
|
|
127
|
+
* invalidate: false, // Disable invalidation
|
|
102
128
|
* });
|
|
103
129
|
*
|
|
104
130
|
* trigger({
|
|
@@ -115,7 +141,8 @@ declare function invalidationPlugin(config?: InvalidationPluginConfig): _spoosh_
|
|
|
115
141
|
readResult: InvalidationReadResult;
|
|
116
142
|
writeResult: InvalidationWriteResult;
|
|
117
143
|
queueResult: InvalidationQueueResult;
|
|
118
|
-
|
|
144
|
+
api: InvalidationInstanceApi;
|
|
145
|
+
internal: InvalidationPluginInternal;
|
|
119
146
|
}>;
|
|
120
147
|
|
|
121
|
-
export { type InvalidateOption, type
|
|
148
|
+
export { type InvalidateFn, type InvalidateOption, type InvalidationInstanceApi, type InvalidationPagesOptions, type InvalidationPluginConfig, type InvalidationPluginInternal, type InvalidationQueueResult, type InvalidationQueueTriggerOptions, type InvalidationReadOptions, type InvalidationReadResult, type InvalidationWriteOptions, type InvalidationWriteResult, type InvalidationWriteTriggerOptions, invalidationPlugin };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,31 +1,57 @@
|
|
|
1
1
|
import * as _spoosh_core from '@spoosh/core';
|
|
2
2
|
|
|
3
|
-
type InvalidationMode = "all" | "self" | "none";
|
|
4
3
|
/**
|
|
5
4
|
* Extract paths that have GET methods (eligible for invalidation)
|
|
5
|
+
* Excludes "/" root path to avoid "//*" pattern in autocomplete
|
|
6
6
|
*/
|
|
7
7
|
type ReadPaths<TSchema> = {
|
|
8
|
-
[K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
|
|
8
|
+
[K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K extends "/" | "" ? never : K : never;
|
|
9
9
|
}[keyof TSchema & string];
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* - String: mode only ('all' | 'self' | 'none')
|
|
13
|
-
* - Array: tags only OR [mode keyword mixed with tags]
|
|
14
|
-
* - If array contains 'all' or 'self' at ANY position, it's treated as mode + tags
|
|
15
|
-
* - Otherwise, it's tags only with mode defaulting to 'none'
|
|
16
|
-
* - 'none' keyword should NOT be used in arrays (use string 'none' instead)
|
|
11
|
+
* Support both exact paths and wildcard patterns in autocomplete
|
|
17
12
|
*/
|
|
18
|
-
type
|
|
13
|
+
type InvalidatePattern<TSchema> = ReadPaths<TSchema> | `${ReadPaths<TSchema>}/*`;
|
|
14
|
+
/**
|
|
15
|
+
* Unified invalidate option with wildcard pattern support
|
|
16
|
+
* - `"posts"` - Exact match only
|
|
17
|
+
* - `"posts/*"` - Children only (posts/1, posts/1/comments) - NOT posts itself
|
|
18
|
+
* - `["posts", "posts/*"]` - posts AND all children
|
|
19
|
+
* - `"*"` - Global refetch
|
|
20
|
+
* - `false` - Disable invalidation
|
|
21
|
+
* - Any custom string is also allowed for custom tags
|
|
22
|
+
*/
|
|
23
|
+
type InvalidateOption<TSchema = unknown> = "*" | false | InvalidatePattern<TSchema> | (string & {}) | (InvalidatePattern<TSchema> | (string & {}))[];
|
|
19
24
|
interface InvalidationPluginConfig {
|
|
20
25
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
26
|
+
* Enable automatic invalidation for mutations.
|
|
27
|
+
* When true, mutations automatically invalidate `[firstSegment, firstSegment/*]`.
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
autoInvalidate?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Path groups that should use deeper segment matching for invalidation.
|
|
33
|
+
* Useful for grouped endpoints like `admin/posts`, `api/v1/users`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* invalidationPlugin({
|
|
38
|
+
* groups: ["admin", "api/v1"]
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* // Without groups:
|
|
42
|
+
* // POST admin/posts → invalidates ["admin", "admin/*"]
|
|
43
|
+
*
|
|
44
|
+
* // With groups: ["admin"]:
|
|
45
|
+
* // POST admin/posts → invalidates ["admin/posts", "admin/posts/*"]
|
|
46
|
+
* // POST admin/users → invalidates ["admin/users", "admin/users/*"]
|
|
47
|
+
* // POST admin → invalidates ["admin", "admin/*"]
|
|
48
|
+
* ```
|
|
23
49
|
*/
|
|
24
|
-
|
|
50
|
+
groups?: string[];
|
|
25
51
|
}
|
|
26
52
|
type InvalidationWriteOptions = object;
|
|
27
53
|
interface InvalidationWriteTriggerOptions<TSchema = unknown> {
|
|
28
|
-
/** Unified invalidation configuration */
|
|
54
|
+
/** Unified invalidation configuration with wildcard pattern support */
|
|
29
55
|
invalidate?: InvalidateOption<TSchema>;
|
|
30
56
|
}
|
|
31
57
|
type InvalidationReadOptions = object;
|
|
@@ -33,43 +59,43 @@ type InvalidationPagesOptions = object;
|
|
|
33
59
|
type InvalidationReadResult = object;
|
|
34
60
|
type InvalidationWriteResult = object;
|
|
35
61
|
interface InvalidationQueueTriggerOptions<TSchema = unknown> {
|
|
36
|
-
/** Unified invalidation configuration */
|
|
62
|
+
/** Unified invalidation configuration with wildcard pattern support */
|
|
37
63
|
invalidate?: InvalidateOption<TSchema>;
|
|
38
64
|
}
|
|
39
65
|
type InvalidationQueueResult = object;
|
|
40
66
|
/**
|
|
41
|
-
* Manual invalidation
|
|
67
|
+
* Manual invalidation with pattern support
|
|
42
68
|
*/
|
|
43
69
|
type InvalidateFn<TSchema> = {
|
|
44
70
|
(tag: "*"): void;
|
|
45
|
-
(
|
|
46
|
-
(
|
|
71
|
+
(pattern: InvalidatePattern<TSchema> | (string & {})): void;
|
|
72
|
+
(patterns: (InvalidatePattern<TSchema> | (string & {}))[]): void;
|
|
47
73
|
};
|
|
48
74
|
interface InvalidationInstanceApi {
|
|
49
|
-
/** Manually invalidate cache entries by
|
|
75
|
+
/** Manually invalidate cache entries by patterns. Useful for external events like WebSocket messages. */
|
|
50
76
|
invalidate: InvalidateFn<unknown>;
|
|
51
77
|
}
|
|
52
|
-
interface
|
|
53
|
-
/**
|
|
54
|
-
|
|
78
|
+
interface InvalidationPluginInternal {
|
|
79
|
+
/** Disable auto-invalidation for this request. Used by optimistic plugin. */
|
|
80
|
+
disableAutoInvalidate: () => void;
|
|
55
81
|
}
|
|
56
82
|
declare module "@spoosh/core" {
|
|
57
|
-
interface PluginExportsRegistry {
|
|
58
|
-
"spoosh:invalidation": InvalidationPluginExports;
|
|
59
|
-
}
|
|
60
83
|
interface PluginResolvers<TContext> {
|
|
61
84
|
invalidate: InvalidateOption<TContext["schema"]> | undefined;
|
|
62
85
|
}
|
|
63
|
-
interface
|
|
86
|
+
interface ApiResolvers<TSchema> {
|
|
64
87
|
invalidate: InvalidateFn<TSchema>;
|
|
65
88
|
}
|
|
89
|
+
interface PluginInternalRegistry {
|
|
90
|
+
"spoosh:invalidation": InvalidationPluginInternal;
|
|
91
|
+
}
|
|
66
92
|
}
|
|
67
93
|
|
|
68
94
|
/**
|
|
69
95
|
* Enables automatic cache invalidation after mutations.
|
|
70
96
|
*
|
|
71
97
|
* Marks related cache entries as stale and triggers refetches
|
|
72
|
-
* based on
|
|
98
|
+
* based on wildcard patterns or explicit invalidation targets.
|
|
73
99
|
*
|
|
74
100
|
* @param config - Plugin configuration
|
|
75
101
|
*
|
|
@@ -81,24 +107,24 @@ declare module "@spoosh/core" {
|
|
|
81
107
|
*
|
|
82
108
|
* const spoosh = new Spoosh<ApiSchema, Error>("/api")
|
|
83
109
|
* .use([
|
|
84
|
-
* invalidationPlugin({
|
|
110
|
+
* invalidationPlugin({ autoInvalidate: true }),
|
|
85
111
|
* ]);
|
|
86
112
|
*
|
|
87
113
|
* // Per-mutation invalidation
|
|
88
114
|
* trigger({
|
|
89
|
-
* invalidate: "
|
|
115
|
+
* invalidate: "posts", // Exact match only
|
|
90
116
|
* });
|
|
91
117
|
*
|
|
92
118
|
* trigger({
|
|
93
|
-
* invalidate: "posts", //
|
|
119
|
+
* invalidate: "posts/*", // Children only (posts/1, posts/1/comments)
|
|
94
120
|
* });
|
|
95
121
|
*
|
|
96
122
|
* trigger({
|
|
97
|
-
* invalidate: ["posts", "
|
|
123
|
+
* invalidate: ["posts", "posts/*"], // posts AND all children
|
|
98
124
|
* });
|
|
99
125
|
*
|
|
100
126
|
* trigger({
|
|
101
|
-
* invalidate:
|
|
127
|
+
* invalidate: false, // Disable invalidation
|
|
102
128
|
* });
|
|
103
129
|
*
|
|
104
130
|
* trigger({
|
|
@@ -115,7 +141,8 @@ declare function invalidationPlugin(config?: InvalidationPluginConfig): _spoosh_
|
|
|
115
141
|
readResult: InvalidationReadResult;
|
|
116
142
|
writeResult: InvalidationWriteResult;
|
|
117
143
|
queueResult: InvalidationQueueResult;
|
|
118
|
-
|
|
144
|
+
api: InvalidationInstanceApi;
|
|
145
|
+
internal: InvalidationPluginInternal;
|
|
119
146
|
}>;
|
|
120
147
|
|
|
121
|
-
export { type InvalidateOption, type
|
|
148
|
+
export { type InvalidateFn, type InvalidateOption, type InvalidationInstanceApi, type InvalidationPagesOptions, type InvalidationPluginConfig, type InvalidationPluginInternal, type InvalidationQueueResult, type InvalidationQueueTriggerOptions, type InvalidationReadOptions, type InvalidationReadResult, type InvalidationWriteOptions, type InvalidationWriteResult, type InvalidationWriteTriggerOptions, invalidationPlugin };
|
package/dist/index.js
CHANGED
|
@@ -27,96 +27,118 @@ module.exports = __toCommonJS(src_exports);
|
|
|
27
27
|
// src/plugin.ts
|
|
28
28
|
var import_core = require("@spoosh/core");
|
|
29
29
|
var PLUGIN_NAME = "spoosh:invalidation";
|
|
30
|
-
var
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
return [];
|
|
30
|
+
var AUTO_INVALIDATE_DISABLED_KEY = "invalidation:autoDisabled";
|
|
31
|
+
function calculateSegmentDepth(path, groups) {
|
|
32
|
+
if (groups.length === 0) return 1;
|
|
33
|
+
const sortedGroups = [...groups].sort((a, b) => b.length - a.length);
|
|
34
|
+
for (const group of sortedGroups) {
|
|
35
|
+
const normalizedGroup = group.replace(/^\/+|\/+$/g, "");
|
|
36
|
+
if (path === normalizedGroup || path.startsWith(normalizedGroup + "/")) {
|
|
37
|
+
return normalizedGroup.split("/").length + 1;
|
|
38
|
+
}
|
|
40
39
|
}
|
|
40
|
+
return 1;
|
|
41
41
|
}
|
|
42
|
-
function resolveInvalidateTags(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
42
|
+
function resolveInvalidateTags(path, params, invalidateOption, autoInvalidate, groups) {
|
|
43
|
+
if (invalidateOption === false) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (invalidateOption === "*" || invalidateOption === "/*") {
|
|
47
|
+
return ["*"];
|
|
48
48
|
}
|
|
49
49
|
if (typeof invalidateOption === "string") {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const normalized = (0, import_core.normalizeTag)(invalidateOption);
|
|
51
|
+
if (normalized === "*") {
|
|
52
|
+
return ["*"];
|
|
52
53
|
}
|
|
53
|
-
return [
|
|
54
|
+
return [normalized];
|
|
54
55
|
}
|
|
55
56
|
if (Array.isArray(invalidateOption)) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return
|
|
57
|
+
return [...new Set(invalidateOption.map(import_core.normalizeTag))];
|
|
58
|
+
}
|
|
59
|
+
if (!autoInvalidate) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const resolvedPath = (0, import_core.normalizeTag)((0, import_core.resolvePathString)(path, params));
|
|
63
|
+
const segments = resolvedPath.split("/");
|
|
64
|
+
const depth = calculateSegmentDepth(resolvedPath, groups);
|
|
65
|
+
const baseSegments = segments.slice(0, depth);
|
|
66
|
+
if (baseSegments.length === 0 || !baseSegments[0]) {
|
|
67
|
+
return false;
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
+
const base = baseSegments.join("/");
|
|
70
|
+
return [base, `${base}/*`];
|
|
69
71
|
}
|
|
70
72
|
function invalidationPlugin(config = {}) {
|
|
71
|
-
const {
|
|
73
|
+
const { autoInvalidate = true, groups = [] } = config;
|
|
72
74
|
return (0, import_core.createSpooshPlugin)({
|
|
73
75
|
name: PLUGIN_NAME,
|
|
74
76
|
operations: ["write", "queue"],
|
|
75
|
-
exports(context) {
|
|
76
|
-
return {
|
|
77
|
-
setDefaultMode(value) {
|
|
78
|
-
context.temp.set(INVALIDATION_DEFAULT_KEY, value);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
},
|
|
82
77
|
afterResponse(context, response) {
|
|
83
78
|
const t = context.tracer?.(PLUGIN_NAME);
|
|
84
79
|
if (!response.error) {
|
|
85
|
-
const
|
|
80
|
+
const params = context.request.params;
|
|
81
|
+
const invalidateOption = context.pluginOptions?.invalidate;
|
|
82
|
+
const isAutoDisabled = context.temp.get(AUTO_INVALIDATE_DISABLED_KEY);
|
|
83
|
+
const effectiveAutoInvalidate = isAutoDisabled ? false : autoInvalidate;
|
|
84
|
+
const tags = resolveInvalidateTags(
|
|
85
|
+
context.path,
|
|
86
|
+
params,
|
|
87
|
+
invalidateOption,
|
|
88
|
+
effectiveAutoInvalidate,
|
|
89
|
+
groups
|
|
90
|
+
);
|
|
91
|
+
if (tags === false) {
|
|
92
|
+
t?.skip("Invalidation disabled", { color: "muted" });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
86
95
|
if (tags.includes("*")) {
|
|
87
96
|
t?.log("Refetch all", { color: "warning" });
|
|
88
97
|
context.eventEmitter.emit("refetchAll", void 0);
|
|
89
98
|
return;
|
|
90
99
|
}
|
|
91
100
|
if (tags.length > 0) {
|
|
92
|
-
t?.log("Invalidated
|
|
101
|
+
t?.log("Invalidated patterns", {
|
|
93
102
|
color: "info",
|
|
94
|
-
info: [{ label: "
|
|
103
|
+
info: [{ label: "Patterns", value: tags }]
|
|
95
104
|
});
|
|
96
105
|
context.stateManager.markStale(tags);
|
|
97
106
|
context.eventEmitter.emit("invalidate", tags);
|
|
98
107
|
} else {
|
|
99
|
-
t?.skip("No
|
|
108
|
+
t?.skip("No patterns to invalidate", { color: "muted" });
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
},
|
|
103
|
-
|
|
112
|
+
api(context) {
|
|
104
113
|
const { stateManager, eventEmitter } = context;
|
|
105
114
|
const et = context.eventTracer?.(PLUGIN_NAME);
|
|
106
115
|
const invalidate = (input) => {
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
116
|
+
const rawPatterns = Array.isArray(input) ? input : [input];
|
|
117
|
+
if (rawPatterns.includes("*") || rawPatterns.includes("/*")) {
|
|
109
118
|
et?.emit("Refetch all (manual)", { color: "warning" });
|
|
110
119
|
eventEmitter.emit("refetchAll", void 0);
|
|
111
120
|
return;
|
|
112
121
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
eventEmitter.emit("
|
|
122
|
+
const patterns = rawPatterns.map(import_core.normalizeTag);
|
|
123
|
+
if (patterns.includes("*")) {
|
|
124
|
+
et?.emit("Refetch all (manual)", { color: "warning" });
|
|
125
|
+
eventEmitter.emit("refetchAll", void 0);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (patterns.length > 0) {
|
|
129
|
+
et?.emit(`Invalidated: ${patterns.join(", ")}`, { color: "info" });
|
|
130
|
+
stateManager.markStale(patterns);
|
|
131
|
+
eventEmitter.emit("invalidate", patterns);
|
|
117
132
|
}
|
|
118
133
|
};
|
|
119
134
|
return { invalidate };
|
|
135
|
+
},
|
|
136
|
+
internal(context) {
|
|
137
|
+
return {
|
|
138
|
+
disableAutoInvalidate() {
|
|
139
|
+
context.temp.set(AUTO_INVALIDATE_DISABLED_KEY, true);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
120
142
|
}
|
|
121
143
|
});
|
|
122
144
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,99 +1,122 @@
|
|
|
1
1
|
// src/plugin.ts
|
|
2
2
|
import {
|
|
3
3
|
resolvePathString,
|
|
4
|
-
createSpooshPlugin
|
|
4
|
+
createSpooshPlugin,
|
|
5
|
+
normalizeTag
|
|
5
6
|
} from "@spoosh/core";
|
|
6
7
|
var PLUGIN_NAME = "spoosh:invalidation";
|
|
7
|
-
var
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
return [];
|
|
8
|
+
var AUTO_INVALIDATE_DISABLED_KEY = "invalidation:autoDisabled";
|
|
9
|
+
function calculateSegmentDepth(path, groups) {
|
|
10
|
+
if (groups.length === 0) return 1;
|
|
11
|
+
const sortedGroups = [...groups].sort((a, b) => b.length - a.length);
|
|
12
|
+
for (const group of sortedGroups) {
|
|
13
|
+
const normalizedGroup = group.replace(/^\/+|\/+$/g, "");
|
|
14
|
+
if (path === normalizedGroup || path.startsWith(normalizedGroup + "/")) {
|
|
15
|
+
return normalizedGroup.split("/").length + 1;
|
|
16
|
+
}
|
|
17
17
|
}
|
|
18
|
+
return 1;
|
|
18
19
|
}
|
|
19
|
-
function resolveInvalidateTags(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return
|
|
20
|
+
function resolveInvalidateTags(path, params, invalidateOption, autoInvalidate, groups) {
|
|
21
|
+
if (invalidateOption === false) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (invalidateOption === "*" || invalidateOption === "/*") {
|
|
25
|
+
return ["*"];
|
|
25
26
|
}
|
|
26
27
|
if (typeof invalidateOption === "string") {
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const normalized = normalizeTag(invalidateOption);
|
|
29
|
+
if (normalized === "*") {
|
|
30
|
+
return ["*"];
|
|
29
31
|
}
|
|
30
|
-
return [
|
|
32
|
+
return [normalized];
|
|
31
33
|
}
|
|
32
34
|
if (Array.isArray(invalidateOption)) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
35
|
+
return [...new Set(invalidateOption.map(normalizeTag))];
|
|
36
|
+
}
|
|
37
|
+
if (!autoInvalidate) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const resolvedPath = normalizeTag(resolvePathString(path, params));
|
|
41
|
+
const segments = resolvedPath.split("/");
|
|
42
|
+
const depth = calculateSegmentDepth(resolvedPath, groups);
|
|
43
|
+
const baseSegments = segments.slice(0, depth);
|
|
44
|
+
if (baseSegments.length === 0 || !baseSegments[0]) {
|
|
45
|
+
return false;
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
const base = baseSegments.join("/");
|
|
48
|
+
return [base, `${base}/*`];
|
|
46
49
|
}
|
|
47
50
|
function invalidationPlugin(config = {}) {
|
|
48
|
-
const {
|
|
51
|
+
const { autoInvalidate = true, groups = [] } = config;
|
|
49
52
|
return createSpooshPlugin({
|
|
50
53
|
name: PLUGIN_NAME,
|
|
51
54
|
operations: ["write", "queue"],
|
|
52
|
-
exports(context) {
|
|
53
|
-
return {
|
|
54
|
-
setDefaultMode(value) {
|
|
55
|
-
context.temp.set(INVALIDATION_DEFAULT_KEY, value);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
},
|
|
59
55
|
afterResponse(context, response) {
|
|
60
56
|
const t = context.tracer?.(PLUGIN_NAME);
|
|
61
57
|
if (!response.error) {
|
|
62
|
-
const
|
|
58
|
+
const params = context.request.params;
|
|
59
|
+
const invalidateOption = context.pluginOptions?.invalidate;
|
|
60
|
+
const isAutoDisabled = context.temp.get(AUTO_INVALIDATE_DISABLED_KEY);
|
|
61
|
+
const effectiveAutoInvalidate = isAutoDisabled ? false : autoInvalidate;
|
|
62
|
+
const tags = resolveInvalidateTags(
|
|
63
|
+
context.path,
|
|
64
|
+
params,
|
|
65
|
+
invalidateOption,
|
|
66
|
+
effectiveAutoInvalidate,
|
|
67
|
+
groups
|
|
68
|
+
);
|
|
69
|
+
if (tags === false) {
|
|
70
|
+
t?.skip("Invalidation disabled", { color: "muted" });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
63
73
|
if (tags.includes("*")) {
|
|
64
74
|
t?.log("Refetch all", { color: "warning" });
|
|
65
75
|
context.eventEmitter.emit("refetchAll", void 0);
|
|
66
76
|
return;
|
|
67
77
|
}
|
|
68
78
|
if (tags.length > 0) {
|
|
69
|
-
t?.log("Invalidated
|
|
79
|
+
t?.log("Invalidated patterns", {
|
|
70
80
|
color: "info",
|
|
71
|
-
info: [{ label: "
|
|
81
|
+
info: [{ label: "Patterns", value: tags }]
|
|
72
82
|
});
|
|
73
83
|
context.stateManager.markStale(tags);
|
|
74
84
|
context.eventEmitter.emit("invalidate", tags);
|
|
75
85
|
} else {
|
|
76
|
-
t?.skip("No
|
|
86
|
+
t?.skip("No patterns to invalidate", { color: "muted" });
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
},
|
|
80
|
-
|
|
90
|
+
api(context) {
|
|
81
91
|
const { stateManager, eventEmitter } = context;
|
|
82
92
|
const et = context.eventTracer?.(PLUGIN_NAME);
|
|
83
93
|
const invalidate = (input) => {
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
94
|
+
const rawPatterns = Array.isArray(input) ? input : [input];
|
|
95
|
+
if (rawPatterns.includes("*") || rawPatterns.includes("/*")) {
|
|
86
96
|
et?.emit("Refetch all (manual)", { color: "warning" });
|
|
87
97
|
eventEmitter.emit("refetchAll", void 0);
|
|
88
98
|
return;
|
|
89
99
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
eventEmitter.emit("
|
|
100
|
+
const patterns = rawPatterns.map(normalizeTag);
|
|
101
|
+
if (patterns.includes("*")) {
|
|
102
|
+
et?.emit("Refetch all (manual)", { color: "warning" });
|
|
103
|
+
eventEmitter.emit("refetchAll", void 0);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (patterns.length > 0) {
|
|
107
|
+
et?.emit(`Invalidated: ${patterns.join(", ")}`, { color: "info" });
|
|
108
|
+
stateManager.markStale(patterns);
|
|
109
|
+
eventEmitter.emit("invalidate", patterns);
|
|
94
110
|
}
|
|
95
111
|
};
|
|
96
112
|
return { invalidate };
|
|
113
|
+
},
|
|
114
|
+
internal(context) {
|
|
115
|
+
return {
|
|
116
|
+
disableAutoInvalidate() {
|
|
117
|
+
context.temp.set(AUTO_INVALIDATE_DISABLED_KEY, true);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
97
120
|
}
|
|
98
121
|
});
|
|
99
122
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spoosh/plugin-invalidation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Cache invalidation plugin for Spoosh - auto-invalidates after mutations",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@spoosh/core": ">=0.
|
|
36
|
+
"@spoosh/core": ">=0.18.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@spoosh/core": "0.
|
|
39
|
+
"@spoosh/core": "0.18.0",
|
|
40
40
|
"@spoosh/test-utils": "0.3.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
"build": "tsup",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
46
|
"lint": "eslint src --max-warnings 0",
|
|
47
|
-
"format": "prettier --write 'src/**/*.ts'"
|
|
47
|
+
"format": "prettier --write 'src/**/*.ts' '*.md'"
|
|
48
48
|
}
|
|
49
49
|
}
|