@danieljvdm/dev-kit 0.4.0 → 0.6.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
@@ -20,7 +20,8 @@ Install the published Dev Kit package:
20
20
  bun add -d @danieljvdm/dev-kit
21
21
  ```
22
22
 
23
- Initialize the project, browse the approved catalog, and add skills:
23
+ Initialize the project, browse available built-in, approved Git, and installed
24
+ package skills, then add the ones you want:
24
25
 
25
26
  ```bash
26
27
  bun x dev-kit init
@@ -72,18 +73,33 @@ bun x dev-kit plan
72
73
  bun x dev-kit apply
73
74
  ```
74
75
 
75
- Commit the generated `dev-kit.lock.json`, then use locked mode in your package
76
- lifecycle:
76
+ Commit the generated `dev-kit.lock.json`, then let the package lifecycle
77
+ converge owned outputs automatically when installed packages change:
77
78
 
78
79
  ```jsonc
79
80
  {
80
81
  "scripts": {
81
- "postinstall": "dev-kit apply --locked"
82
+ "postinstall": "dev-kit apply"
82
83
  }
83
84
  }
84
85
  ```
85
86
 
86
- That single postinstall applies every task enabled in `dev-kit.jsonc`.
87
+ That single postinstall applies every task enabled in `dev-kit.jsonc` and
88
+ regenerates `dev-kit.lock.json` when an intentional package upgrade changes a
89
+ bundled or package-provided skill. Ownership and conflict checks still prevent
90
+ unreviewed overwrites.
91
+
92
+ Keep strict verification in CI. Either install with lifecycle scripts disabled
93
+ before running locked mode:
94
+
95
+ ```bash
96
+ bun install --ignore-scripts
97
+ bun x dev-kit apply --locked
98
+ ```
99
+
100
+ Or allow the normal postinstall and fail CI when it leaves tracked changes.
101
+ Do not run an unlocked apply before a locked verification because that would
102
+ regenerate the drift being checked.
87
103
 
88
104
  This repository dogfoods the same flow with its committed `dev-kit.jsonc` and
89
105
  `dev-kit.lock.json`. From this source checkout, invoke the local CLI with:
@@ -107,7 +123,7 @@ the root package itself.
107
123
  | `dev-kit remove <skill...>` | Deselect and uninstall skills safely. |
108
124
  | `dev-kit list [--all]` | List selected skills or browse the catalog. |
109
125
  | `dev-kit search <words...>` | Search names and descriptions. |
110
- | `dev-kit info <skill>` | Show description, source, and approved commit. |
126
+ | `dev-kit info <skill>` | Show description and Git or installed-package provenance. |
111
127
  | `dev-kit status` | Check whether the project matches its selection. |
112
128
  | `dev-kit sync` | Apply the current manifest. |
113
129
  | `dev-kit plan` | Preview project changes without writing files. |
@@ -149,7 +165,8 @@ tool versions. A project-local process lock also prevents concurrent applies.
149
165
 
150
166
  ## Manifest
151
167
 
152
- `include` accepts individual skill names and skill families:
168
+ `include` accepts static skill names, skill families, and explicit
169
+ `<package>#<skill>` selectors:
153
170
 
154
171
  ```jsonc
155
172
  {
@@ -159,9 +176,14 @@ tool versions. A project-local process lock also prevents concurrent applies.
159
176
  "effect",
160
177
  "workers-best-practices",
161
178
  "wrangler",
162
- "serve-sim"
179
+ "serve-sim",
180
+ "@tanstack/ai#ai-core"
163
181
  ],
164
182
  "exclude": ["animation-vocabulary"],
183
+ "setup": {
184
+ "agentInstructions": { "enabled": true },
185
+ "claudeInstructions": { "enabled": true }
186
+ },
165
187
  "targets": {
166
188
  "agents": { "enabled": true, "mode": "copy" },
167
189
  "claude": { "enabled": true, "mode": "symlink" },
@@ -171,10 +193,13 @@ tool versions. A project-local process lock also prevents concurrent applies.
171
193
  ```
172
194
 
173
195
  - `dev-kit` installs guidance for operating the toolkit itself.
174
- - `effect` expands to the consolidated `effect-ts` skill.
196
+ - `effect` expands to `effect-ts` plus focused Effect Atom HTTP data-fetching
197
+ guidance.
175
198
  - Prefer individual external skills such as `workers-best-practices` and
176
199
  `wrangler`, selected after scanning the project for relevant technologies.
177
200
  - `serve-sim` selects the approved Evan Bacon simulator skill directly.
201
+ - `@tanstack/ai#ai-core` explicitly selects a skill discovered in that direct
202
+ project dependency; discovery alone never selects it.
178
203
  - An approved source ID is broad shorthand that selects every skill from that
179
204
  source. Use it only when the scan confirms that every member applies.
180
205
 
@@ -183,6 +208,34 @@ Dev Kit reserves `.repos/<source-id>` for project-local source checkouts. Run
183
208
  The patch is idempotent, preserves existing lines, and refuses symlinked
184
209
  `.gitignore` files.
185
210
 
211
+ ## Agent instructions
212
+
213
+ Enable a managed project-root instruction wrapper and a portable Claude Code
214
+ bridge in the manifest:
215
+
216
+ ```jsonc
217
+ {
218
+ "include": ["dev-kit"],
219
+ "setup": {
220
+ "agentInstructions": { "enabled": true },
221
+ "claudeInstructions": { "enabled": true }
222
+ }
223
+ }
224
+ ```
225
+
226
+ `setup.agentInstructions` manages `AGENTS.md` as a generated wrapper with a
227
+ short description of dev-kit and a pointer to the installed `dev-kit` skill.
228
+ When the root `package.json` declares `vite-plus` directly, the wrapper also
229
+ includes Vite+'s installed `node_modules/vite-plus/AGENTS.md` instructions.
230
+ Transitive installations do not opt a project in.
231
+
232
+ `setup.claudeInstructions` manages `CLAUDE.md` as the relative symlink
233
+ `CLAUDE.md → AGENTS.md`. It can link to the generated wrapper in the same apply,
234
+ or retain the older behavior of linking to an existing regular `AGENTS.md` when
235
+ the wrapper task is disabled. Both outputs are recorded independently in the
236
+ lockfile and local ownership state. Dev Kit refuses to replace unowned files
237
+ and removes only unchanged owned outputs.
238
+
186
239
  ## Effect source checkout
187
240
 
188
241
  Enable a local checkout of the exact installed Effect release in the manifest:
@@ -248,10 +301,65 @@ troubleshooting the task directly.
248
301
  Package and tsconfig edits remain explicit until Dev Kit can safely own parts
249
302
  of shared JSONC files.
250
303
 
251
- ## Approved external skills
304
+ ## Installed package skills
252
305
 
253
- This repository is an opinionated catalog, not a mirror of every upstream skill
254
- tree. `skill-sources.jsonc` declares sources Dan has approved:
306
+ Dev Kit generically discovers agent skills bundled by the project's installed
307
+ JavaScript packages. It reads the project's direct dependencies, checks the
308
+ package's Intent v1 discovery metadata (or Intent's repository-metadata
309
+ fallback), then looks for the layout
310
+ `node_modules/<package>/skills/<skill>/SKILL.md`.
311
+ TanStack is one publisher of this layout; no TanStack package names or skill
312
+ paths are hard-coded into Dev Kit.
313
+
314
+ Discovery is browse-only. These commands show an installed package skill but do
315
+ not select, copy, symlink, lock, or otherwise install it:
316
+
317
+ ```bash
318
+ bun x dev-kit list --all
319
+ bun x dev-kit search tanstack
320
+ bun x dev-kit info @tanstack/ai#ai-core
321
+ ```
322
+
323
+ Selection is explicit and package-qualified:
324
+
325
+ ```bash
326
+ bun x dev-kit add @tanstack/ai#ai-core
327
+ ```
328
+
329
+ That writes `@tanstack/ai#ai-core` to `dev-kit.jsonc` and, unless
330
+ `--no-apply` is passed, installs it through the normal ownership-safe sync
331
+ path. The qualifier prevents ambiguity when two dependencies publish the same
332
+ skill name. Two selected skills that would both write the same destination are
333
+ rejected before any output is changed.
334
+
335
+ The initial compatibility boundary is intentionally small and deterministic:
336
+
337
+ - only packages named in the root project's `dependencies`,
338
+ `devDependencies`, `optionalDependencies`, or `peerDependencies` are
339
+ scanned;
340
+ - package code is never imported or executed;
341
+ - npm-style and pnpm/workspace symlinks under `node_modules` are supported;
342
+ - Yarn Plug'n'Play and transitive dependency traversal are not scanned; and
343
+ - immediate `skills/<name>/SKILL.md` roots are listed. Nested topic skills and
344
+ references remain part of that root and are copied with it.
345
+
346
+ The last rule adapts Intent's routed, nested skill trees to the immediate folder
347
+ and frontmatter-name invariants expected by Agent Skills targets. Dev Kit does
348
+ not rewrite nested names or ask Intent to manage agent configuration.
349
+
350
+ The project `dev-kit.lock.json` records the selected package name, installed
351
+ version, skill name, and content digest. `apply --locked` therefore rejects
352
+ package-version or skill-content drift. Dev Kit never downloads a missing
353
+ package or substitutes a registry version.
354
+
355
+ See TanStack's
356
+ [Agent Skills documentation](https://tanstack.com/ai/latest/docs/getting-started/agent-skills)
357
+ for a real package suite that uses this convention.
358
+
359
+ ## Approved external Git skills
360
+
361
+ This repository remains an opinionated catalog for Git-hosted skills.
362
+ `skill-sources.jsonc` contains only reviewed Git sources:
255
363
 
256
364
  ```jsonc
257
365
  {
@@ -276,38 +384,25 @@ bun run catalog:refresh
276
384
  bun run catalog:check
277
385
  ```
278
386
 
279
- Adding a source does not require editing JSONC:
387
+ Adding a Git source does not require editing JSONC:
280
388
 
281
389
  ```bash
282
- # Opens a skill picker in a terminal
283
390
  dev-kit catalog add https://github.com/owner/repository
284
-
285
- # Explicit and automation-friendly
286
391
  dev-kit catalog add https://github.com/owner/repository \
287
392
  --skill one --skill two
288
393
  dev-kit catalog add https://github.com/owner/repository --all
289
394
  ```
290
395
 
291
- GitHub tree URLs are accepted, so a URL such as
292
- `https://github.com/owner/repository/tree/main/skills` supplies the repository,
293
- ref, and skills path together. `--all` expands to the skills discovered at that
396
+ GitHub tree URLs are accepted. `--all` expands to the skills found at that
294
397
  exact snapshot; it never writes a wildcard that could silently approve a future
295
- upstream addition.
296
-
297
- The current catalog includes approved snapshots from Emil Kowalski,
298
- Cloudflare, and Evan Bacon. Inspect them with `dev-kit catalog list` and
299
- `dev-kit catalog info <source>`.
300
-
301
- The refresh resolves refs to exact commits, validates names and paths, rejects
302
- symlinks and collisions, extracts short descriptions, and updates
303
- `skill-sources.lock.json`. It does not copy upstream skill trees into this
304
- repository.
305
-
306
- When a consuming project selects an external skill, Dev Kit fetches that exact
307
- approved commit into the ignored `.dev-kit/cache`, applies declared compatibility
308
- transforms, and installs the result through the ownership-safe sync path. Normal
309
- installs never float to a newer upstream commit; only a reviewed catalog refresh
310
- changes what is approved.
398
+ upstream addition. Catalog refresh resolves refs to exact commits, validates
399
+ names and paths, rejects symlinks and collisions, extracts descriptions, and
400
+ updates `skill-sources.lock.json`.
401
+
402
+ When a project selects one of these Git-backed skills, Dev Kit fetches the
403
+ approved commit into the ignored `.dev-kit/cache` and installs it through the
404
+ same ownership-safe sync path. Only a reviewed catalog refresh changes the
405
+ approved Git content.
311
406
 
312
407
  ## Oxlint and Oxfmt configurations
313
408
 
@@ -2,6 +2,8 @@
2
2
  "$schema": "./schema/dev-kit.schema.json",
3
3
  "include": ["dev-kit", "effect"],
4
4
  "setup": {
5
+ "agentInstructions": { "enabled": true },
6
+ "claudeInstructions": { "enabled": true },
5
7
  "effectSource": { "enabled": true },
6
8
  "effectTsgo": { "enabled": true }
7
9
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Declarative project development toolkit with portable agent skills.",
@@ -40,7 +40,8 @@
40
40
  "skills/",
41
41
  "skill-sources.jsonc",
42
42
  "skill-sources.lock.json",
43
- "src/"
43
+ "src/",
44
+ "templates/"
44
45
  ],
45
46
  "scripts": {
46
47
  "prepare": "./bin/dev-kit.mjs apply --locked",
@@ -10,21 +10,21 @@
10
10
  "type": "string"
11
11
  },
12
12
  "include": {
13
- "description": "Skill names or family names to sync. Prefer individually relevant external skills; an external source id selects every approved skill from that source.",
13
+ "description": "Static skill names, family names, or exact <package>#<skill> selectors to sync. Installed package skills are discovered for browsing but selected only when explicitly included.",
14
14
  "type": "array",
15
15
  "items": {
16
16
  "type": "string",
17
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
17
+ "pattern": "^(?:[a-z0-9]+(?:-[a-z0-9]+)*|(?:[a-z0-9][a-z0-9._-]*|@[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*)#[a-z0-9]+(?:-[a-z0-9]+)*)$"
18
18
  },
19
19
  "uniqueItems": true,
20
20
  "minItems": 0
21
21
  },
22
22
  "exclude": {
23
- "description": "Skill names or family names to remove from the selected set after expanding includes.",
23
+ "description": "Static skill names, family names, or exact <package>#<skill> selectors to remove after expanding includes.",
24
24
  "type": "array",
25
25
  "items": {
26
26
  "type": "string",
27
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
27
+ "pattern": "^(?:[a-z0-9]+(?:-[a-z0-9]+)*|(?:[a-z0-9][a-z0-9._-]*|@[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*)#[a-z0-9]+(?:-[a-z0-9]+)*)$"
28
28
  },
29
29
  "uniqueItems": true,
30
30
  "default": []
@@ -34,6 +34,12 @@
34
34
  "type": "object",
35
35
  "additionalProperties": false,
36
36
  "properties": {
37
+ "agentInstructions": {
38
+ "$ref": "#/$defs/agentInstructionsSetup"
39
+ },
40
+ "claudeInstructions": {
41
+ "$ref": "#/$defs/claudeInstructionsSetup"
42
+ },
37
43
  "effectSource": {
38
44
  "$ref": "#/$defs/effectSourceSetup"
39
45
  },
@@ -58,6 +64,28 @@
58
64
  },
59
65
  "required": ["include"],
60
66
  "$defs": {
67
+ "agentInstructionsSetup": {
68
+ "description": "Manage a project-root AGENTS.md wrapper with dev-kit guidance and conditional tool instructions.",
69
+ "type": "object",
70
+ "additionalProperties": false,
71
+ "properties": {
72
+ "enabled": {
73
+ "type": "boolean",
74
+ "default": false
75
+ }
76
+ }
77
+ },
78
+ "claudeInstructionsSetup": {
79
+ "description": "Manage CLAUDE.md as a relative symlink to the project-root AGENTS.md file.",
80
+ "type": "object",
81
+ "additionalProperties": false,
82
+ "properties": {
83
+ "enabled": {
84
+ "type": "boolean",
85
+ "default": false
86
+ }
87
+ }
88
+ },
61
89
  "effectSourceSetup": {
62
90
  "type": "object",
63
91
  "additionalProperties": false,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dev-kit
3
- description: Dev-kit operations for projects that configure dev-kit.jsonc, sync portable skills, run plan/apply or locked postinstalls, maintain dev-kit.lock.json, resolve ownership conflicts, patch managed ignores, or enable Effect TypeScript-Go.
3
+ description: Dev-kit operations for projects that configure dev-kit.jsonc, sync portable skills, run plan/apply or automatic postinstalls, perform locked CI checks, maintain dev-kit.lock.json, resolve ownership conflicts, patch managed ignores, or enable Effect TypeScript-Go.
4
4
  ---
5
5
 
6
6
  # Dev Kit
@@ -51,8 +51,12 @@ stores explicit skill names and exact commit/content digests.
51
51
  6. Resolve conflicts, then run `dev-kit apply`. Commit the manifest and
52
52
  regenerated `dev-kit.lock.json`; keep `.dev-kit/` local. Finish when a second
53
53
  plan reports only unchanged resources and setup tasks.
54
- 7. Use `dev-kit apply --locked` in CI and the package lifecycle. Finish when a
55
- clean install converges from the committed manifest and lock.
54
+ 7. Use `dev-kit apply` in the package lifecycle so intentional dependency
55
+ upgrades regenerate owned outputs and `dev-kit.lock.json`. For strict CI,
56
+ either disable lifecycle scripts before `dev-kit apply --locked`, or run the
57
+ normal lifecycle and require the tracked working tree to remain clean. Never
58
+ run an unlocked apply before locked verification. Finish when a clean install
59
+ converges from the committed manifest and lock.
56
60
 
57
61
  ## Manifest
58
62
 
@@ -67,6 +71,10 @@ skill as `dev-kit` when project agents should carry the toolkit procedure.
67
71
  "$schema": "./node_modules/@danieljvdm/dev-kit/schema/dev-kit.schema.json",
68
72
  "include": ["dev-kit", "effect"],
69
73
  "exclude": [],
74
+ "setup": {
75
+ "agentInstructions": { "enabled": true },
76
+ "claudeInstructions": { "enabled": true }
77
+ },
70
78
  "targets": {
71
79
  "agents": { "enabled": true, "mode": "copy" },
72
80
  "claude": { "enabled": true, "mode": "symlink" },
@@ -80,6 +88,15 @@ use symlinks for additional harness discovery paths. Keep every target path
80
88
  project-relative and separate from the manifest, lock, state, and process-lock
81
89
  paths.
82
90
 
91
+ Enable `setup.agentInstructions` to manage a project-root `AGENTS.md` wrapper
92
+ that points agents back to this skill. When `vite-plus` is a declared direct
93
+ dependency, dev-kit includes its installed agent instructions in the wrapper.
94
+ Enable `setup.claudeInstructions` when Claude Code should consume the same
95
+ project-root instructions; it manages `CLAUDE.md` as a relative symlink to the
96
+ wrapper or to an existing regular `AGENTS.md`. Preserve conflicting paths;
97
+ when disabled, dev-kit removes only unchanged outputs recorded in local
98
+ ownership state.
99
+
83
100
  ## Ownership and conflicts
84
101
 
85
102
  Dev-kit adopts an existing destination only when its digest exactly matches a
@@ -109,11 +126,17 @@ For one lifecycle entry point, configure:
109
126
  ```jsonc
110
127
  {
111
128
  "scripts": {
112
- "postinstall": "dev-kit apply --locked"
129
+ "postinstall": "dev-kit apply"
113
130
  }
114
131
  }
115
132
  ```
116
133
 
134
+ This intentionally refreshes the committed lock and owned outputs when the
135
+ package manager installs a new Dev Kit or selected package-skill version.
136
+ Review and commit those changes with the dependency update. Keep
137
+ `dev-kit apply --locked` as a verification command, not the normal local
138
+ lifecycle; in CI, run it only before any unlocked apply.
139
+
117
140
  ## Effect source checkout
118
141
 
119
142
  Enable the source task when agents should have canonical source matching the
@@ -195,8 +218,10 @@ in the consuming project.
195
218
 
196
219
  ## Current boundary
197
220
 
198
- Manage skill outputs, the `setup.effectSource` checkout, and the explicit
199
- `setup.effectTsgo` task. Edit shared `package.json` and `tsconfig.json`
221
+ Manage skill outputs, the `setup.agentInstructions` wrapper, the
222
+ `setup.claudeInstructions` link, the
223
+ `setup.effectSource` checkout, and the explicit `setup.effectTsgo` task. Edit
224
+ shared `package.json` and `tsconfig.json`
200
225
  contributions deliberately. The Oxlint and Oxfmt configurations are composable
201
226
  package exports, not manifest-managed outputs. Treat broader setup tasks as
202
227
  future manifest capabilities until the installed CLI exposes them.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: effect-atom-data-fetching
3
+ description: Design, implement, debug, or review HTTP data fetching with Effect Atom in React applications. Use for AtomHttpApi.Service queries and mutations, parameterized Atom.family caches, RegistryProvider and runtime placement, TTL/SWR/polling behavior, reactivity-key invalidation, framework-appropriate SSR and focus handling, AsyncResult aggregation resets, and deterministic cache lifecycle tests.
4
+ ---
5
+
6
+ # Effect Atom Data Fetching
7
+
8
+ Model server data as stable atoms owned outside React renders. Give the application one intentional registry boundary and one compatible runtime factory; then choose cache retention, freshness, polling, and invalidation independently.
9
+
10
+ ## Workflow
11
+
12
+ 1. Inspect the installed `effect` and `@effect/atom-react` versions and their source before copying signatures. These APIs live under `effect/unstable/reactivity` and may move.
13
+ 2. Locate every `RegistryProvider`, runtime factory, query atom, mutation atom, and route-level `AsyncResult.all`. Draw the ownership boundary before changing behavior.
14
+ 3. Keep one `RegistryProvider` for the intended client application lifetime. Define the shared runtime factory, API service, query families, and mutation atoms at module scope rather than in components.
15
+ 4. Choose each lifecycle control for its actual job:
16
+ - idle `timeToLive`: retain an unused registry value before disposal;
17
+ - `Atom.swr({ staleTime })`: decide when mount/focus revalidation is needed;
18
+ - `Atom.withRefresh`: force periodic refresh while mounted.
19
+ 5. Give queries stable identities and matching reactivity keys. Let successful mutations invalidate those keys rather than manually coordinating every consumer.
20
+ 6. Keep browser-only signals behind an SSR-safe boundary. Decide explicitly whether initial data is client-only or hydrated from a request-scoped server registry.
21
+ 7. Verify lifecycle behavior with fake time and request counters, not sleeps.
22
+
23
+ ## Ownership rules
24
+
25
+ - Treat a query as shared read state: export one atom or `Atom.family` and let components subscribe.
26
+ - Treat an action as an event owned by the initiating UI or workflow: export the mutation atom, invoke it with `useAtomSet`, and observe its result only where useful.
27
+ - Never allocate a query atom in render. For parameterized queries, use a stable scalar or Effect `Hash`/`Equal` value as the family argument.
28
+ - Ensure every input to a route-level `AsyncResult.all` has stable atom identity and compatible retention. One newly allocated or immediately evicted input returns to `Initial` and makes the whole aggregate appear reset even when the other inputs remain cached.
29
+ - Do not describe manual refresh or polling as freshness caching. Refresh is forceful; `staleTime` only gates SWR's automatic mount/focus decisions.
30
+
31
+ ## References
32
+
33
+ - Read `./references/cache-lifecycle.md` for registry scope, runtime memoization, families, TTL, SWR, polling, and the aggregation trap.
34
+ - Read `./references/http-and-invalidation.md` for `AtomHttpApi.Service`, query/mutation ownership, reactivity-key vocabulary, and invalidation.
35
+ - Read `./references/tanstack-start.md` only for its framework-specific provider placement, SSR isolation, hydration, and focus guidance.
36
+ - Read `./references/testing.md` when adding or diagnosing lifecycle tests.
37
+
38
+ ## Completion check
39
+
40
+ Confirm one registry boundary, stable atom identity, deliberate TTL/staleness/polling values, matching query and mutation keys, SSR-safe browser access, and tests for every lifecycle behavior changed.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Effect Atom Data Fetching"
3
+ short_description: "Cache HTTP data safely with Effect Atom"
4
+ default_prompt: "Use $effect-atom-data-fetching to design or debug Effect Atom HTTP queries, caching, polling, invalidation, and SSR integration in a React app."
@@ -0,0 +1,72 @@
1
+ # Cache lifecycle
2
+
3
+ ## Registry and runtime scope
4
+
5
+ `RegistryProvider` creates one `AtomRegistry` on its first render. Its options do not rebuild that registry later. Provider unmount schedules disposal after a short grace period, so a quick React remount can reuse the same registry; moving or keying the provider still changes the cache boundary.
6
+
7
+ Place one provider around the client application subtree that should share data. Nested or route-local providers create separate caches.
8
+
9
+ An atom runtime and a registry solve different problems:
10
+
11
+ - the registry stores atom nodes, values, subscriptions, idle timers, and finalizers;
12
+ - `Atom.context({ memoMap })` creates runtimes that share `Layer` construction through one `Layer.MemoMap`;
13
+ - the module-level `Atom.runtime` uses Effect's module-level default memo map.
14
+
15
+ Create one client runtime factory when several API/services must share layers:
16
+
17
+ ```ts
18
+ import { Layer } from "effect";
19
+ import { Atom } from "effect/unstable/reactivity";
20
+
21
+ export const appAtomRuntime = Atom.context({
22
+ memoMap: Layer.makeMemoMapUnsafe(),
23
+ });
24
+ ```
25
+
26
+ Pass `appAtomRuntime` to each `AtomHttpApi.Service`. Do not create a memo map per query or component. On an SSR server, do not put request-specific authentication or services into a process-global memo map; use a request-scoped atom environment or keep the atom data path client-only.
27
+
28
+ ## Stable identity and families
29
+
30
+ Export fixed queries directly. Use `Atom.family` when a parameter selects the resource:
31
+
32
+ ```ts
33
+ export const projectAtom = Atom.family((projectId: string) =>
34
+ ApiClient.query("projects", "get", {
35
+ params: { projectId },
36
+ timeToLive: "5 minutes",
37
+ reactivityKeys: { projects: [projectId] },
38
+ }).pipe(Atom.swr({ staleTime: "30 seconds", revalidateOnMount: true })),
39
+ );
40
+ ```
41
+
42
+ The family must receive a stable key. Prefer a primitive ID. If the key is an object, give it deliberate Effect `Equal`/`Hash` semantics or reuse the same object; repeated object literals can produce distinct family entries.
43
+
44
+ ## Three independent clocks
45
+
46
+ | Control | Clock starts | What happens | What it does not mean |
47
+ | ----------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
48
+ | Registry/default idle TTL or query `timeToLive` | When an atom becomes unused | The registry keeps the cached node until idle eviction | The value is fresh during that period |
49
+ | `Atom.swr({ staleTime })` | From the latest success timestamp | A stale value can revalidate automatically on mount or focus while the prior success remains visible | The node survives an unmount long enough to be reused |
50
+ | `Atom.withRefresh(interval)` | While the wrapper is mounted | A timer force-refreshes the source and is canceled on disposal | Fresh requests are skipped |
51
+
52
+ Set retention long enough for the navigation/remount reuse window. `staleTime` cannot rescue a source that idle eviction already removed. A common policy is a longer `timeToLive`, a shorter `staleTime`, and polling only on screens that truly need it.
53
+
54
+ Manual `registry.refresh`, `useAtomRefresh`, invalidation, and `Atom.withRefresh` are forceful. They do not consult SWR freshness. Polling stops when the polling wrapper's lifetime is disposed because its finalizer clears the timer; applying `keepAlive` to that wrapper intentionally keeps polling alive.
55
+
56
+ ## The `AsyncResult.all` route reset
57
+
58
+ `AsyncResult.all` returns the first non-success input. Therefore a route aggregate is only as reusable as its least-stable input:
59
+
60
+ ```ts
61
+ const routeDataAtom = Atom.make((get) =>
62
+ AsyncResult.all({
63
+ project: get(projectAtom("p-1")),
64
+ // Bad if created during render or rebuilt for every route visit:
65
+ preferences: get(makePreferencesAtom()),
66
+ }),
67
+ );
68
+ ```
69
+
70
+ If `preferences` is a fresh or evicted atom, it starts at `Initial`; the aggregate also looks initial even though `project` is cached. Fix the input's ownership and retention. Define a singleton/family atom outside render and give it a deliberate idle TTL. Memoizing only the `AsyncResult.all` call does not repair an unstable input atom.
71
+
72
+ `AsyncResult.all` also constructs a new success container. Keep aggregation inside a derived atom so the registry controls recomputation instead of rebuilding the container ad hoc in render.
@@ -0,0 +1,93 @@
1
+ # HTTP queries and invalidation
2
+
3
+ ## Build one API service
4
+
5
+ `AtomHttpApi.Service` generates a typed client, a runtime, query atoms, and mutation functions. Pass the shared runtime factory so API services share the intended `Layer.MemoMap`:
6
+
7
+ ```ts
8
+ import { FetchHttpClient } from "effect/unstable/http";
9
+ import { AtomHttpApi } from "effect/unstable/reactivity";
10
+ import { appAtomRuntime } from "./atom-runtime";
11
+ import { Api } from "./api";
12
+
13
+ export const ApiClient = AtomHttpApi.Service()("ApiClient", {
14
+ api: Api,
15
+ httpClient: FetchHttpClient.layer,
16
+ baseUrl: "/api",
17
+ runtime: appAtomRuntime,
18
+ });
19
+ ```
20
+
21
+ Compile examples against the installed Effect version; the reactivity and HTTP APIs are unstable.
22
+
23
+ ## Queries
24
+
25
+ `query(group, endpoint, request)` returns an `Atom<AsyncResult<...>>`. The service internally memoizes request keys with a family. A public `Atom.family` remains useful for expressing domain ownership with a simple, stable parameter and applying one cache policy.
26
+
27
+ Query options have separate roles:
28
+
29
+ - `timeToLive`: finite values apply idle TTL; infinity keeps the query alive;
30
+ - `reactivityKeys`: register the query for refresh after matching invalidation;
31
+ - `serializationKey`: make decoded-only results serializable for hydration; it is not the runtime cache key;
32
+ - `responseMode`: changes the response and error shape.
33
+
34
+ Never place secrets in `serializationKey`, URL state, hydration payloads, or client-visible layers.
35
+
36
+ ## Mutations and action ownership
37
+
38
+ Create mutation atoms once, then invoke them from the component or workflow that owns the action:
39
+
40
+ ```ts
41
+ export const updateProject = ApiClient.mutation("projects", "update");
42
+
43
+ // In the initiating component:
44
+ const mutate = useAtomSet(updateProject, { mode: "promise" });
45
+ await mutate({
46
+ params: { projectId },
47
+ payload: patch,
48
+ reactivityKeys: { projects: [projectId] },
49
+ });
50
+ ```
51
+
52
+ The mutation's `reactivityKeys` are invalidated only after the request succeeds. Failed mutations do not invalidate. Keep navigation, toasts, dialog closure, and optimistic UI at the action owner; keep shared server-state refresh in reactivity keys.
53
+
54
+ ## Use one key vocabulary
55
+
56
+ Array keys represent independent keys. Record keys support hierarchical broad-plus-entity invalidation:
57
+
58
+ ```ts
59
+ const listKeys = { projects: [] };
60
+ const detailKeys = { projects: [projectId] };
61
+
62
+ ApiClient.query("projects", "list", { reactivityKeys: listKeys });
63
+ ApiClient.query("projects", "get", { params: { projectId }, reactivityKeys: detailKeys });
64
+
65
+ // This mutation invalidates the broad namespace and this entity key.
66
+ await mutate({ params: { projectId }, payload: patch, reactivityKeys: detailKeys });
67
+ ```
68
+
69
+ Record semantics register the property name and each `property:id` combination. Consequently, `{ projects: [projectId] }` is hierarchical: it invalidates both the broad `projects` namespace and the specific `projects:projectId` key. Every record-form project query also subscribed to that broad namespace can refresh. Use this when an entity write may affect lists or aggregates.
70
+
71
+ For exact entity-only invalidation, use namespaced primitive array keys consistently instead:
72
+
73
+ ```ts
74
+ const projectKey = (id: string) => `project:${id}`;
75
+
76
+ const detailKeys = [projectKey(projectId)];
77
+ const collectionKeys = ["projects"];
78
+
79
+ ApiClient.query("projects", "get", { params: { projectId }, reactivityKeys: detailKeys });
80
+ await mutate({ params: { projectId }, payload: patch, reactivityKeys: detailKeys });
81
+ ```
82
+
83
+ Use `collectionKeys` for the collection query and for mutations that can change membership or ordering.
84
+
85
+ Standardize key constructors in one module when several endpoints share them; mismatched strings fail silently.
86
+
87
+ Choose invalidation breadth from the server write:
88
+
89
+ - invalidate an exact array-form entity key when only one detail changed;
90
+ - invalidate the collection key, or use hierarchical record keys, when list membership, ordering, totals, or filters can change;
91
+ - invalidate multiple record properties when a write affects related aggregates.
92
+
93
+ Do not both invalidate and manually refresh the same query unless two requests are intentional.