@gorgonjs/gorgon 1.6.0 → 1.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Gorgon
2
2
  ![coverage](https://img.shields.io/badge/coverage-97%25-brightgreen)
3
3
  ![size](https://img.shields.io/badge/size-5.71KB-brightgreen)
4
- ![version](https://img.shields.io/badge/version-1.6.0-blue)
4
+ ![version](https://img.shields.io/badge/version-1.6.1-blue)
5
5
  ![license](https://img.shields.io/badge/license-MIT-blue)
6
6
 
7
7
  A typescript async based caching library for node or the browser.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gorgonjs/gorgon",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "A simple caching library for async functions",
5
5
  "homepage": "https://gorgonjs.dev",
6
6
  "main": "./dist/index.umd.js",
@@ -29,11 +29,17 @@
29
29
  "type": "git",
30
30
  "url": "git@github.com:mikevalstar/gorgon.git"
31
31
  },
32
+ "files": [
33
+ "dist",
34
+ "skills",
35
+ "!skills/_artifacts"
36
+ ],
32
37
  "keywords": [
33
38
  "cache",
34
39
  "promise",
35
40
  "async",
36
- "typescript"
41
+ "typescript",
42
+ "tanstack-intent"
37
43
  ],
38
44
  "author": "Mike Valstar <mike@valstar.dev>",
39
45
  "license": "MIT",
@@ -50,7 +56,8 @@
50
56
  "typescript": "^5.1.6",
51
57
  "vite": "^4.4.4",
52
58
  "vite-plugin-dts": "^3.3.0",
53
- "vitest": "^0.33.0"
59
+ "vitest": "^0.33.0",
60
+ "@tanstack/intent": "^0.0.23"
54
61
  },
55
62
  "jest": {
56
63
  "collectCoverage": true,
@@ -0,0 +1,336 @@
1
+ ---
2
+ name: core
3
+ description: >
4
+ Core caching with @gorgonjs/gorgon: Gorgon.get, Gorgon.put, Gorgon.clear
5
+ (with wildcards), Gorgon.overwrite, expiry policies, cache key naming,
6
+ concurrency deduplication, hooks, settings, custom storage providers via
7
+ IGorgonCacheProvider, @gorgonjs/file-provider for disk persistence, and
8
+ @gorgonjs/clearlink for distributed cache invalidation via WebSocket.
9
+ Use when caching API calls, database queries, or any async operation in
10
+ TypeScript/JavaScript.
11
+ type: core
12
+ library: gorgon
13
+ library_version: "1.6.0"
14
+ sources:
15
+ - "mikevalstar/gorgon:library/index.ts"
16
+ - "mikevalstar/gorgon:gorgonjs.dev/src/pages/docs/usage/get.md"
17
+ - "mikevalstar/gorgon:gorgonjs.dev/src/pages/docs/usage/policies.md"
18
+ - "mikevalstar/gorgon:gorgonjs.dev/src/pages/docs/concurrency.md"
19
+ - "mikevalstar/gorgon:gorgonjs.dev/src/pages/docs/custom-storage.md"
20
+ ---
21
+
22
+ # Gorgon.js — Core Caching
23
+
24
+ Gorgon is a lightweight (~4kb, ~1.3kb gzipped) TypeScript caching library for async functions. It works in Node.js and browsers. Its key feature is automatic concurrency protection — multiple simultaneous requests for the same cache key are deduplicated, with all callers sharing the same result. Errors are never cached.
25
+
26
+ ## Setup
27
+
28
+ ```typescript
29
+ import Gorgon from '@gorgonjs/gorgon';
30
+
31
+ const user = await Gorgon.get(`user/${id}`, async () => {
32
+ const res = await fetch(`/api/users/${id}`);
33
+ return res.json();
34
+ }, 60 * 1000); // cache for 1 minute
35
+ ```
36
+
37
+ For React usage, see `react/SKILL.md`.
38
+
39
+ ## Core Patterns
40
+
41
+ ### Cache an async function with typed return
42
+
43
+ ```typescript
44
+ import Gorgon from '@gorgonjs/gorgon';
45
+
46
+ interface User { id: number; name: string; email: string; }
47
+
48
+ const getUser = (id: number): Promise<User> =>
49
+ Gorgon.get(`user/${id}`, async () => {
50
+ const res = await fetch(`/api/users/${id}`);
51
+ return res.json();
52
+ }, 60 * 1000);
53
+ ```
54
+
55
+ The return type flows through — `getUser` returns `Promise<User>` with full type safety.
56
+
57
+ ### Cache key naming for wildcard invalidation
58
+
59
+ Use the format `type/id/sub-id` so you can clear groups with wildcards:
60
+
61
+ ```typescript
62
+ await Gorgon.get(`user/${id}/profile`, fetchProfile, 60000);
63
+ await Gorgon.get(`user/${id}/posts`, fetchPosts, 60000);
64
+ await Gorgon.get(`user/${id}/settings`, fetchSettings, 60000);
65
+
66
+ // Clear everything for a user at once
67
+ Gorgon.clear(`user/${id}/*`);
68
+ ```
69
+
70
+ ### Directly insert or force-refresh cached data
71
+
72
+ ```typescript
73
+ // Insert a value directly
74
+ await Gorgon.put(`user/${id}`, userData, 60000);
75
+
76
+ // Force-refresh (always executes the function, no dedup)
77
+ const updated = await Gorgon.overwrite(`user/${id}`, async () => {
78
+ return fetch(`/api/users/${id}`).then(r => r.json());
79
+ }, 60000);
80
+ ```
81
+
82
+ ### Expiry policies
83
+
84
+ ```typescript
85
+ // Milliseconds from now
86
+ Gorgon.get('key', fn, 60000);
87
+
88
+ // Specific date
89
+ Gorgon.get('key', fn, new Date('2026-12-31'));
90
+
91
+ // Cache forever (use with caution — see Common Mistakes)
92
+ Gorgon.get('key', fn, false);
93
+
94
+ // No policy — cached until manually cleared
95
+ Gorgon.get('key', fn);
96
+
97
+ // Full policy object — specify provider and expiry
98
+ Gorgon.get('key', fn, { expiry: 60000, provider: 'file' });
99
+ ```
100
+
101
+ ### Configure global settings
102
+
103
+ ```typescript
104
+ Gorgon.settings({
105
+ debug: true, // log cache hits/misses to console
106
+ defaultProvider: 'memory', // which storage provider to use
107
+ retry: 5000 // ms before a "stuck" request retries (default: 5000)
108
+ });
109
+ ```
110
+
111
+ ### Hook into cache lifecycle events
112
+
113
+ ```typescript
114
+ Gorgon.addHook('clear', (key, input, output) => {
115
+ console.log('Cache cleared:', input);
116
+ });
117
+
118
+ Gorgon.addHook('valueError', (key, input, output) => {
119
+ console.error('Cache function threw:', output);
120
+ });
121
+ ```
122
+
123
+ Available events: `settings`, `addProvider`, `put`, `clear`, `clearAll`, `overwrite`, `get`, `valueError`.
124
+
125
+ ### Custom storage providers
126
+
127
+ Implement `IGorgonCacheProvider` for Redis, IndexedDB, localStorage, etc.:
128
+
129
+ ```typescript
130
+ import Gorgon, { IGorgonCacheProvider, GorgonPolicySanitized } from '@gorgonjs/gorgon';
131
+
132
+ const myProvider: IGorgonCacheProvider = {
133
+ init: async () => {},
134
+ get: async (key: string) => { /* return cached value or undefined */ },
135
+ set: async <R>(key: string, value: R, policy: GorgonPolicySanitized): Promise<R> => {
136
+ /* store value, policy.expiry is ms or false */
137
+ return value;
138
+ },
139
+ clear: async (key?: string) => { /* clear key or all */ return true; },
140
+ keys: async () => { /* return all keys */ return []; },
141
+ };
142
+
143
+ Gorgon.addProvider('my-provider', myProvider);
144
+ Gorgon.settings({ defaultProvider: 'my-provider' });
145
+ ```
146
+
147
+ ### File provider — persist cache to disk (server-side)
148
+
149
+ ```typescript
150
+ import Gorgon from '@gorgonjs/gorgon';
151
+ import { FileProvider } from '@gorgonjs/file-provider';
152
+
153
+ const fileCache = FileProvider('./cache', {
154
+ createSubfolder: false, // true creates a dated subfolder
155
+ clearFolder: false // true clears the directory on init
156
+ });
157
+ Gorgon.addProvider('file', fileCache);
158
+
159
+ const movie = await Gorgon.get(`movie/${id}`, async () => {
160
+ return fetch(`https://api.example.com/movie/${id}`).then(r => r.json());
161
+ }, { provider: 'file', expiry: false });
162
+ ```
163
+
164
+ File provider uses `JSON.stringify` — only serializable data. For `Date`, `Set`, `Map`, use the `superjson` library.
165
+
166
+ ### ClearLink — sync cache clearing across servers
167
+
168
+ ```typescript
169
+ // Server
170
+ import { server } from '@gorgonjs/clearlink';
171
+ server.init({ port: 8686 });
172
+
173
+ // Client (on each app instance)
174
+ import Gorgon from '@gorgonjs/gorgon';
175
+ import { client } from '@gorgonjs/clearlink';
176
+
177
+ client.connect('ws://127.0.0.1:8686');
178
+ client.apply(Gorgon); // hooks into clear and clearAll events
179
+
180
+ // Now Gorgon.clear() on any instance broadcasts to all others
181
+ ```
182
+
183
+ Only `clear` and `clearAll` are synced (not `put` or auto-expiry). Auto-reconnects after 10 seconds on disconnect.
184
+
185
+ ## Common Mistakes
186
+
187
+ ### CRITICAL Not clearing cache after mutating underlying data
188
+
189
+ Wrong:
190
+
191
+ ```typescript
192
+ const user = await Gorgon.get(`user/${id}`, () => fetchUser(id), 60000);
193
+ await updateUser(id, newData);
194
+ // Forgot to clear — subsequent reads return stale data
195
+ ```
196
+
197
+ Correct:
198
+
199
+ ```typescript
200
+ const user = await Gorgon.get(`user/${id}`, () => fetchUser(id), 60000);
201
+ await updateUser(id, newData);
202
+ Gorgon.clear(`user/${id}`);
203
+ ```
204
+
205
+ Gorgon does not know when the underlying data changes. Always clear the relevant cache key after any mutation (POST, PUT, DELETE) that affects cached data.
206
+
207
+ Source: maintainer interview
208
+
209
+ ### CRITICAL Cache keys not specific enough to input parameters
210
+
211
+ Wrong:
212
+
213
+ ```typescript
214
+ const results = await Gorgon.get('search-results', () =>
215
+ searchAPI(query, page, filters), 60000);
216
+ ```
217
+
218
+ Correct:
219
+
220
+ ```typescript
221
+ const results = await Gorgon.get(
222
+ `search/${query}/${page}/${JSON.stringify(filters)}`,
223
+ () => searchAPI(query, page, filters), 60000);
224
+ ```
225
+
226
+ If the cache key does not include all varying parameters, different requests share the same cached result, returning wrong data silently.
227
+
228
+ Source: maintainer interview
229
+
230
+ ### HIGH Caching forever without expiry in production
231
+
232
+ Wrong:
233
+
234
+ ```typescript
235
+ await Gorgon.get('config', fetchConfig, false);
236
+ ```
237
+
238
+ Correct:
239
+
240
+ ```typescript
241
+ await Gorgon.get('config', fetchConfig, 24 * 60 * 60 * 1000); // 1 day
242
+ ```
243
+
244
+ Even rarely-changing data should have an expiry. Permanent caches become stale silently and are hard to debug in production.
245
+
246
+ Source: maintainer interview
247
+
248
+ ### MEDIUM Aligned cache expiry causing thundering herd
249
+
250
+ Wrong:
251
+
252
+ ```typescript
253
+ for (const id of popularIds) {
254
+ await Gorgon.get(`item/${id}`, () => fetchItem(id), 3600000);
255
+ }
256
+ ```
257
+
258
+ Correct:
259
+
260
+ ```typescript
261
+ for (const id of popularIds) {
262
+ const fuzz = Math.random() * 600000; // up to 10 min variance
263
+ await Gorgon.get(`item/${id}`, () => fetchItem(id), 3600000 + fuzz);
264
+ }
265
+ ```
266
+
267
+ When many items share the same TTL, they all expire simultaneously causing a burst of requests. Fuzz the expiry to spread cache refreshes over time.
268
+
269
+ Source: maintainer interview
270
+
271
+ ### HIGH Using a plain Map or global object instead of Gorgon
272
+
273
+ Wrong:
274
+
275
+ ```typescript
276
+ const cache = new Map();
277
+ async function getUser(id: number) {
278
+ if (cache.has(id)) return cache.get(id);
279
+ const user = await fetchUser(id);
280
+ cache.set(id, user);
281
+ return user;
282
+ }
283
+ ```
284
+
285
+ Correct:
286
+
287
+ ```typescript
288
+ import Gorgon from '@gorgonjs/gorgon';
289
+
290
+ const getUser = (id: number) =>
291
+ Gorgon.get(`user/${id}`, () => fetchUser(id), 60000);
292
+ ```
293
+
294
+ A hand-rolled cache misses concurrency deduplication (10 simultaneous calls hit the API 10 times), has no expiry management, no wildcard clearing, and no type safety on cached returns.
295
+
296
+ Source: documentation — concurrency
297
+
298
+ ### MEDIUM Wrapping Gorgon.get in custom deduplication logic
299
+
300
+ Wrong:
301
+
302
+ ```typescript
303
+ const pending = new Map<string, Promise<any>>();
304
+ async function getUser(id: number) {
305
+ const key = `user/${id}`;
306
+ if (pending.has(key)) return pending.get(key);
307
+ const promise = Gorgon.get(key, () => fetchUser(id), 60000);
308
+ pending.set(key, promise);
309
+ const result = await promise;
310
+ pending.delete(key);
311
+ return result;
312
+ }
313
+ ```
314
+
315
+ Correct:
316
+
317
+ ```typescript
318
+ import Gorgon from '@gorgonjs/gorgon';
319
+
320
+ const getUser = (id: number) =>
321
+ Gorgon.get(`user/${id}`, () => fetchUser(id), 60000);
322
+ ```
323
+
324
+ Gorgon already deduplicates concurrent requests for the same key. External dedup is redundant and can introduce bugs with stale promise references.
325
+
326
+ Source: documentation — concurrency
327
+
328
+ ### HIGH Tension: Cache duration vs data freshness
329
+
330
+ Longer cache times improve performance but increase risk of stale data. Agents optimizing for performance tend to set very long TTLs without an invalidation strategy; agents optimizing for correctness set very short TTLs, defeating the purpose of caching. Choose a TTL appropriate for the data's change frequency and always pair it with explicit `Gorgon.clear()` calls on mutation.
331
+
332
+ See also: `react/SKILL.md` § Common Mistakes
333
+
334
+ ## Version
335
+
336
+ Targets @gorgonjs/gorgon v1.6.0.
@@ -0,0 +1,242 @@
1
+ ---
2
+ name: react
3
+ description: >
4
+ React integration for @gorgonjs/gorgon via @gorgonjs/react: useGorgon hook
5
+ with data/error/loading/refetch state, clearGorgon helper for cache
6
+ invalidation from components, cache key management tied to component
7
+ lifecycle, and DIY hook patterns. Use when building React components
8
+ that need cached async data fetching.
9
+ type: framework
10
+ library: gorgon
11
+ framework: react
12
+ library_version: "1.6.0"
13
+ requires:
14
+ - core
15
+ sources:
16
+ - "mikevalstar/gorgon:clients/react/index.ts"
17
+ - "mikevalstar/gorgon:gorgonjs.dev/src/pages/docs/ui/react.md"
18
+ ---
19
+
20
+ This skill builds on `core/SKILL.md`. Read the core skill first for cache key naming, policies, and clearing patterns.
21
+
22
+ # Gorgon.js — React
23
+
24
+ ## Setup
25
+
26
+ ```bash
27
+ npm install @gorgonjs/react @gorgonjs/gorgon
28
+ ```
29
+
30
+ ```typescript
31
+ import { useGorgon, clearGorgon } from '@gorgonjs/react';
32
+
33
+ function UserProfile({ userId }: { userId: string }) {
34
+ const { data, error, loading, refetch } = useGorgon(
35
+ `user/${userId}`,
36
+ () => fetch(`/api/users/${userId}`).then(r => r.json()),
37
+ 60 * 1000
38
+ );
39
+
40
+ if (loading) return <p>Loading...</p>;
41
+ if (error) return <p>Error: {error.message}</p>;
42
+
43
+ return (
44
+ <div>
45
+ <h1>{data.name}</h1>
46
+ <button onClick={() => refetch()}>Refresh</button>
47
+ </div>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## Hooks and Components
53
+
54
+ ### useGorgon hook
55
+
56
+ ```typescript
57
+ const { data, error, loading, refetch } = useGorgon<R>(
58
+ key: string,
59
+ asyncFunc: () => Promise<R>,
60
+ policy?: GorgonPolicyInput,
61
+ options?: { debug?: boolean }
62
+ );
63
+ ```
64
+
65
+ Returns:
66
+ - `data: R | null` — resolved data, null while loading
67
+ - `error: Error | null` — error if the async function threw
68
+ - `loading: boolean` — true while fetching
69
+ - `refetch(opts?: { clearKey?: string }): void` — clears cache key and re-fetches
70
+
71
+ The hook re-fetches when `key` changes. Include all dynamic parameters in the key.
72
+
73
+ ### clearGorgon helper
74
+
75
+ ```typescript
76
+ import { clearGorgon } from '@gorgonjs/react';
77
+
78
+ clearGorgon('user/*'); // clear keys matching pattern
79
+ clearGorgon(); // clear all cached data
80
+ ```
81
+
82
+ ### Refetch with wildcard clearing
83
+
84
+ ```typescript
85
+ const { data, refetch } = useGorgon(
86
+ `user/${userId}`,
87
+ () => fetchUser(userId),
88
+ 60000
89
+ );
90
+
91
+ // Clear just this key and re-fetch
92
+ const handleRefresh = () => refetch();
93
+
94
+ // Clear all user keys and re-fetch this one
95
+ const handleClearAll = () => refetch({ clearKey: 'user/*' });
96
+ ```
97
+
98
+ ## React-Specific Patterns
99
+
100
+ ### Typed data fetching in components
101
+
102
+ ```typescript
103
+ import { useGorgon } from '@gorgonjs/react';
104
+
105
+ interface Todo { id: number; title: string; completed: boolean; }
106
+
107
+ function TodoItem({ id }: { id: number }) {
108
+ const { data, loading } = useGorgon<Todo>(
109
+ `todo/${id}`,
110
+ () => fetch(`/api/todos/${id}`).then(r => r.json()),
111
+ 30000
112
+ );
113
+
114
+ if (loading || !data) return <span>Loading...</span>;
115
+ return <span>{data.title}</span>;
116
+ }
117
+ ```
118
+
119
+ ### Invalidate on mutation
120
+
121
+ ```typescript
122
+ function EditUser({ userId }: { userId: string }) {
123
+ const { data, refetch } = useGorgon(
124
+ `user/${userId}`,
125
+ () => fetchUser(userId),
126
+ 60000
127
+ );
128
+
129
+ const handleSave = async (formData: UserUpdate) => {
130
+ await updateUser(userId, formData);
131
+ refetch(); // clears cache and re-renders with fresh data
132
+ };
133
+
134
+ return <UserForm data={data} onSave={handleSave} />;
135
+ }
136
+ ```
137
+
138
+ ### DIY minimal hook (without @gorgonjs/react)
139
+
140
+ ```typescript
141
+ import { useState, useEffect } from 'react';
142
+ import Gorgon, { GorgonPolicyInput } from '@gorgonjs/gorgon';
143
+
144
+ function useGorgon<R>(
145
+ key: string,
146
+ asyncFunc: () => Promise<R>,
147
+ policy?: GorgonPolicyInput
148
+ ): R | null {
149
+ const [data, setData] = useState<R | null>(null);
150
+
151
+ useEffect(() => {
152
+ let mounted = true;
153
+ Gorgon.get(key, asyncFunc, policy)
154
+ .then(result => { if (mounted) setData(result); })
155
+ .catch(err => console.error('Gorgon error', err));
156
+ return () => { mounted = false; };
157
+ }, [key]);
158
+
159
+ return data;
160
+ }
161
+ ```
162
+
163
+ ## Common Mistakes
164
+
165
+ ### CRITICAL Not including all dynamic params in the cache key
166
+
167
+ Wrong:
168
+
169
+ ```typescript
170
+ const { data } = useGorgon('user-profile', () => fetchUser(userId), 60000);
171
+ ```
172
+
173
+ Correct:
174
+
175
+ ```typescript
176
+ const { data } = useGorgon(`user/${userId}`, () => fetchUser(userId), 60000);
177
+ ```
178
+
179
+ `useGorgon` re-fetches when the key changes. If dynamic parameters are not in the key, changing `userId` returns stale data from the previous user.
180
+
181
+ Source: documentation — react
182
+
183
+ ### HIGH Using Gorgon.clear directly instead of refetch
184
+
185
+ Wrong:
186
+
187
+ ```typescript
188
+ const { data } = useGorgon('user/1', fetchUser, 60000);
189
+ const handleUpdate = async () => {
190
+ await updateUser(1, newData);
191
+ Gorgon.clear('user/1'); // cache is cleared but component doesn't re-render
192
+ };
193
+ ```
194
+
195
+ Correct:
196
+
197
+ ```typescript
198
+ const { data, refetch } = useGorgon('user/1', fetchUser, 60000);
199
+ const handleUpdate = async () => {
200
+ await updateUser(1, newData);
201
+ refetch(); // clears cache AND triggers re-render
202
+ };
203
+ ```
204
+
205
+ `Gorgon.clear()` removes the cached value but does not trigger a React re-render. Use `refetch()` from the hook to clear and re-fetch in one step.
206
+
207
+ Source: documentation — react
208
+
209
+ ### HIGH Missing cleanup in DIY hooks
210
+
211
+ Wrong:
212
+
213
+ ```typescript
214
+ useEffect(() => {
215
+ Gorgon.get(key, asyncFunc, policy).then(setData);
216
+ }, [key]);
217
+ ```
218
+
219
+ Correct:
220
+
221
+ ```typescript
222
+ useEffect(() => {
223
+ let mounted = true;
224
+ Gorgon.get(key, asyncFunc, policy)
225
+ .then(result => { if (mounted) setData(result); });
226
+ return () => { mounted = false; };
227
+ }, [key]);
228
+ ```
229
+
230
+ Without the mounted guard, setting state after unmount causes React warnings and potential bugs with rapid navigation.
231
+
232
+ Source: documentation — react
233
+
234
+ ### HIGH Tension: Cache duration vs data freshness
235
+
236
+ When using `useGorgon`, the same tension from core caching applies in the UI: long TTLs show stale data to users, short TTLs cause excessive re-fetching and loading spinners. Pair appropriate TTLs with explicit `refetch()` calls on user-initiated mutations.
237
+
238
+ See also: `core/SKILL.md` § Common Mistakes
239
+
240
+ ## Version
241
+
242
+ Targets @gorgonjs/react with @gorgonjs/gorgon v1.6.0.