@shivaedev/effect-react 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 - 2026-10-18
4
+
5
+ ### Added
6
+
7
+ - `authAction` in `client/authAction.ts` binds Better Auth client promises to native action atoms for `useAction`, with pending state and typed returned failures. Rejected promises fail with the exported `AuthActionUnavailable` error; `AuthActionResult` describes the structural client result.
8
+
9
+ ## 0.5.1 - 2026-10-18
10
+
11
+ ### Fixed
12
+
13
+ - Route loaders and `loadQuery` recheck the owning `SessionBoundary` session once when a query fails with `Unauthorized`, so sign-in redirects can see an ended session.
14
+
3
15
  ## 0.5.0 - 2026-10-18
4
16
 
5
17
  ### Breaking
package/README.md CHANGED
@@ -137,10 +137,35 @@ create.dispatch({
137
137
  });
138
138
  ```
139
139
 
140
- `NotesClient` is an application's `AtomRpc.Service` with `ListNotes` and `CreateNote` RPCs. The native mutation's reactivity keys refresh the matching query after success. The package's rendered native RPC fixture exercises this shape through a real repository, including a rejected save that preserves the list.
140
+ `NotesClient` is an application's `AtomRpc.Service` with `ListNotes` and `CreateNote` RPCs. The native mutation's reactivity keys refresh the matching query after success.
141
141
 
142
142
  An action's `dispatch(input)` returns `void`. Read the atom's latest result from `data`, `cause` or `result`. With the default `Atom.fn` action, a later dispatch interrupts the earlier work and the shared state reports the later result. Disable duplicate submissions or give independent work distinct native action atoms. When one caller needs its own success or failure, run the actual RPC Effect through its runtime, as the editor's `save` does; shared atom state cannot identify one invocation's completion.
143
143
 
144
+ ### Better Auth actions
145
+
146
+ `authAction` from `@shivaedev/effect-react/client/authAction.ts` turns a Better Auth client call into a native action atom. Create it with the client, then pass it to `useAction` for the same `pending`, `data`, `cause`, `result` and `dispatch` shape as commands:
147
+
148
+ ```tsx
149
+ const signIn = authAction((credentials: { email: string; password: string }, signal) =>
150
+ authClient.signIn.email(credentials, { signal }),
151
+ );
152
+
153
+ function SignInButton({ email, password }: { email: string; password: string }) {
154
+ const action = useAction(signIn);
155
+ const failure = AsyncResult.error(action.result);
156
+ return (
157
+ <>
158
+ <button disabled={action.pending} onClick={() => action.dispatch({ email, password })}>
159
+ {action.pending ? "Signing in…" : "Sign in"}
160
+ </button>
161
+ {Option.isSome(failure) && <p role="alert">Sign-in failed</p>}
162
+ </>
163
+ );
164
+ }
165
+ ```
166
+
167
+ The example uses `authAction`, `useAction`, `effect/reactivity/AsyncResult` and `effect/Option`, with an application-owned `authClient`. Returned auth errors keep their fields in the typed failure channel. Rejected promises become `AuthActionUnavailable` with the original rejection in `cause`. The application chooses error text and what happens after success.
168
+
144
169
  ### Route loaders with TanStack Router
145
170
 
146
171
  TanStack Router owns routes, navigation, loader data and error components. `@shivaedev/effect-react/loadQuery.ts` supplies `loadQuery(registry, atom, loaderContext)` to read a contract query atom in the same registry as React. The atom runs through its native RPC runtime; the loader does not create another runtime or query cache.
@@ -195,7 +220,7 @@ export function App() {
195
220
  }
196
221
  ```
197
222
 
198
- `defaultPendingMs: Infinity` keeps the current route's data visible during navigation; the shell reads TanStack's `isLoading` to display pending state. The new route commits when its loader settles. This is TanStack's pending policy, separate from `useQuery`'s retained display. The render fixture proves navigation between two note payloads, shared loader/component query execution, and a declared RPC failure reaching the error component as the original tagged error.
223
+ `defaultPendingMs: Infinity` keeps the current route's data visible during navigation; the shell reads TanStack's `isLoading` to display pending state. The new route commits when its loader settles. This is TanStack's pending policy, separate from `useQuery`'s retained display. The loader and the component share one query execution.
199
224
 
200
225
  `loadQuery` waits through a native query refresh instead of returning its retained success. Typed failures are thrown directly, so the route can inspect `_tag` rather than an Effect fiber wrapper. Defects remain failed Promises. TanStack's loader `abortController` cancels the waiting subscription and rejects with `name: "AbortError"`; other readers of the same query can still complete. The application owns the registry's lifetime and disposes it when its client or session ends. Use the same registry for router context and `RegistryContext.Provider`.
201
226
 
@@ -356,22 +381,9 @@ const query = Atom.swr(atom, {
356
381
 
357
382
  `useQuery(atom, options?)` adds `refresh(): void`; `useAction` adds `dispatch(input): void`. `QueryOptions.keepPreviousData` defaults to `false` and controls retention across query atom changes. Same-identity refresh retention is always enabled. `result` and `cause` describe the current native query even when `data` displays a previous atom's success. Success, input and typed failure types are inferred from the supplied atom.
358
383
 
359
- | Defining module | Exports |
360
- | --- | --- |
361
- | `client/resultState.ts` | `useQuery`, `useAction`, `isUnauthorized`; `ResultState`, `QueryOptions`, `QueryState`, `ActionState`. |
362
- | `client/sessionBoundary.ts` | `SessionBoundary`, `useSessionRecheck`; `SessionBoundaryProps`. |
363
- | `client/editor.ts` | `useEditor`; `Editor`, `EditorConfig`. |
364
- | `client/create.ts` | `useCreate`; `Create`, `CreateConfig`. |
365
- | `resumeSignal.ts` | `resumeSignal`; `ResumeSource`, `ResumeWindow`, `ResumeOptions`. |
366
- | `loadQuery.ts` | `loadQuery`. |
367
- | `client/saveState.ts` | `useSaveState`; `SaveState`. |
368
- | `fieldRejection.ts` | `fieldRejectionOf`, `rejecting`; `FieldRejection`, `RejectField`, `FieldRejectionMapping`, `AtomServices`. |
369
- | `client/testing.ts` | `render`, `eventually`, `expectNoReactWarnings`; `Rendered`. |
370
- | `submission.ts` | `submission`; `Submission`, `SubmissionConfig`. |
371
-
372
- Import the defining `.ts` module, such as `@shivaedev/effect-react/client/editor.ts`. There is no root import. `SessionBoundaryProps` supplies `session`, `identify`, `connect`, `recheck`, a client-to-children function and optional `signedOut`; `useSessionRecheck()` reads that boundary's callback.
384
+ Import the defining `.ts` module, such as `@shivaedev/effect-react/client/editor.ts`. There is no root import. `useSessionRecheck()` from `client/sessionBoundary.ts` reads the boundary's `recheck` callback.
373
385
 
374
- `Editor` exposes `query`, optional `form` and `SaveState`; `Create` exposes `form`, `created` and `SaveState`. `SaveState` has `save`, `saving`, `dirty`, `failure` and `revert`. Prefer the two complete hooks when binding a form to a command. The lower-level form modules expose the same composition pieces: `submission` makes a form and its submission snapshot reader, `rejecting` maps save failures through a submitter, and `useSaveState` observes submission and invokes a success callback.
386
+ Prefer `useEditor` and `useCreate` when binding a form to a command. The lower-level modules expose the same composition pieces: `submission` from `submission.ts` makes a form and its submission snapshot reader, `rejecting` from `fieldRejection.ts` maps save failures through a submitter, and `useSaveState` from `client/saveState.ts` observes submission and invokes a success callback.
375
387
 
376
388
  ### Render tests in happy-dom
377
389
 
@@ -458,13 +470,7 @@ it("renders a query and refetches it after a command through a test RPC layer",
458
470
 
459
471
  `render(node)` returns the attached `container`, its fresh native `registry`, `render(nextNode)` to update the same root and registry, and `unmount()` to release the root, registry and container. Always unmount in `finally`. Keep component interactions inside React's `act`. `eventually(assert)` retries the assertion for up to three seconds, allowing pending responses to settle inside `act` between attempts; it throws the last assertion failure on timeout. `expectNoReactWarnings()` registers a Vitest hook that fails a test if it logged to `console.error`.
460
472
 
461
- The RPC layer stays application-owned: replace these in-memory handlers with the service Layer your component needs. This test proves rendered query data and command-triggered invalidation in happy-dom, without an HTTP server or a physical browser. The generic `effect-test` package has no React dependency.
462
-
463
- ### Testing a feature through its stack
464
-
465
- The [order feature’s application-owned kit](https://github.com/ShivaeDev/platform/tree/main/packages/effect-react/src/test-support/order-example) uses `test-story` above `effect-test` to compose real PostgreSQL, HTTP RPC and the actual React components. Its screen traits mount fresh native atom registries and scoped roots; its verbs dispatch DOM events and wait for rendered output. The [stories](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/orderStory.dom.spec.tsx) verify list refresh through `useAction` and rejected-input retention and retry through `useEditor`, with persistence read from a separate PostgreSQL pool. These tests require `PLATFORM_EFFECT_SQL_TEST_DATABASE_URL`; DOM and loopback HTTP establish no physical browser, SSR or mobile behavior.
466
-
467
- The [reference PostgreSQL feature](https://github.com/ShivaeDev/platform/blob/main/docs/framework/composed-example.md) uses the same leased database infrastructure and a scoped story verb for its real server and rendered clients. Its stories verify commit visibility, live refresh of a second editor, retained local edits and rollback without publication. To start an application, use the [copyable starter](https://github.com/ShivaeDev/platform/tree/main/examples/starter) with `pnpm create @shivaedev/app my-app`.
473
+ The RPC layer stays application-owned: replace these in-memory handlers with the service Layer your component needs.
468
474
 
469
475
  ### Install, setup and limits
470
476
 
@@ -478,12 +484,7 @@ For editor, create or lower-level form modules, also install the optional form p
478
484
  pnpm add @shivaedev/effect-form
479
485
  ```
480
486
 
481
- Query, action, session and resume modules do not require effect-form. The package declares Node 24 or newer and peers on Effect, atom-react and React; this repository checks Effect and atom-react `4.0.4` with React `19.3.0`.
487
+ Query, action, session and resume modules do not require effect-form. The package needs Node 24 or newer.
482
488
 
483
489
  - Query identity, TTL, invalidation and action concurrency remain native atom policies. Put the registry at the lifetime boundary those policies need.
484
- - Action state describes the atom's latest result. A `dispatch` is not a Promise or a separate result handle.
485
- - A session generation is application-defined. Credentials captured by `connect` require a changed generation when they rotate.
486
490
  - The package has no SSR registry/hydration adapter, offline write queue or optimistic reconciliation policy.
487
- - DOM tests and an injected resume source do not prove real-browser or mobile lifecycle behavior. The real HTTP order fixture proves its local composition, not a deployed host's auth, cancellation or rendering policy.
488
-
489
- The full design is in [the north star](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/docs/north-star.md). Implementation status and unresolved choices are in [the roadmap](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/docs/roadmap.md).
@@ -0,0 +1,15 @@
1
+ import * as Atom from "effect/reactivity/Atom";
2
+ declare const AuthActionUnavailable_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
3
+ readonly _tag: "AuthActionUnavailable";
4
+ } & Readonly<A>;
5
+ export declare class AuthActionUnavailable extends AuthActionUnavailable_base<{
6
+ readonly cause: unknown;
7
+ }> {
8
+ }
9
+ export interface AuthActionResult<A, E> {
10
+ readonly data: A;
11
+ readonly error: E | null;
12
+ }
13
+ export declare function authAction<TInput, A, E extends object = never>(run: (input: TInput, signal: AbortSignal) => Promise<AuthActionResult<A, E>>): Atom.AtomResultFn<TInput, A, E | AuthActionUnavailable>;
14
+ export {};
15
+ //# sourceMappingURL=authAction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authAction.d.ts","sourceRoot":"","sources":["../../src/client/authAction.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;;;;AAE/C,qBAAa,qBAAsB,SAAQ,2BAA0C;IACpF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB,CAAC;CAAG;AAEL,MAAM,WAAW,gBAAgB,CAAC,CAAC,EAAE,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;CACzB;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,KAAK,EAC7D,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAC1E,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,qBAAqB,CAAC,CAOzD"}
@@ -0,0 +1,11 @@
1
+ import { Data, Effect } from "effect";
2
+ import * as Atom from "effect/reactivity/Atom";
3
+ export class AuthActionUnavailable extends Data.TaggedError("AuthActionUnavailable") {
4
+ }
5
+ export function authAction(run) {
6
+ return Atom.fn()((input) => Effect.tryPromise({
7
+ catch: (cause) => new AuthActionUnavailable({ cause }),
8
+ try: (signal) => run(input, signal),
9
+ }).pipe(Effect.flatMap((result) => (result.error === null ? Effect.succeed(result.data) : Effect.fail(result.error)))));
10
+ }
11
+ //# sourceMappingURL=authAction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authAction.js","sourceRoot":"","sources":["../../src/client/authAction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAE/C,MAAM,OAAO,qBAAsB,SAAQ,IAAI,CAAC,WAAW,CAAC,uBAAuB,CAEjF;CAAG;AAOL,MAAM,UAAU,UAAU,CACzB,GAA4E;IAE5E,OAAO,IAAI,CAAC,EAAE,EAAU,CAAC,CAAC,KAAK,EAAE,EAAE,CAClC,MAAM,CAAC,UAAU,CAAC;QACjB,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,qBAAqB,CAAC,EAAE,KAAK,EAAE,CAAC;QACtD,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC;KACnC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CACtH,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sessionBoundary.d.ts","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0C,KAAK,SAAS,EAAmC,MAAM,OAAO,CAAC;AAGhH,MAAM,WAAW,oBAAoB,CAAC,QAAQ,EAAE,OAAO;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,SAAS,CAAC;IAClD,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAC/B;AAED,wBAAgB,iBAAiB,IAAI,MAAM,IAAI,CAE9C;AAsDD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAClD,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,GACR,EAAE,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,SAAS,CAUrD"}
1
+ {"version":3,"file":"sessionBoundary.d.ts","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0C,KAAK,SAAS,EAAmC,MAAM,OAAO,CAAC;AAIhH,MAAM,WAAW,oBAAoB,CAAC,QAAQ,EAAE,OAAO;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,SAAS,CAAC;IAClD,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAC/B;AAED,wBAAgB,iBAAiB,IAAI,MAAM,IAAI,CAE9C;AAuDD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAClD,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,GACR,EAAE,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,SAAS,CAUrD"}
@@ -2,6 +2,7 @@ import { RegistryContext } from "@effect/atom-react";
2
2
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
3
  import { createContext, createElement, Fragment, useContext, useEffect, useState } from "react";
4
4
  import { DraftSessionContext } from "@shivaedev/effect-form/client/react.ts";
5
+ import { registerSessionRecheck } from "#internal/sessionRecheck.ts";
5
6
  import { useDraftOwner } from "./useDraftOwner.js";
6
7
  const RecheckContext = createContext(() => undefined);
7
8
  export function useSessionRecheck() {
@@ -30,6 +31,7 @@ function hidden(client) {
30
31
  }
31
32
  function Generation({ session, connect, recheck, children }) {
32
33
  const [owned, own] = useState(() => owning(connect(session)));
34
+ useEffect(() => registerSessionRecheck(owned.registry, recheck), [owned, recheck]);
33
35
  useEffect(() => {
34
36
  owned.mounted += 1;
35
37
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"sessionBoundary.js","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sessionBoundary.js","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
2
+ export declare function registerSessionRecheck(registry: AtomRegistry.AtomRegistry, recheck: () => void): () => void;
3
+ export declare function recheckUnauthorized(registry: AtomRegistry.AtomRegistry, error: unknown): Promise<void>;
4
+ //# sourceMappingURL=sessionRecheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionRecheck.d.ts","sourceRoot":"","sources":["../../src/internal/sessionRecheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAIpE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK3G;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5G"}
@@ -0,0 +1,14 @@
1
+ import { Predicate } from "effect";
2
+ const rechecks = new WeakMap();
3
+ export function registerSessionRecheck(registry, recheck) {
4
+ rechecks.set(registry, recheck);
5
+ return () => {
6
+ rechecks.delete(registry);
7
+ };
8
+ }
9
+ export async function recheckUnauthorized(registry, error) {
10
+ if (Predicate.isTagged(error, "Unauthorized")) {
11
+ await rechecks.get(registry)?.();
12
+ }
13
+ }
14
+ //# sourceMappingURL=sessionRecheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionRecheck.js","sourceRoot":"","sources":["../../src/internal/sessionRecheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAGnC,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAyC,CAAC;AAEtE,MAAM,UAAU,sBAAsB,CAAC,QAAmC,EAAE,OAAmB;IAC9F,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,GAAG,EAAE;QACX,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3B,CAAC,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAmC,EAAE,KAAc;IAC5F,IAAI,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,EAAE,CAAC;QAC/C,MAAM,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;IAClC,CAAC;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"loadQuery.d.ts","sourceRoot":"","sources":["../src/loadQuery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAE/D,wBAAsB,SAAS,CAAC,CAAC,EAAE,CAAC,EACnC,QAAQ,EAAE,YAAY,CAAC,YAAY,EACnC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAC9C,EAAE,eAAe,EAAE,EAAE,IAAI,CAAC,eAAe,EAAE,iBAAiB,CAAC,GAC3D,OAAO,CAAC,CAAC,CAAC,CAgBZ"}
1
+ {"version":3,"file":"loadQuery.d.ts","sourceRoot":"","sources":["../src/loadQuery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAG/D,wBAAsB,SAAS,CAAC,CAAC,EAAE,CAAC,EACnC,QAAQ,EAAE,YAAY,CAAC,YAAY,EACnC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAC9C,EAAE,eAAe,EAAE,EAAE,IAAI,CAAC,eAAe,EAAE,iBAAiB,CAAC,GAC3D,OAAO,CAAC,CAAC,CAAC,CAiBZ"}
package/dist/loadQuery.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { Effect } from "effect";
2
2
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
+ import { recheckUnauthorized } from "#internal/sessionRecheck.ts";
3
4
  export async function loadQuery(registry, atom, { abortController }) {
4
5
  const { signal } = abortController;
5
6
  try {
6
7
  const result = await Effect.runPromise(Effect.result(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true })), { signal });
7
8
  if (result._tag === "Failure") {
9
+ await recheckUnauthorized(registry, result.failure);
8
10
  throw result.failure;
9
11
  }
10
12
  return result.success;
@@ -1 +1 @@
1
- {"version":3,"file":"loadQuery.js","sourceRoot":"","sources":["../src/loadQuery.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAE/D,MAAM,CAAC,KAAK,UAAU,SAAS,CAC9B,QAAmC,EACnC,IAA8C,EAC9C,EAAE,eAAe,EAA4C;IAE7D,MAAM,EAAE,MAAM,EAAE,GAAG,eAAe,CAAC;IACnC,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACtI,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,MAAM,CAAC,OAAO,CAAC;QACtB,CAAC;QACD,OAAO,MAAM,CAAC,OAAO,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,wBAAwB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACvE,QAAQ,CAAC,IAAI,GAAG,YAAY,CAAC;YAC7B,MAAM,QAAQ,CAAC;QAChB,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"loadQuery.js","sourceRoot":"","sources":["../src/loadQuery.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE,MAAM,CAAC,KAAK,UAAU,SAAS,CAC9B,QAAmC,EACnC,IAA8C,EAC9C,EAAE,eAAe,EAA4C;IAE7D,MAAM,EAAE,MAAM,EAAE,GAAG,eAAe,CAAC;IACnC,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACtI,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YACpD,MAAM,MAAM,CAAC,OAAO,CAAC;QACtB,CAAC;QACD,OAAO,MAAM,CAAC,OAAO,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,wBAAwB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACvE,QAAQ,CAAC,IAAI,GAAG,YAAY,CAAC;YAC7B,MAAM,QAAQ,CAAC;QAChB,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/effect-react",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "React query, action, editor and session hooks for native Effect atoms",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-react#readme",
6
6
  "bugs": {
@@ -28,11 +28,11 @@
28
28
  "types": "./dist/*.d.ts",
29
29
  "default": "./dist/*.js"
30
30
  },
31
+ "./internal/*": null,
31
32
  "./package.json": "./package.json"
32
33
  },
33
34
  "files": [
34
35
  "dist",
35
- "docs",
36
36
  "src",
37
37
  "!src/**/*.test.ts",
38
38
  "!src/**/*.test.tsx",
@@ -51,24 +51,23 @@
51
51
  "@effect/platform-node": "4.0.4",
52
52
  "@effect/sql-pg": "4.0.4",
53
53
  "@effect/sql-sqlite-node": "4.0.4",
54
- "@shivaedev/effect-pg-boss": "0.9.0",
54
+ "@shivaedev/effect-pg-boss": "0.10.0",
55
55
  "@shivaedev/effect-service": "0.4.0",
56
- "@shivaedev/effect-sql": "0.5.1",
56
+ "@shivaedev/effect-sql": "0.5.2",
57
57
  "@shivaedev/effect-test": "0.6.0",
58
- "@shivaedev/local-postgres": "0.4.0",
59
- "@shivaedev/platform": "0.16.0",
60
- "@shivaedev/quality": "0.25.0",
61
- "@shivaedev/test-story": "0.7.1",
58
+ "@shivaedev/local-postgres": "0.4.1",
59
+ "@shivaedev/platform": "0.17.0",
60
+ "@shivaedev/quality": "0.27.0",
61
+ "@shivaedev/test-story": "0.9.0",
62
62
  "@tanstack/react-router": "1.170.42",
63
63
  "@types/node": "24.19.2",
64
64
  "@types/react": "19.3.0",
65
65
  "@types/react-dom": "19.3.0",
66
- "@typescript/native": "npm:typescript@7.0.2",
67
66
  "effect": "4.0.4",
68
67
  "happy-dom": "20.14.6",
69
68
  "react": "19.3.0",
70
69
  "react-dom": "19.3.0",
71
- "typescript": "npm:@typescript/typescript6@6.0.2",
70
+ "typescript": "7.0.2",
72
71
  "vitest": "5.0.3"
73
72
  },
74
73
  "peerDependencies": {
@@ -0,0 +1,22 @@
1
+ import { Data, Effect } from "effect";
2
+ import * as Atom from "effect/reactivity/Atom";
3
+
4
+ export class AuthActionUnavailable extends Data.TaggedError("AuthActionUnavailable")<{
5
+ readonly cause: unknown;
6
+ }> {}
7
+
8
+ export interface AuthActionResult<A, E> {
9
+ readonly data: A;
10
+ readonly error: E | null;
11
+ }
12
+
13
+ export function authAction<TInput, A, E extends object = never>(
14
+ run: (input: TInput, signal: AbortSignal) => Promise<AuthActionResult<A, E>>,
15
+ ): Atom.AtomResultFn<TInput, A, E | AuthActionUnavailable> {
16
+ return Atom.fn<TInput>()((input) =>
17
+ Effect.tryPromise({
18
+ catch: (cause) => new AuthActionUnavailable({ cause }),
19
+ try: (signal) => run(input, signal),
20
+ }).pipe(Effect.flatMap((result) => (result.error === null ? Effect.succeed(result.data) : Effect.fail(result.error)))),
21
+ );
22
+ }
@@ -2,6 +2,7 @@ import { RegistryContext } from "@effect/atom-react";
2
2
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
3
  import { createContext, createElement, Fragment, type ReactNode, useContext, useEffect, useState } from "react";
4
4
  import { DraftSessionContext } from "@shivaedev/effect-form/client/react.ts";
5
+ import { registerSessionRecheck } from "#internal/sessionRecheck.ts";
5
6
  import { useDraftOwner } from "./useDraftOwner.ts";
6
7
  export interface SessionBoundaryProps<TSession, TClient> {
7
8
  readonly children: (client: TClient) => ReactNode;
@@ -49,6 +50,7 @@ interface GenerationProps<TSession, TClient> {
49
50
  }
50
51
  function Generation<TSession, TClient>({ session, connect, recheck, children }: GenerationProps<TSession, TClient>): ReactNode {
51
52
  const [owned, own] = useState(() => owning(connect(session)));
53
+ useEffect(() => registerSessionRecheck(owned.registry, recheck), [owned, recheck]);
52
54
  useEffect(() => {
53
55
  owned.mounted += 1;
54
56
  return () => {
@@ -0,0 +1,17 @@
1
+ import { Predicate } from "effect";
2
+ import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
+
4
+ const rechecks = new WeakMap<AtomRegistry.AtomRegistry, () => void>();
5
+
6
+ export function registerSessionRecheck(registry: AtomRegistry.AtomRegistry, recheck: () => void): () => void {
7
+ rechecks.set(registry, recheck);
8
+ return () => {
9
+ rechecks.delete(registry);
10
+ };
11
+ }
12
+
13
+ export async function recheckUnauthorized(registry: AtomRegistry.AtomRegistry, error: unknown): Promise<void> {
14
+ if (Predicate.isTagged(error, "Unauthorized")) {
15
+ await rechecks.get(registry)?.();
16
+ }
17
+ }
package/src/loadQuery.ts CHANGED
@@ -3,6 +3,7 @@ import { Effect } from "effect";
3
3
  import type * as AsyncResult from "effect/reactivity/AsyncResult";
4
4
  import type * as Atom from "effect/reactivity/Atom";
5
5
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
6
+ import { recheckUnauthorized } from "#internal/sessionRecheck.ts";
6
7
 
7
8
  export async function loadQuery<A, E>(
8
9
  registry: AtomRegistry.AtomRegistry,
@@ -13,6 +14,7 @@ export async function loadQuery<A, E>(
13
14
  try {
14
15
  const result = await Effect.runPromise(Effect.result(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true })), { signal });
15
16
  if (result._tag === "Failure") {
17
+ await recheckUnauthorized(registry, result.failure);
16
18
  throw result.failure;
17
19
  }
18
20
  return result.success;
@@ -1,45 +0,0 @@
1
- # North star
2
-
3
- ## The problem
4
-
5
- Native Effect atoms already know how to run, refresh and share an asynchronous result. A React application still needs to decide what to display while they work, what to keep when a request fails and which lifetime owns their registry. Repeating those decisions in every screen makes them drift. One screen blanks during a refresh, another loses a draft when a response arrives, and a third keeps the previous session's state after the user changes.
6
-
7
- Editing combines two kinds of state. A server row can change independently, while the user types encoded input values that a schema must decode before saving. The latest server response is not necessarily the user's latest intent. Whole-form replacement makes a simple screen overwrite newer edits; leaving the form alone makes untouched fields stale. A create form adds another race: clearing the successful entry must not clear what the user has already begun typing for the next one.
8
-
9
- ## The ideal
10
-
11
- React components read state in the words they render: data, failure, pending and refreshing. An editor says which query supplies its row, which fields describe its inputs and which Effect saves the decoded values. A session root says which identity owns those screens. Each definition stays small because the package supplies repeated coordination, while native Effect and effect-form remain the owners of the underlying work.
12
-
13
- Good client state has these properties:
14
-
15
- - Useful data can stay on screen during refresh and after a refresh failure. Showing retained data does not hide the failed result.
16
- - Query identity changes reset displayed data by default. Applications can opt into `keepPreviousData` for payload transitions; displayed data still clears when the registry owner changes.
17
- - A draft belongs to one query identity and one session generation. Changing either must not transfer edits into a different record or person's screen.
18
- - A refresh, a normalized save result and a create reset respect edits made after the operation began. Untouched fields can adopt new server values without replacing newer input.
19
- - Failure types reach the component. A field rejection names its field; another failure remains available for application-owned wording.
20
- - An action reports the native atom's current state honestly. An imperative caller that needs one invocation's outcome runs an Effect with that lifetime instead of guessing from a shared latest-result atom.
21
- - A registry has an owner. Session replacement and unmount release the old generation, including work created around React's StrictMode and Activity lifecycles.
22
-
23
- ## Boundaries
24
-
25
- The root repository's one path for client state is this package. It should add React coordination where applications repeat it, without taking a neighbouring package's job:
26
-
27
- - Native atoms own execution, identity, cache retention, invalidation and action scheduling.
28
- - effect-contract owns query and command declarations, declared rejections and native client bindings. Its shared resume source also works without React.
29
- - effect-form owns encoded fields, schema decoding, validation, submission and draft merge rules. The editor and create hooks arrange those pieces around rows and command results.
30
- - The platform request modules and the application's auth integration verify identity. The session boundary owns a client-side generation; its `recheck` callback asks the auth owner for a verdict.
31
- - The application owns transport, loading/error wording, credential rotation policy and the point where a runtime runs a particular Effect.
32
-
33
- ## Trade-offs
34
-
35
- 1. **Native ownership before convenience.** A second query cache or parallel form model would make easy call sites look simpler while giving the application two places to understand identity and cleanup. Thin bindings keep the native state visible.
36
- 2. **Identity isolation before draft continuity.** A draft is valuable only within the record and session that created it. Switching identities starts fresh even if retaining a previous draft would save typing.
37
- 3. **Newer intent before replacing the whole form.** A response updates what it can without erasing fields the user edited meanwhile. A user who wants the server's latest values can explicitly revert.
38
- 4. **Truthful shared state before per-call ergonomics.** One atom's result cannot describe several concurrent invocations independently. The action hook preserves its native semantics rather than implying a Promise belongs to one dispatch.
39
- 5. **Observable evidence before brevity.** A helper earns its place by removing repeated application code and having tests for the races and lifetimes it claims to handle. A DOM fixture proves that fixture, not another runtime or deployment.
40
-
41
- ## What it leaves out
42
-
43
- The package does not define another RPC contract language, replace native atom scheduling or decide an application's auth policy. It does not supply a general form language, an optimistic reconciliation strategy or an offline write queue. Server rendering and mobile host integration need their own architecture and evidence; a resume callback alone cannot establish them.
44
-
45
- Use [README.md](../README.md) for examples and [roadmap.md](./roadmap.md) for what is implemented and which choices remain with the maintainer.
package/docs/roadmap.md DELETED
@@ -1,30 +0,0 @@
1
- # Roadmap
2
-
3
- This roadmap owns the package's hooks, form coordination and session-registry implementation. The [framework roadmap](https://github.com/ShivaeDev/platform/blob/main/docs/framework/roadmap.md) owns composed feature fixtures, adoption and host/deployment validation. A local hook test is evidence for its own runtime and scenario.
4
-
5
- ## Built
6
-
7
- - [x] Opt-in `keepPreviousData` retains the last successful query value across payload changes, pending requests and failures; the default resets it. Registry replacement clears retained data, superseded responses do not replace the latest result, and disabling retention drops an inherited value. [Rendered query evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/queryPayload.dom.spec.ts).
8
- - [x] Public `client/testing.ts` render helpers supply an isolated native registry per mount, retain it on rerender, and dispose native resources and remove the container on idempotent unmount. [DOM evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/client/testing.dom.test.tsx) also exercises query refetch after a command through a test RPC Layer; it does not establish browser networking.
9
- - [x] Native query and action hooks with inferred input/success/failure types, retained data, pending/refresh state and explicit refresh/dispatch methods. DOM tests cover query identity changes, shared query execution and latest-dispatch interruption; compiler fixtures cover types.
10
- - [x] A session generation owning its client, registry and subtree, with replacement/sign-out cleanup, query Unauthorized rechecking and retained screens until the auth owner changes the session.
11
- - [x] Unauthorized saves in `useCreate` and `useEditor` invoke the session callback exactly once and retain the draft and typed failure, including after rerendering and further edits. [Rendered save evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/client/saveState.dom.test.tsx).
12
- - [x] Unauthorized action failures invoke the session callback once per failed result and retain successful data and the typed cause, including after rerendering. [Rendered action evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/client/sessionBoundary.dom.test.tsx).
13
- - [x] Queries and actions preserve a successful `undefined` as `Option.some(undefined)` with settled pending/refreshing flags. Defect and interruption causes remain available, are not Unauthorized and do not request a session recheck. [Rendered result-state evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/client/resultState.dom.test.ts).
14
- - [x] `eventually` rethrows the last assertion failure after its three-second deadline; `expectNoReactWarnings` fails a test that logs to `console.error` and restores the spy even after that assertion fails. [Testing-helper evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/client/testing.dom.test.tsx).
15
- - [x] Session cleanup across StrictMode effect replay and Activity hiding, reveal and hidden unmount, with credential rotation tested against the chosen identity key.
16
- - [x] `useEditor` composing query, fields and save Effect, with initial loading, per-query forms, per-field refresh/saved-result merges, normalized results and in-flight edits covered by rendered tests.
17
- - [x] `useCreate` starting a fresh form after success while retaining fields edited during schema decoding or the request; submissions through the form API settle the hooks too.
18
- - [x] Tagged field-rejection mapping, a typed custom mapper and compile-time requirements for rejection field types that exceed the form's names; other save failures remain separately available.
19
- - [x] A resume module using effect-contract's shared implementation, with visible browser events, an injected native source, listener disposal and native stale-while-revalidate policy covered by tests.
20
- - [x] TanStack Router query loaders sharing the native registry, with tagged RPC failures, refresh waiting and abort handling. Render tests cover shared query execution, the route error component and previous data during navigation.
21
-
22
- ## Next
23
-
24
- - [ ] Verify changing hook configuration during one form lifetime before documenting any supported reconfiguration policy. Creation currently captures its initial configuration, and an editor holds submission functions per query identity.
25
-
26
- ## Open questions
27
-
28
- - Do consumers need an independently scoped action helper, or are distinct native atoms and actual Effects enough for imperative per-invocation outcomes? Keep native shared-action semantics unless a demonstrated use case requires more.
29
- - What per-request registry and hydration wiring does the TanStack Router host need for SSR? Client render fixtures do not establish SSR isolation.
30
- - Which hook configuration values should be allowed to change without replacing the form? A rule for changing schemas, runtimes, save functions or initial values needs consumer evidence and explicit lifetime tests.