@danieljvdm/dev-kit 0.5.0 → 0.7.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 +161 -68
- package/dev-kit.example.jsonc +8 -3
- package/package.json +19 -15
- package/schema/dev-kit.schema.json +52 -0
- package/skill-sources.jsonc +8 -12
- package/skill-sources.lock.json +3 -9
- package/skills/dev-kit/SKILL.md +74 -24
- package/skills/effect-atom-data-fetching/SKILL.md +40 -0
- package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
- package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
- package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
- package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
- package/skills/effect-atom-data-fetching/references/testing.md +63 -0
- package/skills/effect-ts/agents/openai.yaml +0 -1
- package/skills/effect-ts/references/audit-services.md +11 -11
- package/skills/effect-ts/references/guide-effect.md +56 -69
- package/skills/effect-ts/references/guide-error-handling.md +64 -73
- package/skills/effect-ts/references/guide-layers.md +187 -215
- package/skills/effect-ts/references/guide-observability.md +91 -116
- package/skills/effect-ts/references/guide-retries.md +32 -44
- package/skills/effect-ts/references/guide-schedule.md +26 -40
- package/skills/effect-ts/references/guide-schema.md +50 -57
- package/skills/effect-ts/references/guide-sql.md +47 -50
- package/skills/effect-ts/references/guide-testing.md +96 -98
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
- package/skills/effect-ts/references/version-and-source.md +0 -1
- package/src/bin/dev-kit.ts +61 -28
- package/src/catalog-manager.ts +86 -34
- package/src/catalog.ts +72 -34
- package/src/cli-ui.ts +20 -16
- package/src/effect-source.ts +49 -19
- package/src/effect-tsgo.ts +66 -35
- package/src/gitignore.ts +19 -6
- package/src/index.ts +12 -0
- package/src/manifest.ts +51 -3
- package/src/node-symbolic-link.ts +3 -0
- package/src/oxlint-plugin-effect.js +3 -0
- package/src/oxlint-plugin-style.d.ts +8 -0
- package/src/oxlint-plugin-style.js +8 -0
- package/src/oxlint.js +14 -0
- package/src/oxlint.ts +14 -0
- package/src/package-skill-source.ts +190 -75
- package/src/path-digest.ts +37 -10
- package/src/project-package.ts +59 -0
- package/src/project-process-lock.ts +19 -12
- package/src/project-state.ts +29 -2
- package/src/skill-manager.ts +134 -55
- package/src/skill-selector.ts +8 -2
- package/src/source-manifest.ts +2 -6
- package/src/sync.ts +491 -121
- package/src/vendor.ts +112 -42
- package/src/vite-plus-hooks.ts +174 -0
- package/src/vite-plus-quality.ts +49 -0
- package/templates/AGENTS.md +9 -0
- package/templates/vite-plus/github-actions-check.yml +44 -0
- package/templates/vite-plus/vite.config.ts +22 -0
package/skills/dev-kit/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
@@ -28,7 +28,7 @@ stores explicit skill names and exact commit/content digests.
|
|
|
28
28
|
repository actually uses; do not infer capabilities from a product or
|
|
29
29
|
company name alone.
|
|
30
30
|
2. Run `dev-kit list --all`, then use `dev-kit search <terms>` and `dev-kit info
|
|
31
|
-
|
|
31
|
+
<skill>` for each capability in the inventory. Compare every candidate's
|
|
32
32
|
trigger description with concrete repository evidence. Keep explicitly
|
|
33
33
|
requested creative or advisory skills even when they have no mechanical
|
|
34
34
|
dependency signal.
|
|
@@ -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
|
|
55
|
-
|
|
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
|
|
|
@@ -68,13 +72,18 @@ skill as `dev-kit` when project agents should carry the toolkit procedure.
|
|
|
68
72
|
"include": ["dev-kit", "effect"],
|
|
69
73
|
"exclude": [],
|
|
70
74
|
"setup": {
|
|
71
|
-
"
|
|
75
|
+
"agentInstructions": { "enabled": true },
|
|
76
|
+
"claudeInstructions": { "enabled": true },
|
|
77
|
+
"vitePlus": {
|
|
78
|
+
"hooks": { "enabled": true },
|
|
79
|
+
"quality": { "enabled": true },
|
|
80
|
+
},
|
|
72
81
|
},
|
|
73
82
|
"targets": {
|
|
74
83
|
"agents": { "enabled": true, "mode": "copy" },
|
|
75
84
|
"claude": { "enabled": true, "mode": "symlink" },
|
|
76
|
-
"opencode": { "enabled": false, "mode": "symlink" }
|
|
77
|
-
}
|
|
85
|
+
"opencode": { "enabled": false, "mode": "symlink" },
|
|
86
|
+
},
|
|
78
87
|
}
|
|
79
88
|
```
|
|
80
89
|
|
|
@@ -83,11 +92,36 @@ use symlinks for additional harness discovery paths. Keep every target path
|
|
|
83
92
|
project-relative and separate from the manifest, lock, state, and process-lock
|
|
84
93
|
paths.
|
|
85
94
|
|
|
95
|
+
Enable `setup.agentInstructions` to manage a project-root `AGENTS.md` wrapper
|
|
96
|
+
that points agents back to this skill. When `vite-plus` is a declared direct
|
|
97
|
+
dependency, dev-kit includes its installed agent instructions in the wrapper.
|
|
86
98
|
Enable `setup.claudeInstructions` when Claude Code should consume the same
|
|
87
|
-
project-root instructions
|
|
88
|
-
|
|
89
|
-
disabled, dev-kit removes only
|
|
90
|
-
state.
|
|
99
|
+
project-root instructions; it manages `CLAUDE.md` as a relative symlink to the
|
|
100
|
+
wrapper or to an existing regular `AGENTS.md`. Preserve conflicting paths;
|
|
101
|
+
when disabled, dev-kit removes only unchanged outputs recorded in local
|
|
102
|
+
ownership state.
|
|
103
|
+
|
|
104
|
+
Enable `setup.vitePlus.hooks` when an installed direct `vite-plus` dependency
|
|
105
|
+
should manage Git hooks. Each apply checks the local `.vite-hooks/_` dispatcher,
|
|
106
|
+
the portable `.vite-hooks/pre-commit` hook, and `core.hooksPath`, then runs the
|
|
107
|
+
project-local `vp config --no-agent` when they need convergence. This recreates
|
|
108
|
+
ignored dispatchers in linked worktrees. Preserve other hook managers; Dev Kit
|
|
109
|
+
refuses to replace an unrelated `core.hooksPath`. Use `VITE_GIT_HOOKS=0` or
|
|
110
|
+
`HUSKY=0` to skip hook setup for an invocation.
|
|
111
|
+
|
|
112
|
+
Enable `setup.vitePlus.quality` only in supported Vite+/Effect repositories
|
|
113
|
+
that explicitly want Dev Kit to own the canonical root `vite.config.ts` and
|
|
114
|
+
`.github/workflows/check.yml`. It requires direct Dev Kit, Vite+, Effect,
|
|
115
|
+
Effect TypeScript-Go, and native TypeScript dependencies, as well as
|
|
116
|
+
`setup.effectTsgo.enabled`. It refuses custom destination files and conflicting
|
|
117
|
+
`check` or `typecheck` package scripts. Exact canonical files can be adopted;
|
|
118
|
+
later updates and cleanup occur only while the owned files remain unchanged.
|
|
119
|
+
|
|
120
|
+
The managed Vite config composes the shared formatter and linter presets,
|
|
121
|
+
configures `vp staged`, and defines cached `check` and `typecheck` Vite tasks.
|
|
122
|
+
The check task and GitHub Actions workflow run `vp fmt --check`, `vp lint`,
|
|
123
|
+
`vp test`, and finally `vp run typecheck` so Effect diagnostics come from the
|
|
124
|
+
patched native compiler.
|
|
91
125
|
|
|
92
126
|
## Ownership and conflicts
|
|
93
127
|
|
|
@@ -118,11 +152,17 @@ For one lifecycle entry point, configure:
|
|
|
118
152
|
```jsonc
|
|
119
153
|
{
|
|
120
154
|
"scripts": {
|
|
121
|
-
"postinstall": "dev-kit apply
|
|
122
|
-
}
|
|
155
|
+
"postinstall": "dev-kit apply",
|
|
156
|
+
},
|
|
123
157
|
}
|
|
124
158
|
```
|
|
125
159
|
|
|
160
|
+
This intentionally refreshes the committed lock and owned outputs when the
|
|
161
|
+
package manager installs a new Dev Kit or selected package-skill version.
|
|
162
|
+
Review and commit those changes with the dependency update. Keep
|
|
163
|
+
`dev-kit apply --locked` as a verification command, not the normal local
|
|
164
|
+
lifecycle; in CI, run it only before any unlocked apply.
|
|
165
|
+
|
|
126
166
|
## Effect source checkout
|
|
127
167
|
|
|
128
168
|
Enable the source task when agents should have canonical source matching the
|
|
@@ -131,8 +171,8 @@ installed Effect package:
|
|
|
131
171
|
```jsonc
|
|
132
172
|
{
|
|
133
173
|
"setup": {
|
|
134
|
-
"effectSource": { "enabled": true }
|
|
135
|
-
}
|
|
174
|
+
"effectSource": { "enabled": true },
|
|
175
|
+
},
|
|
136
176
|
}
|
|
137
177
|
```
|
|
138
178
|
|
|
@@ -151,8 +191,8 @@ Enable the setup task in the same manifest:
|
|
|
151
191
|
```jsonc
|
|
152
192
|
{
|
|
153
193
|
"setup": {
|
|
154
|
-
"effectTsgo": { "enabled": true }
|
|
155
|
-
}
|
|
194
|
+
"effectTsgo": { "enabled": true },
|
|
195
|
+
},
|
|
156
196
|
}
|
|
157
197
|
```
|
|
158
198
|
|
|
@@ -191,7 +231,16 @@ Use `lint.extends` instead of spreading the object so Vite+ composes nested
|
|
|
191
231
|
rule maps correctly. Oxfmt has no inheritance mechanism, so spread its object
|
|
192
232
|
before project-local options. Standalone `oxlint.config.ts` uses the same
|
|
193
233
|
`extends: [recommendedOxlintConfig]`; standalone `oxfmt.config.ts` spreads the
|
|
194
|
-
same `recommendedOxfmtConfig`.
|
|
234
|
+
same `recommendedOxfmtConfig`. The shared lint preset enables `typeAware` for
|
|
235
|
+
semantic lint rules but leaves `typeCheck` disabled. Effect TypeScript-Go
|
|
236
|
+
projects must run the patched native compiler separately with
|
|
237
|
+
`vp run typecheck` after `vp fmt --check`, `vp lint`, and `vp test`; Oxlint's
|
|
238
|
+
bundled `tsgolint` does not use the Effect patch.
|
|
239
|
+
|
|
240
|
+
The Oxlint preset enables the fixable
|
|
241
|
+
`stylistic/padding-line-between-statements` rule. It keeps adjacent variable
|
|
242
|
+
declarations grouped, requires a blank line before the next logical statement,
|
|
243
|
+
and separates every `return` statement from the preceding statement.
|
|
195
244
|
|
|
196
245
|
The Oxlint preset registers Dev Kit's shared Effect plugin as `effect`, but
|
|
197
246
|
does not enable its scope-sensitive rules globally. Effect projects should
|
|
@@ -204,9 +253,10 @@ in the consuming project.
|
|
|
204
253
|
|
|
205
254
|
## Current boundary
|
|
206
255
|
|
|
207
|
-
Manage skill outputs, the `setup.
|
|
208
|
-
`setup.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
256
|
+
Manage skill outputs, the `setup.agentInstructions` wrapper, the
|
|
257
|
+
`setup.claudeInstructions` link, the `setup.vitePlus.hooks` dispatcher, the
|
|
258
|
+
opt-in `setup.vitePlus.quality` config and GitHub workflow, the
|
|
259
|
+
`setup.effectSource` checkout, and the explicit `setup.effectTsgo` task.
|
|
260
|
+
Dependency and `tsconfig.json` contributions remain deliberate user-owned
|
|
261
|
+
edits. Custom Vite configs compose the Oxlint and Oxfmt package exports manually
|
|
262
|
+
and leave the canonical quality task disabled.
|
|
@@ -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.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# TanStack Start integration
|
|
2
|
+
|
|
3
|
+
TanStack Start code is isomorphic by default, including route loaders. Treat every module used by a route as server-capable unless an explicit boundary says otherwise.
|
|
4
|
+
|
|
5
|
+
## Choose an SSR strategy
|
|
6
|
+
|
|
7
|
+
Use one of these models deliberately:
|
|
8
|
+
|
|
9
|
+
### Client-only atom data
|
|
10
|
+
|
|
11
|
+
- Put one `RegistryProvider` in the app/root component so client navigations share a registry.
|
|
12
|
+
- Render atom consumers that touch browser-only APIs inside `ClientOnly` from `@tanstack/react-router`.
|
|
13
|
+
- Accept that the fallback is the server-rendered state and fetching begins on the client.
|
|
14
|
+
- Keep the client runtime and API service as module singletons.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { RegistryProvider } from "@effect/atom-react";
|
|
18
|
+
import { ClientOnly } from "@tanstack/react-router";
|
|
19
|
+
|
|
20
|
+
export function AppShell() {
|
|
21
|
+
return (
|
|
22
|
+
<RegistryProvider defaultIdleTTL={30_000}>
|
|
23
|
+
<ClientOnly fallback={<DashboardSkeleton />}>
|
|
24
|
+
<Dashboard />
|
|
25
|
+
</ClientOnly>
|
|
26
|
+
</RegistryProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### SSR plus hydration
|
|
32
|
+
|
|
33
|
+
- Create the registry and any request-specific runtime/layers per request; never share user/auth state through a process-global registry or memo map.
|
|
34
|
+
- Give decoded query atoms deterministic `serializationKey` values.
|
|
35
|
+
- Mount/run the required serializable atoms on the server, dehydrate only the intended values, and pass them through the document safely.
|
|
36
|
+
- Create the client registry once, then hydrate matching atom identities before descendants consume them. Use `HydrationBoundary` where the installed React adapter supports it.
|
|
37
|
+
- Verify that server and client construct the same API service, family arguments, and serialization keys.
|
|
38
|
+
|
|
39
|
+
Prefer framework loaders/server functions when they already own SSR data. Do not build a second atom SSR cache merely to mirror loader data; seed atoms from the loader or keep atom fetching client-only.
|
|
40
|
+
|
|
41
|
+
## Focus is browser-only
|
|
42
|
+
|
|
43
|
+
`Atom.windowFocusSignal` reads `window` and `document.visibilityState` when mounted. Do not mount it during SSR. Put focus-enabled consumers behind `ClientOnly`, or inject a no-op server signal and the browser signal on the client.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const project = projectAtom(projectId).pipe(
|
|
47
|
+
Atom.swr({
|
|
48
|
+
staleTime: "30 seconds",
|
|
49
|
+
revalidateOnFocus: true,
|
|
50
|
+
focusSignal: Atom.windowFocusSignal,
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`revalidateOnFocus: true` respects `staleTime`; `"always"` forces a request on every focus signal.
|
|
56
|
+
|
|
57
|
+
## In-memory limits
|
|
58
|
+
|
|
59
|
+
Effect Atom's registry cache is in memory and scoped to that registry:
|
|
60
|
+
|
|
61
|
+
- a new tab, hard reload, server process, or newly created provider starts another cache;
|
|
62
|
+
- idle TTL evicts only unused atoms and is not a maximum-entry or byte-size bound;
|
|
63
|
+
- `keepAlive` and infinite TTL can grow memory with unbounded family keys;
|
|
64
|
+
- browser memory is not durable or shared across users/devices;
|
|
65
|
+
- hydration transfers a snapshot, not a persistent distributed cache.
|
|
66
|
+
|
|
67
|
+
For large or unbounded parameter spaces, use finite TTLs and avoid `keepAlive`. Put durable/shared caching at the HTTP, server, CDN, or database layer.
|
|
68
|
+
|
|
69
|
+
Primary TanStack references: [execution model](https://tanstack.com/start/latest/docs/framework/react/guide/execution-model) and [`ClientOnly`](https://tanstack.com/router/latest/docs/api/router/clientOnlyComponent).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Deterministic lifecycle testing
|
|
2
|
+
|
|
3
|
+
Test cache policy below React first with `AtomRegistry.make()`. Add a React integration test only for provider placement, hook behavior, a browser-only SSR boundary, or hydration.
|
|
4
|
+
|
|
5
|
+
Use fake timers, a request counter, controllable Effects, and explicit mounts:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const registry = AtomRegistry.make({ defaultIdleTTL: 1_000 });
|
|
9
|
+
const unmount = registry.mount(queryAtom);
|
|
10
|
+
const first = registry.get(queryAtom);
|
|
11
|
+
|
|
12
|
+
// Advance the Effect scheduler/microtasks as required by the installed version.
|
|
13
|
+
// Assert with AsyncResult predicates and request counts.
|
|
14
|
+
|
|
15
|
+
unmount();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Avoid wall-clock sleeps. Flush Effect work with the repository's established `Effect.yieldNow`/test-clock pattern and advance the test runner's fake timers.
|
|
19
|
+
|
|
20
|
+
Prefer the installed test APIs over invented helpers or matchers. The upstream Effect tests use `assert(AsyncResult.isSuccess(result))`, `Effect.runPromise(Effect.yieldNow)`, `vitest.advanceTimersByTimeAsync(...)`, and the cleanup returned by `registry.mount(atom)`.
|
|
21
|
+
|
|
22
|
+
## Required scenarios
|
|
23
|
+
|
|
24
|
+
### Remount reuse
|
|
25
|
+
|
|
26
|
+
1. Mount and resolve the query; assert request count `1`.
|
|
27
|
+
2. Unmount, advance less than idle TTL, remount the same atom identity.
|
|
28
|
+
3. Assert the cached success is immediately available and no request occurs while still fresh.
|
|
29
|
+
|
|
30
|
+
### Stale refresh
|
|
31
|
+
|
|
32
|
+
1. Resolve once through an SWR wrapper.
|
|
33
|
+
2. Advance past `staleTime` but not idle TTL.
|
|
34
|
+
3. Remount or emit the injected focus signal.
|
|
35
|
+
4. Assert the previous success stays available with `waiting: true`, then a second success arrives and request count becomes `2`.
|
|
36
|
+
5. Also prove a fresh mount/focus does not request.
|
|
37
|
+
|
|
38
|
+
### TTL eviction
|
|
39
|
+
|
|
40
|
+
1. Resolve and unmount.
|
|
41
|
+
2. Advance to just before TTL; assert reuse.
|
|
42
|
+
3. Advance to/after TTL and flush disposal; remount.
|
|
43
|
+
4. Assert `Initial`/waiting behavior and a new request.
|
|
44
|
+
|
|
45
|
+
### Polling cleanup
|
|
46
|
+
|
|
47
|
+
1. Mount the `Atom.withRefresh` wrapper and resolve once.
|
|
48
|
+
2. Advance one interval; assert one forced refresh.
|
|
49
|
+
3. Unmount and advance several intervals.
|
|
50
|
+
4. Assert the counter does not change. This catches leaked timers or accidental `keepAlive`.
|
|
51
|
+
|
|
52
|
+
### Mutation invalidation
|
|
53
|
+
|
|
54
|
+
1. Mount list and detail queries with explicit keys.
|
|
55
|
+
2. Run a successful mutation with matching keys; assert only the intended queries refresh.
|
|
56
|
+
3. Run a failed mutation; assert no invalidation.
|
|
57
|
+
4. Assert cleanup removes invalidation handlers after query disposal.
|
|
58
|
+
|
|
59
|
+
### Aggregate stability
|
|
60
|
+
|
|
61
|
+
Mount a route atom that uses `AsyncResult.all`, resolve every input, unmount, and remount inside the retention window. Assert the aggregate never returns to `Initial`. Then let one input expire and prove the aggregate reset is caused by that input, not the retained queries.
|
|
62
|
+
|
|
63
|
+
For runtime/layer tests, seed `runtime.layer` through `RegistryProvider initialValues` with a deterministic test layer. This replaces network services without changing the production atom graph.
|
|
@@ -44,17 +44,17 @@ Enumerate source and test files, then find:
|
|
|
44
44
|
|
|
45
45
|
Record one row per discovered service or candidate:
|
|
46
46
|
|
|
47
|
-
| Field
|
|
48
|
-
|
|
|
49
|
-
| Owner
|
|
50
|
-
| Contract
|
|
51
|
-
| Construction
|
|
52
|
-
| Production
|
|
53
|
-
| Tests
|
|
54
|
-
| Consumers
|
|
55
|
-
| Requirements
|
|
56
|
-
| Type boundary | Who owns decoding, narrowing, and error translation?
|
|
57
|
-
| Verdict
|
|
47
|
+
| Field | Question |
|
|
48
|
+
| ------------- | ----------------------------------------------------------- |
|
|
49
|
+
| Owner | Which module owns the capability's meaning? |
|
|
50
|
+
| Contract | Where are its interface and tag? |
|
|
51
|
+
| Construction | Does construction yield every runtime dependency? |
|
|
52
|
+
| Production | Who owns the concrete implementation and Layer choice? |
|
|
53
|
+
| Tests | Does it have an intentional and honest substitute strategy? |
|
|
54
|
+
| Consumers | Are capabilities yielded or drilled as values? |
|
|
55
|
+
| Requirements | Do requirements remain visible to the composition root? |
|
|
56
|
+
| Type boundary | Who owns decoding, narrowing, and error translation? |
|
|
57
|
+
| Verdict | Keep, deepen, relocate, merge, remove, or create? |
|
|
58
58
|
|
|
59
59
|
Build a companion type-safety inventory using
|
|
60
60
|
[`guide-type-safety-and-boundaries.md`](guide-type-safety-and-boundaries.md).
|