@danieljvdm/dev-kit 0.11.3 → 0.12.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 +70 -76
- package/dev-kit.example.jsonc +0 -4
- package/package.json +10 -6
- package/schema/dev-kit.schema.json +1 -46
- package/skills/build-effect-apis/SKILL.md +77 -0
- package/skills/build-effect-apis/agents/openai.yaml +4 -0
- package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
- package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
- package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
- package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
- package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
- package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
- package/skills/build-effect-apis/references/shared-contracts.md +108 -0
- package/skills/build-effect-apis/references/tanstack-start.md +86 -0
- package/skills/build-effect-apis/references/verification.md +50 -0
- package/skills/dev-kit/SKILL.md +58 -46
- package/skills/effect-architecture-audit/SKILL.md +26 -0
- package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
- package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
- package/skills/effect-ts/SKILL.md +21 -256
- package/skills/effect-ts/agents/openai.yaml +3 -3
- package/skills/testing/SKILL.md +5 -0
- package/src/catalog-manager.ts +16 -17
- package/src/catalog.ts +71 -16
- package/src/effect-source.ts +46 -24
- package/src/effect-tsgo.ts +49 -24
- package/src/gitignore.ts +5 -5
- package/src/index.ts +0 -6
- package/src/manifest.ts +0 -34
- package/src/node-symbolic-link.ts +2 -2
- package/src/oxfmt.js +5 -0
- package/src/oxfmt.ts +5 -0
- package/src/oxlint.js +5 -0
- package/src/oxlint.ts +5 -0
- package/src/package-skill-source.ts +51 -59
- package/src/path-digest.ts +7 -7
- package/src/project-package.ts +8 -7
- package/src/project-process-lock.ts +17 -12
- package/src/project-state.ts +1 -1
- package/src/skill-manager.ts +16 -14
- package/src/skill-selector.ts +12 -0
- package/src/sync.ts +170 -120
- package/src/tool-ignore-patterns.js +9 -0
- package/src/tool-ignore-patterns.ts +15 -0
- package/src/vendor.ts +67 -61
- package/src/vite-plus-dependency.ts +10 -11
- package/src/vite-plus-hooks.ts +24 -14
- package/src/vite-plus-quality.ts +21 -172
- package/src/vite-plus.js +81 -0
- package/src/vite-plus.ts +102 -0
- package/templates/AGENTS.md +1 -1
- package/skills/effect-ts/UPSTREAM.md +0 -28
- package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
- package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
- package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
- package/skills/effect-ts/references/atom-testing.md +0 -67
- package/skills/effect-ts/references/audit-services.md +0 -144
- package/skills/effect-ts/references/features.md +0 -525
- package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
- package/skills/effect-ts/references/guide-cli.md +0 -107
- package/skills/effect-ts/references/guide-datetime.md +0 -72
- package/skills/effect-ts/references/guide-effect.md +0 -440
- package/skills/effect-ts/references/guide-error-handling.md +0 -565
- package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
- package/skills/effect-ts/references/guide-layers.md +0 -989
- package/skills/effect-ts/references/guide-observability.md +0 -746
- package/skills/effect-ts/references/guide-retries.md +0 -434
- package/skills/effect-ts/references/guide-schedule.md +0 -343
- package/skills/effect-ts/references/guide-schema.md +0 -664
- package/skills/effect-ts/references/guide-sql.md +0 -536
- package/skills/effect-ts/references/guide-testing.md +0 -532
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
- package/skills/effect-ts/references/version-and-source.md +0 -86
- package/templates/vite-plus/vite.config.ts +0 -22
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Effect Atom lifecycle
|
|
2
|
+
|
|
3
|
+
Treat query atoms as shared read state and mutations as actions owned by an
|
|
4
|
+
initiating UI or workflow. Choose registry scope, atom identity, retention,
|
|
5
|
+
freshness, polling, and cancellation independently.
|
|
6
|
+
|
|
7
|
+
## Registry and runtime scope
|
|
8
|
+
|
|
9
|
+
`RegistryProvider` creates its registry on first render; later option changes do
|
|
10
|
+
not rebuild it. Provider unmount schedules disposal after a short grace period,
|
|
11
|
+
while moving or keying the provider changes the cache boundary. Place one
|
|
12
|
+
provider around the application subtree that should share client state; nested
|
|
13
|
+
or route-local providers create separate caches.
|
|
14
|
+
|
|
15
|
+
The registry stores nodes, values, subscriptions, idle timers, and finalizers.
|
|
16
|
+
An Atom runtime supplies Effect services, and `Atom.context({ memoMap })` lets
|
|
17
|
+
several runtimes share layer construction. Create neither a registry nor a memo
|
|
18
|
+
map per query or render. Keep request-specific authentication out of a
|
|
19
|
+
process-global server memo map.
|
|
20
|
+
|
|
21
|
+
## Stable identity and families
|
|
22
|
+
|
|
23
|
+
Export fixed queries directly. Use `Atom.family` when a parameter selects the
|
|
24
|
+
resource. Prefer a primitive family argument; give object keys deliberate Effect
|
|
25
|
+
`Equal`/`Hash` semantics or reuse the same object. Calling a family with a stable
|
|
26
|
+
primitive during render already returns its stable member; an extra `useMemo`
|
|
27
|
+
does not improve identity.
|
|
28
|
+
|
|
29
|
+
## Three independent clocks
|
|
30
|
+
|
|
31
|
+
| Control | Clock starts | Effect | It does not mean |
|
|
32
|
+
| ----------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------- | -------------------------- |
|
|
33
|
+
| Registry/default idle TTL or query `timeToLive` | When the atom becomes unused | Retain the cached node until idle eviction | The value remains fresh |
|
|
34
|
+
| `Atom.swr({ staleTime })` | At the latest success | Revalidate stale data on configured mount/focus signals while showing the prior success | The node survives unmount |
|
|
35
|
+
| `Atom.withRefresh(interval)` | While its wrapper is mounted | Force periodic refresh until disposal | Fresh requests are skipped |
|
|
36
|
+
|
|
37
|
+
Set retention long enough for the intended navigation/remount reuse window.
|
|
38
|
+
`staleTime` cannot rescue an already evicted node. Manual refresh, invalidation,
|
|
39
|
+
and polling are forceful and do not consult SWR freshness. Applying `keepAlive`
|
|
40
|
+
to a polling wrapper intentionally keeps that polling lifetime alive.
|
|
41
|
+
|
|
42
|
+
Atom registries are in-memory caches, not durable or shared storage. New tabs,
|
|
43
|
+
hard reloads, processes, and providers have separate caches. Idle TTL is not a
|
|
44
|
+
maximum-entry bound; finite TTLs are important for unbounded family keys.
|
|
45
|
+
|
|
46
|
+
## React mounts and action ownership
|
|
47
|
+
|
|
48
|
+
- `useAtomValue(atom)` subscribes for rendering;
|
|
49
|
+
- `useAtomSet(atom)` mounts a writable atom and returns a setter;
|
|
50
|
+
- `useAtom(atom)` subscribes and writes when one component owns both behaviors.
|
|
51
|
+
|
|
52
|
+
Use `useAtom` instead of pairing `useAtomValue` and `useAtomSet` for the same
|
|
53
|
+
atom in one component. The split form creates a value subscription plus a
|
|
54
|
+
separate mount and obscures ownership.
|
|
55
|
+
|
|
56
|
+
Unmount releases only that hook's subscription or mount. Registry eviction waits
|
|
57
|
+
for remaining consumers and idle TTL, then runs node finalizers. Component
|
|
58
|
+
unmount, registry eviction, and `Atom.Interrupt` are distinct events.
|
|
59
|
+
|
|
60
|
+
Use `Atom.Interrupt` for explicit cancellation. A cleanup write publishes an
|
|
61
|
+
interrupted `AsyncResult.Failure`; it is not passive unmount release, can run
|
|
62
|
+
during React Strict Mode replay, and can cancel work owned by another consumer.
|
|
63
|
+
|
|
64
|
+
Place a multi-request action in an owner that lives for the whole sequence. If
|
|
65
|
+
navigation or disconnect must not leave a partially completed fan-out, use a
|
|
66
|
+
stable workflow atom/service or one durable server-side command.
|
|
67
|
+
|
|
68
|
+
## Keep aggregates stable
|
|
69
|
+
|
|
70
|
+
`AsyncResult.all` returns the first non-success input, so an aggregate is only
|
|
71
|
+
as reusable as its least-stable input. A newly allocated or evicted input
|
|
72
|
+
returns to `Initial` and makes the aggregate appear reset even while other
|
|
73
|
+
queries remain cached.
|
|
74
|
+
|
|
75
|
+
Define every input as a module-scoped singleton or stable family member with
|
|
76
|
+
compatible retention. Aggregate inside a derived atom so the registry owns
|
|
77
|
+
recomputation; memoizing only the `AsyncResult.all` call does not repair an
|
|
78
|
+
unstable input atom.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Deterministic Effect Atom testing
|
|
2
|
+
|
|
3
|
+
Test cache policy below React first with `AtomRegistry.make()`. Add a React
|
|
4
|
+
integration test for provider placement, hook behavior, browser-only SSR
|
|
5
|
+
boundaries, hydration, or Strict Mode ownership.
|
|
6
|
+
|
|
7
|
+
Use fake timers, a request counter, controllable Effects, and explicit mounts:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const registry = AtomRegistry.make({ defaultIdleTTL: 1_000 });
|
|
11
|
+
const unmount = registry.mount(queryAtom);
|
|
12
|
+
const first = registry.get(queryAtom);
|
|
13
|
+
|
|
14
|
+
// Flush Effect work and advance the test runner's fake timers.
|
|
15
|
+
|
|
16
|
+
unmount();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Use the repository's established `Effect.yieldNow` or test-clock pattern rather
|
|
20
|
+
than wall-clock sleeps. Prefer installed APIs and predicates such as
|
|
21
|
+
`AsyncResult.isSuccess`; use the cleanup returned by `registry.mount(atom)`.
|
|
22
|
+
|
|
23
|
+
## Remount reuse
|
|
24
|
+
|
|
25
|
+
1. Mount and resolve the query; assert request count `1`.
|
|
26
|
+
2. Unmount, advance less than idle TTL, and remount the same atom identity.
|
|
27
|
+
3. Assert cached success is immediately available and no fresh request occurs.
|
|
28
|
+
|
|
29
|
+
## Stale refresh
|
|
30
|
+
|
|
31
|
+
1. Resolve once through an SWR wrapper.
|
|
32
|
+
2. Advance past `staleTime` but not idle TTL.
|
|
33
|
+
3. Remount or emit the injected focus signal.
|
|
34
|
+
4. Assert the previous success remains with `waiting: true`, then a second
|
|
35
|
+
success arrives and request count becomes `2`.
|
|
36
|
+
5. Prove a fresh mount or focus signal does not request.
|
|
37
|
+
|
|
38
|
+
## TTL eviction
|
|
39
|
+
|
|
40
|
+
1. Resolve and unmount.
|
|
41
|
+
2. Advance to just before TTL and assert reuse.
|
|
42
|
+
3. Advance through TTL, flush disposal, and 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 and assert one forced refresh.
|
|
49
|
+
3. Unmount, advance several intervals, and assert the counter stops changing.
|
|
50
|
+
|
|
51
|
+
## Mutation invalidation
|
|
52
|
+
|
|
53
|
+
1. Mount list and detail queries with explicit keys.
|
|
54
|
+
2. Run a successful mutation and assert only intended queries refresh.
|
|
55
|
+
3. Run a failed mutation and assert nothing invalidates.
|
|
56
|
+
4. Dispose the queries and assert invalidation handlers are removed.
|
|
57
|
+
|
|
58
|
+
## Strict Mode and action ownership
|
|
59
|
+
|
|
60
|
+
Render the owner inside `StrictMode`. Prove development setup-cleanup replay
|
|
61
|
+
does not write `Atom.Interrupt` or publish an interrupted failure before an
|
|
62
|
+
explicit cancellation. For multi-request actions, unmount during the sequence
|
|
63
|
+
and prove the chosen policy. If the atom is shared, prove unmounting one
|
|
64
|
+
consumer does not cancel work still owned by another.
|
|
65
|
+
|
|
66
|
+
## Aggregate stability
|
|
67
|
+
|
|
68
|
+
Mount a route atom using `AsyncResult.all`, resolve every input, unmount, and
|
|
69
|
+
remount inside the retention window. Assert the aggregate never returns to
|
|
70
|
+
`Initial`. Then expire one input and prove that input causes the reset.
|
|
71
|
+
|
|
72
|
+
For runtime/layer tests, seed `runtime.layer` through `RegistryProvider`
|
|
73
|
+
`initialValues` with a deterministic test layer. Replace network services
|
|
74
|
+
without changing the production atom graph.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Standard runtime assembly
|
|
2
|
+
|
|
3
|
+
Use this branch for conventional Node/Bun processes, generated documentation,
|
|
4
|
+
and Web-standard handlers. Confirm platform package names against the installed
|
|
5
|
+
versions.
|
|
6
|
+
|
|
7
|
+
## Compose routes before choosing the runtime
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
|
|
11
|
+
import { Layer } from "effect";
|
|
12
|
+
import { HttpRouter, HttpServer } from "effect/unstable/http";
|
|
13
|
+
import { HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi";
|
|
14
|
+
import { createServer } from "node:http";
|
|
15
|
+
import { ApplicationApi } from "@app/domain/http";
|
|
16
|
+
|
|
17
|
+
const ApplicationHttpLive = Layer.mergeAll(ProjectsHandlersLive, AuthenticateLive).pipe(
|
|
18
|
+
Layer.provide(ApplicationServicesLive),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const ApiRoutes = HttpApiBuilder.layer(ApplicationApi, {
|
|
22
|
+
openapiPath: "/openapi.json",
|
|
23
|
+
}).pipe(Layer.provide(ApplicationHttpLive));
|
|
24
|
+
|
|
25
|
+
const DocsRoute = HttpApiScalar.layer(ApplicationApi, { path: "/docs" });
|
|
26
|
+
const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Build the typed API route and documentation route from the same contract. Merge
|
|
30
|
+
raw routes here only when the API schema cannot represent their transport, and
|
|
31
|
+
ensure they independently enforce equivalent identity and security behavior.
|
|
32
|
+
|
|
33
|
+
## Run a long-lived server
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const HttpServerLive = HttpRouter.serve(AllRoutes).pipe(
|
|
37
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
Layer.launch(HttpServerLive).pipe(NodeRuntime.runMain);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Use the matching platform server layer for Bun or another runtime. Keep the
|
|
44
|
+
platform choice at this composition root.
|
|
45
|
+
|
|
46
|
+
## Export a Web handler
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export const { handler, dispose } = HttpRouter.toWebHandler(
|
|
50
|
+
AllRoutes.pipe(Layer.provide(HttpServer.layerServices)),
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use the Web handler for serverless and framework adapters that accept standard
|
|
55
|
+
`Request` and `Response` values. Preserve and call `dispose` when the host has a
|
|
56
|
+
lifecycle hook; do not recreate the handler and its runtime for every request.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Server handlers and middleware
|
|
2
|
+
|
|
3
|
+
Implement the shared contract with group handlers, application services, and
|
|
4
|
+
layers. Confirm names against the installed Effect version; examples here use
|
|
5
|
+
the Effect v4 `effect/unstable/httpapi` surface.
|
|
6
|
+
|
|
7
|
+
- [Implement thin group handlers](#implement-thin-group-handlers)
|
|
8
|
+
- [Declare security middleware in the shared contract](#declare-security-middleware-in-the-shared-contract)
|
|
9
|
+
- [Implement security middleware on the server](#implement-security-middleware-on-the-server)
|
|
10
|
+
- [Assemble layers at the runtime edge](#assemble-layers-at-the-runtime-edge)
|
|
11
|
+
- [Use response escape hatches deliberately](#use-response-escape-hatches-deliberately)
|
|
12
|
+
|
|
13
|
+
## Implement thin group handlers
|
|
14
|
+
|
|
15
|
+
Resolve application services once in the group builder, then map every endpoint
|
|
16
|
+
identifier to one handler. Use `Effect.fn` for callbacks that return Effects.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Effect } from "effect";
|
|
20
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
21
|
+
import { ApplicationApi } from "@app/domain/http";
|
|
22
|
+
import { Projects } from "../services/Projects";
|
|
23
|
+
|
|
24
|
+
export const ProjectsHandlersLive = HttpApiBuilder.group(
|
|
25
|
+
ApplicationApi,
|
|
26
|
+
"projects",
|
|
27
|
+
Effect.fn("ProjectsApi.handlers")(function* (handlers) {
|
|
28
|
+
const projects = yield* Projects;
|
|
29
|
+
|
|
30
|
+
return handlers
|
|
31
|
+
.handle("listProjects", () => projects.list().pipe(Effect.orDie))
|
|
32
|
+
.handle("getProject", ({ params }) =>
|
|
33
|
+
projects
|
|
34
|
+
.get(params.projectId)
|
|
35
|
+
.pipe(Effect.catchReasons("ProjectsError", { ProjectNotFound: Effect.fail }, Effect.die)),
|
|
36
|
+
);
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Handlers receive decoded `params`, `query`, `headers`, and `payload`. Check
|
|
42
|
+
cross-field invariants at this boundary when schemas cannot express them, then
|
|
43
|
+
call one application workflow. Keep persistence, transactions, retries, and
|
|
44
|
+
multi-service orchestration in services.
|
|
45
|
+
|
|
46
|
+
Reconcile the service error channel with the endpoint contract here. Use
|
|
47
|
+
`Effect.orDie` when an endpoint declares no service failures. Use
|
|
48
|
+
`Effect.catchReason`, `Effect.catchReasons`, or `Effect.unwrapReason` when an
|
|
49
|
+
application service wraps domain reasons: re-fail only reasons declared by the
|
|
50
|
+
endpoint and turn unexpected infrastructure or invariant failures into defects.
|
|
51
|
+
Finish when each handler's expected error type is a subset of the endpoint's
|
|
52
|
+
declared error schema.
|
|
53
|
+
|
|
54
|
+
## Declare security middleware in the shared contract
|
|
55
|
+
|
|
56
|
+
Keep the middleware declaration in the shared contract package so server,
|
|
57
|
+
OpenAPI, and generated clients see the same requirement. Keep its implementation
|
|
58
|
+
out of that package.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// packages/domain/src/http/authorization.ts
|
|
62
|
+
import { Context, Schema } from "effect";
|
|
63
|
+
import { HttpApiGroup, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi";
|
|
64
|
+
|
|
65
|
+
export class CurrentActor extends Context.Service<CurrentActor, { readonly id: string }>()(
|
|
66
|
+
"app/CurrentActor",
|
|
67
|
+
) {}
|
|
68
|
+
|
|
69
|
+
export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
70
|
+
"Unauthorized",
|
|
71
|
+
{ message: Schema.String },
|
|
72
|
+
{ httpApiStatus: 401 },
|
|
73
|
+
) {}
|
|
74
|
+
|
|
75
|
+
export class Authenticate extends HttpApiMiddleware.Service<
|
|
76
|
+
Authenticate,
|
|
77
|
+
{
|
|
78
|
+
readonly provides: CurrentActor;
|
|
79
|
+
readonly requires: never;
|
|
80
|
+
}
|
|
81
|
+
>()("app/Authenticate", {
|
|
82
|
+
requiredForClient: true,
|
|
83
|
+
security: { bearer: HttpApiSecurity.bearer },
|
|
84
|
+
error: Unauthorized,
|
|
85
|
+
}) {}
|
|
86
|
+
|
|
87
|
+
export const ProjectsApi = HttpApiGroup.make("projects")
|
|
88
|
+
.add(GetProjectEndpoint, ListProjectsEndpoint)
|
|
89
|
+
.middleware(Authenticate)
|
|
90
|
+
.prefix("/projects");
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`requiredForClient` makes the generated client require a matching client-side
|
|
94
|
+
middleware layer. The security declaration also owns credential decoding and
|
|
95
|
+
OpenAPI security metadata; do not manually decode an `Authorization` header for
|
|
96
|
+
this case.
|
|
97
|
+
|
|
98
|
+
## Implement security middleware on the server
|
|
99
|
+
|
|
100
|
+
A security middleware implementation is keyed by the names in its `security`
|
|
101
|
+
object. It can use server-only services without leaking them into clients.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// apps/api/src/http/Authenticate.ts
|
|
105
|
+
import { Effect, Layer, Redacted } from "effect";
|
|
106
|
+
import { Authenticate, CurrentActor, Unauthorized } from "@app/domain/http";
|
|
107
|
+
import { Actors } from "../services/Actors";
|
|
108
|
+
|
|
109
|
+
export const AuthenticateLive = Layer.effect(
|
|
110
|
+
Authenticate,
|
|
111
|
+
Effect.gen(function* () {
|
|
112
|
+
const actors = yield* Actors;
|
|
113
|
+
|
|
114
|
+
return Authenticate.of({
|
|
115
|
+
bearer: Effect.fn("Authenticate.bearer")(function* (httpEffect, { credential }) {
|
|
116
|
+
const actor = yield* actors
|
|
117
|
+
.authenticate(Redacted.value(credential))
|
|
118
|
+
.pipe(
|
|
119
|
+
Effect.catch(() => Effect.fail(new Unauthorized({ message: "Invalid bearer token" }))),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return yield* Effect.provideService(httpEffect, CurrentActor, actor);
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Use ordinary `HttpApiMiddleware.Service` implementations for request logging,
|
|
130
|
+
tenancy, correlation IDs, or other cross-cutting work that is not a declared
|
|
131
|
+
security scheme. Those implementations may access `HttpServerRequest`, but they
|
|
132
|
+
must decode raw headers, cookies, and external values before providing services.
|
|
133
|
+
When middleware uses `requires` and `provides`, confirm ordering from the
|
|
134
|
+
installed docs: the outer middleware that requires a service is attached before
|
|
135
|
+
the inner middleware that provides it.
|
|
136
|
+
|
|
137
|
+
Every error a middleware can return belongs in its `error` schema. Keep the
|
|
138
|
+
server layer separate from the shared declaration so importing the contract
|
|
139
|
+
never pulls secrets, repositories, or platform services into a client bundle.
|
|
140
|
+
|
|
141
|
+
## Assemble layers at the runtime edge
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const ProjectsHandlersProvided = ProjectsHandlersLive.pipe(Layer.provide(ApplicationServicesLive));
|
|
145
|
+
|
|
146
|
+
const ApiLive = HttpApiBuilder.layer(ApplicationApi, {
|
|
147
|
+
openapiPath: "/api/openapi.json",
|
|
148
|
+
}).pipe(
|
|
149
|
+
Layer.provide(ProjectsHandlersProvided),
|
|
150
|
+
Layer.provide(AuthenticateLive),
|
|
151
|
+
Layer.provideMerge(HttpRuntimePrerequisitesLive),
|
|
152
|
+
);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The exact HTTP platform/router/server layers vary by runtime. Keep that wiring
|
|
156
|
+
at the entrypoint, provide all group and middleware layers, and let the type
|
|
157
|
+
system expose missing requirements. Verify that the OpenAPI path composes with
|
|
158
|
+
any API prefix as intended.
|
|
159
|
+
|
|
160
|
+
For a conventional Node/Bun process, generated docs route, or serverless web
|
|
161
|
+
handler, read [runtime-assembly.md](runtime-assembly.md).
|
|
162
|
+
|
|
163
|
+
Transport-wide middleware such as CORS may wrap the final HTTP application.
|
|
164
|
+
Keep API middleware for contract-visible behavior and runtime HTTP middleware
|
|
165
|
+
for concerns that also apply to raw or non-API routes.
|
|
166
|
+
|
|
167
|
+
## Use response escape hatches deliberately
|
|
168
|
+
|
|
169
|
+
Return typed success values and declared errors for ordinary endpoints. Use
|
|
170
|
+
`HttpServerResponse` when the boundary must pass through an upstream response,
|
|
171
|
+
redirect, set cookies, stream, or control a non-default success status or body.
|
|
172
|
+
Use Effect request and cookie APIs to parse boundary state. Reserve unsafe JSON
|
|
173
|
+
response constructors for values whose safety is established elsewhere; encode
|
|
174
|
+
ordinary typed 4xx failures through the endpoint's declared error schemas.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Shared contracts
|
|
2
|
+
|
|
3
|
+
Use a runtime-neutral domain package as the protocol boundary shared by the
|
|
4
|
+
server and every typed client.
|
|
5
|
+
|
|
6
|
+
## Package shape
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
packages/domain/
|
|
10
|
+
├── package.json
|
|
11
|
+
└── src/
|
|
12
|
+
├── models/
|
|
13
|
+
│ ├── ids.ts
|
|
14
|
+
│ ├── errors.ts
|
|
15
|
+
│ └── views.ts
|
|
16
|
+
└── http/
|
|
17
|
+
├── endpoints/
|
|
18
|
+
│ ├── get-project.ts
|
|
19
|
+
│ └── update-project.ts
|
|
20
|
+
├── authorization.ts
|
|
21
|
+
├── project-api.ts
|
|
22
|
+
├── application-api.ts
|
|
23
|
+
└── index.ts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Export the root API, contract-visible middleware declarations, and any genuinely
|
|
27
|
+
reusable schemas. Keep middleware implementations, server handlers, React,
|
|
28
|
+
database clients, secrets, platform bindings, and runtime layers out of this
|
|
29
|
+
package. A workspace export map can expose `./http`, `./models`, and endpoint
|
|
30
|
+
subpaths when consumers need them.
|
|
31
|
+
|
|
32
|
+
## Model the wire
|
|
33
|
+
|
|
34
|
+
Use schemas for every external value. Prefer reusable named classes for DTOs,
|
|
35
|
+
branded scalar schemas for identifiers, and serializable tagged errors with
|
|
36
|
+
HTTP status metadata for expected failures.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { Schema } from "effect";
|
|
40
|
+
import { HttpApiSchema } from "effect/unstable/httpapi";
|
|
41
|
+
|
|
42
|
+
export const ProjectId = Schema.String.pipe(Schema.brand("ProjectId"));
|
|
43
|
+
export type ProjectId = typeof ProjectId.Type;
|
|
44
|
+
|
|
45
|
+
export class ProjectDto extends Schema.Class<ProjectDto>("ProjectDto")({
|
|
46
|
+
id: ProjectId,
|
|
47
|
+
name: Schema.NonEmptyString,
|
|
48
|
+
}) {}
|
|
49
|
+
|
|
50
|
+
export class ProjectNotFoundError extends Schema.TaggedError<ProjectNotFoundError>()(
|
|
51
|
+
"ProjectNotFoundError",
|
|
52
|
+
{ id: ProjectId },
|
|
53
|
+
) {}
|
|
54
|
+
|
|
55
|
+
export const ProjectNotFound = ProjectNotFoundError.pipe(HttpApiSchema.status("NotFound"));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Keep success schemas limited to successful values. Put every expected 4xx/5xx
|
|
59
|
+
failure in `error`; defects remain defects. Reuse a tuple of transport errors
|
|
60
|
+
only when the same set is genuinely standard across endpoints.
|
|
61
|
+
|
|
62
|
+
Make DTOs describe the wire exactly. If clients receive `id`, expose `id` even
|
|
63
|
+
when a persistence model uses `publicId`; map between them in a small boundary
|
|
64
|
+
function. Keep endpoint-specific request DTOs beside their endpoint and move a
|
|
65
|
+
DTO into shared models only when multiple contracts reuse the same concept.
|
|
66
|
+
|
|
67
|
+
## Define one endpoint per file
|
|
68
|
+
|
|
69
|
+
An endpoint declares the complete request and response contract inline.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { HttpApiEndpoint } from "effect/unstable/httpapi";
|
|
73
|
+
import { ProjectDto, ProjectId, ProjectNotFound } from "../../models";
|
|
74
|
+
import { RequestHeaders } from "../headers";
|
|
75
|
+
|
|
76
|
+
export const GetProjectEndpoint = HttpApiEndpoint.get("getProject", "/projects/:projectId", {
|
|
77
|
+
params: { projectId: ProjectId },
|
|
78
|
+
headers: RequestHeaders,
|
|
79
|
+
success: ProjectDto,
|
|
80
|
+
error: ProjectNotFound,
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use the operation identifier as a stable protocol name: handlers and generated
|
|
85
|
+
clients address it directly. Represent optional query values with the installed
|
|
86
|
+
version's optional schema operator, such as `Schema.optionalKey`.
|
|
87
|
+
|
|
88
|
+
If headers use `Schema.Class`, generated client input may require a constructed
|
|
89
|
+
class instance rather than a structurally similar object. Encode a representative
|
|
90
|
+
instance in a test so this contract stays explicit.
|
|
91
|
+
|
|
92
|
+
## Compose groups and the root
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { HttpApi, HttpApiGroup } from "effect/unstable/httpapi";
|
|
96
|
+
import { GetProjectEndpoint, UpdateProjectEndpoint } from "./endpoints";
|
|
97
|
+
|
|
98
|
+
export const ProjectsApi = HttpApiGroup.make("projects")
|
|
99
|
+
.add(GetProjectEndpoint, UpdateProjectEndpoint)
|
|
100
|
+
.prefix("/projects");
|
|
101
|
+
|
|
102
|
+
export const ApplicationApi = HttpApi.make("application").add(ProjectsApi).prefix("/api");
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Group and root files compose; endpoint files specify. Re-export through index
|
|
106
|
+
files so server and clients import the same `ApplicationApi` value. A contract
|
|
107
|
+
change is complete only after all affected handlers, callers, tests, and
|
|
108
|
+
OpenAPI output agree with it.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# TanStack Start integration
|
|
2
|
+
|
|
3
|
+
TanStack Start modules are isomorphic unless an explicit boundary says
|
|
4
|
+
otherwise. Choose one data-execution model before wiring Effect Atom.
|
|
5
|
+
|
|
6
|
+
## Client-only atom data
|
|
7
|
+
|
|
8
|
+
Use this when identity, focus signals, or other dependencies require browser
|
|
9
|
+
globals.
|
|
10
|
+
|
|
11
|
+
1. Mount one `RegistryProvider` around the application subtree whose client
|
|
12
|
+
navigations should share cache state.
|
|
13
|
+
2. Put browser-dependent atom consumers behind `ClientOnly`.
|
|
14
|
+
3. Keep the runtime, `AtomHttpApi.Service`, query families, and mutations as
|
|
15
|
+
client module singletons.
|
|
16
|
+
4. Render a useful server fallback and accept that API fetching begins after
|
|
17
|
+
hydration.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { RegistryProvider } from "@effect/atom-react";
|
|
21
|
+
import { ClientOnly, Outlet } from "@tanstack/react-router";
|
|
22
|
+
|
|
23
|
+
export function Root() {
|
|
24
|
+
return (
|
|
25
|
+
<RegistryProvider defaultIdleTTL={60_000}>
|
|
26
|
+
<ClientOnly fallback={<AppSkeleton />}>
|
|
27
|
+
<Outlet />
|
|
28
|
+
</ClientOnly>
|
|
29
|
+
</RegistryProvider>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`Atom.windowFocusSignal`, `localStorage`, `window`, and `document` belong in
|
|
35
|
+
this client-only branch.
|
|
36
|
+
|
|
37
|
+
## SSR or loader-owned data
|
|
38
|
+
|
|
39
|
+
Prefer TanStack loaders or server functions when they already own SSR data.
|
|
40
|
+
Pass loader data into the client state graph or keep the query loader-owned;
|
|
41
|
+
avoid maintaining two independent server caches for the same request.
|
|
42
|
+
|
|
43
|
+
When atom SSR is intentional:
|
|
44
|
+
|
|
45
|
+
- create the registry and request-specific runtime/layers per request;
|
|
46
|
+
- give serializable queries deterministic `serializationKey` values;
|
|
47
|
+
- mount or run only the intended serializable atoms on the server;
|
|
48
|
+
- dehydrate only intended values;
|
|
49
|
+
- create the browser registry once and hydrate matching atom identities before
|
|
50
|
+
descendants read them;
|
|
51
|
+
- use `HydrationBoundary` when the installed React adapter supports it.
|
|
52
|
+
|
|
53
|
+
Process-global registries or memo maps can leak request-specific authentication
|
|
54
|
+
and server state across users. Verify request isolation with concurrent SSR
|
|
55
|
+
tests.
|
|
56
|
+
|
|
57
|
+
## Focus is browser-only
|
|
58
|
+
|
|
59
|
+
`Atom.windowFocusSignal` reads `window` and `document.visibilityState` when
|
|
60
|
+
mounted. Keep focus-enabled consumers behind `ClientOnly`, or inject a no-op
|
|
61
|
+
server signal and the browser signal on the client. `revalidateOnFocus: true`
|
|
62
|
+
respects `staleTime`; `"always"` forces a request on each focus signal.
|
|
63
|
+
|
|
64
|
+
## Separate API development
|
|
65
|
+
|
|
66
|
+
When TanStack Start and the Effect API run as separate local processes, proxy a
|
|
67
|
+
stable prefix and WebSocket upgrades through the app dev server:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export default defineConfig({
|
|
71
|
+
plugins: [tanstackStart(), react()],
|
|
72
|
+
server: {
|
|
73
|
+
proxy: {
|
|
74
|
+
"/api": {
|
|
75
|
+
target: "http://127.0.0.1:8787",
|
|
76
|
+
changeOrigin: true,
|
|
77
|
+
ws: true,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Keep the client base URL aligned with proxy and production routing. If
|
|
85
|
+
credentials cross origins, configure CORS and cookie/header behavior at the API
|
|
86
|
+
edge and test the deployed topology, not only the same-origin development path.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Verification
|
|
2
|
+
|
|
3
|
+
Test the contract spine at its seams. Use the repository's established Effect
|
|
4
|
+
test integration and command authority.
|
|
5
|
+
|
|
6
|
+
## Contract tests
|
|
7
|
+
|
|
8
|
+
- Decode and re-encode representative request, success, and error values.
|
|
9
|
+
- Prove branded path/query/header values reject invalid wire input.
|
|
10
|
+
- Encode any `Schema.Class` headers exactly as generated clients receive them.
|
|
11
|
+
- Assert each expected error carries the intended HTTP status and body encoding.
|
|
12
|
+
- Compare or smoke-test generated OpenAPI when the public contract changes.
|
|
13
|
+
|
|
14
|
+
## Server tests
|
|
15
|
+
|
|
16
|
+
- Build every changed group and fail the test if an endpoint handler is missing.
|
|
17
|
+
- Provide deterministic test layers for application services and middleware.
|
|
18
|
+
- Prove each handler exposes only errors declared by its endpoint; exercise
|
|
19
|
+
expected reason mapping and unexpected failure defects separately.
|
|
20
|
+
- Exercise success, declared failure, malformed input, and middleware rejection.
|
|
21
|
+
- Assert cross-field boundary invariants before the service workflow runs.
|
|
22
|
+
- Exercise raw routes separately and prove they enforce middleware-equivalent
|
|
23
|
+
identity/security rules.
|
|
24
|
+
|
|
25
|
+
Use the installed `HttpApiTest` or an in-memory `HttpClient` when available;
|
|
26
|
+
otherwise run the built HTTP application against representative requests. Avoid
|
|
27
|
+
mocking below the contract so heavily that request encoding and response
|
|
28
|
+
decoding are skipped.
|
|
29
|
+
|
|
30
|
+
## Client tests
|
|
31
|
+
|
|
32
|
+
- Derive the client from the same root API used by the server.
|
|
33
|
+
- Provide every `requiredForClient` middleware with
|
|
34
|
+
`HttpApiMiddleware.layerClient` and assert that it transforms the request.
|
|
35
|
+
- Assert params, query, headers, payload, and expected errors at least once for
|
|
36
|
+
every changed request shape.
|
|
37
|
+
- For Atom clients, complete every applicable scenario routed through
|
|
38
|
+
`effect-atom-testing.md`; use a deterministic HTTP layer so request encoding,
|
|
39
|
+
invalidation, and lifecycle remain observable.
|
|
40
|
+
|
|
41
|
+
## Completion matrix
|
|
42
|
+
|
|
43
|
+
Account for every changed endpoint across these columns:
|
|
44
|
+
|
|
45
|
+
| Contract | Middleware | Handler/service | Client/query/mutation | Tests |
|
|
46
|
+
| ------------------------------------------------ | -------------------------------- | -------------------------------- | ---------------------------------------------- | -------------------------------- |
|
|
47
|
+
| Params, query, headers, payload, success, errors | Scope, provided services, errors | Identifier, invariants, workflow | Typed call shape, identity, cache/invalidation | Round-trip and boundary behavior |
|
|
48
|
+
|
|
49
|
+
Completion means every changed endpoint has an entry in every applicable
|
|
50
|
+
column and the repository's formatter, linter, typechecker, and tests pass.
|