@shivaedev/effect-react 0.0.0 → 0.1.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +86 -1
  4. package/dist/create.d.ts +17 -0
  5. package/dist/create.d.ts.map +1 -0
  6. package/dist/create.js +24 -0
  7. package/dist/create.js.map +1 -0
  8. package/dist/editor.d.ts +20 -0
  9. package/dist/editor.d.ts.map +1 -0
  10. package/dist/editor.js +34 -0
  11. package/dist/editor.js.map +1 -0
  12. package/dist/field-rejection.d.ts +23 -0
  13. package/dist/field-rejection.d.ts.map +1 -0
  14. package/dist/field-rejection.js +16 -0
  15. package/dist/field-rejection.js.map +1 -0
  16. package/dist/form.d.ts +7 -0
  17. package/dist/form.d.ts.map +1 -0
  18. package/dist/form.js +3 -0
  19. package/dist/form.js.map +1 -0
  20. package/dist/index.d.ts +7 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +4 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/result-state.d.ts +21 -0
  25. package/dist/result-state.d.ts.map +1 -0
  26. package/dist/result-state.js +35 -0
  27. package/dist/result-state.js.map +1 -0
  28. package/dist/resume-signal.d.ts +17 -0
  29. package/dist/resume-signal.d.ts.map +1 -0
  30. package/dist/resume-signal.js +21 -0
  31. package/dist/resume-signal.js.map +1 -0
  32. package/dist/save-state.d.ts +11 -0
  33. package/dist/save-state.d.ts.map +1 -0
  34. package/dist/save-state.js +34 -0
  35. package/dist/save-state.js.map +1 -0
  36. package/dist/session-boundary.d.ts +12 -0
  37. package/dist/session-boundary.d.ts.map +1 -0
  38. package/dist/session-boundary.js +42 -0
  39. package/dist/session-boundary.js.map +1 -0
  40. package/dist/submission.d.ts +17 -0
  41. package/dist/submission.d.ts.map +1 -0
  42. package/dist/submission.js +17 -0
  43. package/dist/submission.js.map +1 -0
  44. package/package.json +73 -4
  45. package/src/create.ts +43 -0
  46. package/src/editor.ts +56 -0
  47. package/src/field-rejection.ts +45 -0
  48. package/src/form.ts +6 -0
  49. package/src/index.ts +6 -0
  50. package/src/result-state.ts +56 -0
  51. package/src/resume-signal.ts +37 -0
  52. package/src/save-state.ts +50 -0
  53. package/src/session-boundary.ts +78 -0
  54. package/src/submission.ts +32 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-26
4
+
5
+ ### Added
6
+
7
+ - Add `useEditor`: binds a query atom, schema fields and a save Effect. It
8
+ creates the form when data first arrives, remounts it per query atom, merges
9
+ query refreshes and saved results per field, maps tagged
10
+ `{ _tag, field, message }` rejections (such as effect-contract field
11
+ rejections) onto fields by default and requires `rejectField` when a tagged
12
+ rejection's field type names a field the form lacks, exposes other save
13
+ failures and ignores submits while saving.
14
+ - Add `useCreate`: after a successful create it starts a fresh form from the
15
+ initial values, keeping fields edited while the save was in flight.
16
+ - Export `useEditor` and `useCreate` from the `@shivaedev/effect-react/form`
17
+ subpath; `@shivaedev/effect-form` (`^0.1.0`) is an optional peer dependency
18
+ needed only by that subpath.
19
+ - Add `SessionBoundary`: one credential-bound client and atom registry per
20
+ session generation, remounting its subtree and disposing the registry when the
21
+ session changes or ends. A generation hidden by `<Activity>` renders against a
22
+ fresh registry instead of a disposed one.
23
+ - Report `Unauthorized` query/action failures to the boundary's `recheck`
24
+ instead of clearing retained data.
25
+ - Add `resumeSignal`, combining visible `visibilitychange`, visible `online` and
26
+ an injectable native resume source for native `Atom.makeRefreshOnSignal` and
27
+ `Atom.swr`.
28
+ - Add native atom query and action hooks with retained data, refresh state and
29
+ typed action state.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShivaeDev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,88 @@
1
1
  # @shivaedev/effect-react
2
2
 
3
- This version only reserves the name. Releases are published from https://github.com/ShivaeDev/platform.
3
+ Small React hooks and a session boundary for native Effect atoms. Query data survives refreshes and failures when the underlying `AsyncResult` contains a previous success. Actions expose ordinary RPC success values and typed failures as native atom state.
4
+
5
+ This package targets Effect and `@effect/atom-react` **4.0.0-rc.112** and React 19. Native atoms own identity, caching, invalidation, serialization and cancellation. There is no second query cache or RPC contract language.
6
+
7
+ ## Native RPC usage
8
+
9
+ Given an `Api` service constructed with native `AtomRpc.Service`, and RPCs `Meals` and `RenameMeal`:
10
+
11
+ ```tsx
12
+ import { RegistryProvider } from "@effect/atom-react"
13
+ import { useAction, useQuery } from "@shivaedev/effect-react"
14
+ import { Option } from "effect"
15
+
16
+ function Meals() {
17
+ const meals = useQuery(Api.query("Meals", {}, {
18
+ reactivityKeys: ["meals"],
19
+ timeToLive: "1 minute",
20
+ }))
21
+ const rename = useAction(Api.mutation("RenameMeal"))
22
+
23
+ function save(id: string, title: string) {
24
+ rename.dispatch({
25
+ payload: { id, title },
26
+ reactivityKeys: ["meals"],
27
+ })
28
+ }
29
+
30
+ if (Option.isNone(meals.data)) {
31
+ return Option.isSome(meals.cause)
32
+ ? <button onClick={meals.refresh}>Retry</button>
33
+ : <p>Loading…</p>
34
+ }
35
+ return <MealList meals={meals.data.value} refreshing={meals.refreshing}
36
+ pending={rename.pending} onRename={save} />
37
+ }
38
+
39
+ function App() {
40
+ return <RegistryProvider><Meals /></RegistryProvider>
41
+ }
42
+ ```
43
+
44
+ `Api` and `MealList` above are application definitions. The hooks also accept non-RPC atoms built with `Atom.make`, `Atom.fn` or an atom runtime. Stable atom identities should be declared outside render, created with native `Atom.family`, or memoized; native `Api.query(...)` already supplies its own family.
45
+
46
+ ## Editing
47
+
48
+ `useEditor` and `useCreate` from `@shivaedev/effect-react/form` bind `@shivaedev/effect-form` to a query atom and a save Effect, for example from an `@shivaedev/effect-contract` binding `api`. `@shivaedev/effect-form` is an optional peer dependency: install it only when you import this subpath; the root entry does not load it. See the [editing guide](../../docs/framework/editing.md).
49
+
50
+ ```ts
51
+ const editor = useEditor({
52
+ query: api.get.query({ id }),
53
+ fields: FoodFields,
54
+ values: (food) => ({ name: food.name, grams: String(food.grams) }),
55
+ save: (values) => api.save.run({ id, ...values }),
56
+ runtime: FoodsClient.runtime,
57
+ })
58
+ // editor.query: QueryState; editor.form: Form | undefined until data first arrives
59
+ // editor.save(), editor.saving, editor.dirty, editor.failure (non-field failures), editor.revert()
60
+ ```
61
+
62
+ The form is created from the first successful value and replaced when the query atom changes, so a different id never inherits a draft. Each new query value and each saved result is received per field: untouched fields adopt it, fields edited meanwhile keep local input. A tagged failure shaped `{ _tag, field, message }` whose `field` names a form field, such as an `@shivaedev/effect-contract` `fieldRejection`, becomes that field's message; pass `rejectField` to map differently. When a tagged rejection's `field` type names a field the form lacks, `rejectField` is required at compile time. Other failures appear in `failure`. `save()` does nothing while a save is running; submitting through the form's own `submit` atom settles the same way. Render fields with `useField(editor.form, name)` from `@shivaedev/effect-form/react`.
63
+
64
+ `useCreate({ fields, initialValues, create, runtime, rejectField })` returns the same save state with an always-present `form` and `created: Option<A>`. After success it replaces the form with a fresh one at `initialValues`, carrying over fields changed while the create was in flight, so their messages and submitted state do not leak into the next entry.
65
+
66
+ ## State
67
+
68
+ Both hooks return:
69
+
70
+ - `result`: the unchanged native `AsyncResult`, including its discriminant and typed failure.
71
+ - `data`: `Option<A>`, using the current or previous successful result. A valid `undefined` success remains `Some(undefined)`.
72
+ - `cause`: `Option<Cause<E>>`, preserving failures, defects and interruption information.
73
+ - `pending`: whether the native result is waiting, including refreshes.
74
+ - `refreshing`: waiting while successful data is available.
75
+
76
+ `useQuery(atom)` additionally returns `refresh()`. It does not carry data across different query atoms. `useAction(atom)` additionally returns `dispatch(input): void`. Read its ordinary success value from `data` and errors from `cause` or the discriminated `result`. Dispatch does not return a completion promise. For imperative per-invocation outcomes (such as closing only the editor whose save succeeded), execute the actual native RPC Effect through your application runtime.
77
+
78
+ ## Scope and concurrency
79
+
80
+ Mount `SessionBoundary` at the authenticated root. For each session key it builds one credential-bound client and one registry, remounts its subtree when the key changes, renders `signedOut` without a session and disposes the outgoing registry. `connect` runs once per key, so a client must read a rotating credential per request, or the key must include a credential generation. A subtree hidden by `<Activity>` keeps its state and gets a fresh registry on reveal. Inside it, `useQuery` and `useAction` call the boundary's `recheck` when a result fails with a tagged `Unauthorized` error; they keep retained data until the auth owner ends the session. See [client lifecycle](../../docs/framework/client-lifecycle.md). SSR isolation is not implemented.
81
+
82
+ `resumeSignal({ window, native })` is an atom that increments when the page becomes visible, when the network returns while visible, and when an injected native resume source fires. Feed it to native `Atom.makeRefreshOnSignal` or to `Atom.swr` as `focusSignal`.
83
+
84
+ State belongs to the **supplied atom in the registry**, not to an individual hook call. In particular, native `Api.mutation("RenameMeal")` returns the same atom for that operation. Components using it share pending/result state and the native action's concurrency behavior. With the default native action, a later dispatch interrupts the earlier one; shared state then represents the later action. Disable duplicate submissions, or create distinct native action atoms when independent lifetimes are needed. Native registry retention and disposal govern cleanup. The package adds no optimistic policy, offline queue, form state or parallel action scheduler.
85
+
86
+ ## Validation
87
+
88
+ React DOM tests exercise native atom execution, refresh retention, typed failures and query identity changes. Compiler fixtures check success/input/error inference. The package smoke test installs its tarball into a standalone consumer and checks both compiler versions and NodeNext resolution.
@@ -0,0 +1,17 @@
1
+ import type { Decoded, Encoded, FieldFailure, Fields, Form, Services } from "@shivaedev/effect-form";
2
+ import { type Effect, Option, type Schema } from "effect";
3
+ import type * as Atom from "effect/unstable/reactivity/Atom";
4
+ import type { AtomServices, FieldRejectionMapping } from "./field-rejection.ts";
5
+ import { type SaveState } from "./save-state.ts";
6
+ export type CreateConfig<F extends Fields, Created, SE, R, ER> = {
7
+ readonly fields: Schema.Struct<F>;
8
+ readonly initialValues: Encoded<F>;
9
+ readonly create: (values: Decoded<F>) => Effect.Effect<Created, SE, AtomServices<R>>;
10
+ readonly runtime: Atom.AtomRuntime<Services<F, R>, ER>;
11
+ } & FieldRejectionMapping<F, SE>;
12
+ export interface Create<F extends Fields, Created, SE, ER> extends SaveState<SE | ER> {
13
+ readonly form: Form<F, Created, SE | FieldFailure, ER>;
14
+ readonly created: Option.Option<Created>;
15
+ }
16
+ export declare const useCreate: <F extends Fields, Created, SE, R, ER>(config: CreateConfig<F, Created, SE, R, ER>) => Create<F, Created, SE, ER>;
17
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACrG,OAAO,EAAE,KAAK,MAAM,EAAS,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AACjE,OAAO,KAAK,KAAK,IAAI,MAAM,iCAAiC,CAAC;AAE7D,OAAO,KAAK,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,iBAAiB,CAAC;AAG/D,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IACrF,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;CACvD,GAAG,qBAAqB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAEjC,MAAM,WAAW,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,CAAE,SAAQ,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC;IACpF,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,GAAG,YAAY,EAAE,EAAE,CAAC,CAAC;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;CACzC;AAOD,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,QAAQ,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,KAAG,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,CAiBtI,CAAC"}
package/dist/create.js ADDED
@@ -0,0 +1,24 @@
1
+ import { Equal, Option } from "effect";
2
+ import { useCallback, useState } from "react";
3
+ import { useSaveState } from "./save-state.js";
4
+ import { submission } from "./submission.js";
5
+ const editedSince = (current, submitted) => {
6
+ const before = new Map(Object.entries(submitted ?? current));
7
+ return Object.fromEntries(Object.entries(current).filter(([name, value]) => !Equal.equals(value, before.get(name))));
8
+ };
9
+ export const useCreate = (config) => {
10
+ const [{ initialValues, fresh }] = useState(() => {
11
+ const { fields, initialValues, runtime, create, rejectField } = config;
12
+ return { initialValues, fresh: () => submission({ fields, initialValues, runtime, save: create, rejectField }) };
13
+ });
14
+ const [current, replace] = useState(fresh);
15
+ const [created, record] = useState(Option.none());
16
+ const settled = useCallback((value) => {
17
+ const next = fresh();
18
+ next.form.values.set({ ...initialValues, ...editedSince(current.form.values.value, current.submitted()) });
19
+ record(Option.some(value));
20
+ replace(next);
21
+ }, [current, fresh, initialValues]);
22
+ return { ...useSaveState(current.form, settled), form: current.form, created };
23
+ };
24
+ //# sourceMappingURL=create.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.js","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,EAAE,MAAM,EAAe,MAAM,QAAQ,CAAC;AAEjE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE9C,OAAO,EAAkB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAc7C,MAAM,WAAW,GAAG,CAAmB,OAAU,EAAE,SAAwB,EAAqC,EAAE;IACjH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC;IAC7D,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACtH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAuC,MAA2C,EAA8B,EAAE;IAC1I,MAAM,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE;QAChD,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;QACvE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IAClH,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAyB,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1E,MAAM,OAAO,GAAG,WAAW,CAC1B,CAAC,KAAc,EAAE,EAAE;QAClB,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,aAAa,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;QAC3G,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,EACD,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,CAC/B,CAAC;IACF,OAAO,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC;AAChF,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { Decoded, Encoded, FieldFailure, Fields, Form, Services } from "@shivaedev/effect-form";
2
+ import { type Effect, type Schema } from "effect";
3
+ import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
+ import type * as Atom from "effect/unstable/reactivity/Atom";
5
+ import type { AtomServices, FieldRejectionMapping } from "./field-rejection.ts";
6
+ import { type QueryState } from "./result-state.ts";
7
+ import { type SaveState } from "./save-state.ts";
8
+ export type EditorConfig<F extends Fields, Row, QE, SE, R, ER> = {
9
+ readonly query: Atom.Atom<AsyncResult.AsyncResult<Row, QE>>;
10
+ readonly fields: Schema.Struct<F>;
11
+ readonly values: (row: Row) => Encoded<F>;
12
+ readonly save: (values: Decoded<F>) => Effect.Effect<Row, SE, AtomServices<R>>;
13
+ readonly runtime: Atom.AtomRuntime<Services<F, R>, ER>;
14
+ } & FieldRejectionMapping<F, SE>;
15
+ export interface Editor<F extends Fields, Row, QE, SE, ER> extends SaveState<SE | ER> {
16
+ readonly query: QueryState<Row, QE>;
17
+ readonly form: Form<F, Row, SE | FieldFailure, ER> | undefined;
18
+ }
19
+ export declare const useEditor: <F extends Fields, Row, QE, SE, R, ER>(config: EditorConfig<F, Row, QE, SE, R, ER>) => Editor<F, Row, QE, SE, ER>;
20
+ //# sourceMappingURL=editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACrG,OAAO,EAAE,KAAK,MAAM,EAAU,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,KAAK,WAAW,MAAM,wCAAwC,CAAC;AAC3E,OAAO,KAAK,KAAK,IAAI,MAAM,iCAAiC,CAAC;AAE7D,OAAO,KAAK,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,KAAK,UAAU,EAAY,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,iBAAiB,CAAC;AAG/D,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI;IAChE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1C,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;CACvD,GAAG,qBAAqB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAEjC,MAAM,WAAW,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAE,SAAQ,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC;IACpF,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,YAAY,EAAE,EAAE,CAAC,GAAG,SAAS,CAAC;CAC/D;AAQD,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,QAAQ,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,KAAG,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CA0BtI,CAAC"}
package/dist/editor.js ADDED
@@ -0,0 +1,34 @@
1
+ import { Option } from "effect";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { useQuery } from "./result-state.js";
4
+ import { useSaveState } from "./save-state.js";
5
+ import { submission } from "./submission.js";
6
+ export const useEditor = (config) => {
7
+ const query = useQuery(config.query);
8
+ const row = Option.getOrUndefined(query.data);
9
+ const [held, hold] = useState(undefined);
10
+ let current = held?.query === config.query ? held : undefined;
11
+ if (current === undefined && row !== undefined) {
12
+ current = {
13
+ query: config.query,
14
+ values: config.values,
15
+ form: submission({
16
+ fields: config.fields,
17
+ initialValues: config.values(row),
18
+ runtime: config.runtime,
19
+ save: config.save,
20
+ rejectField: config.rejectField,
21
+ }).form,
22
+ };
23
+ hold(current);
24
+ }
25
+ const form = current?.form;
26
+ const values = current?.values;
27
+ useEffect(() => {
28
+ if (form && values && row !== undefined)
29
+ form.receive(values(row));
30
+ }, [form, values, row]);
31
+ const settled = useCallback((saved) => (form && values ? form.receive(values(saved)) : undefined), [form, values]);
32
+ return { ...useSaveState(form, settled), query, form };
33
+ };
34
+ //# sourceMappingURL=editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.js","sourceRoot":"","sources":["../src/editor.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,MAAM,EAAe,MAAM,QAAQ,CAAC;AAG1D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEzD,OAAO,EAAmB,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAkB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAqB7C,MAAM,CAAC,MAAM,SAAS,GAAG,CAAuC,MAA2C,EAA8B,EAAE;IAC1I,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAmC,SAAS,CAAC,CAAC;IAC3E,IAAI,OAAO,GAAG,IAAI,EAAE,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9D,IAAI,OAAO,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QAChD,OAAO,GAAG;YACT,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,UAAU,CAAC;gBAChB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC;gBACjC,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,WAAW,EAAE,MAAM,CAAC,WAAW;aAC/B,CAAC,CAAC,IAAI;SACP,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,CAAC;IACf,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,CAAC;IAC3B,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,IAAI,IAAI,MAAM,IAAI,GAAG,KAAK,SAAS;YAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACpE,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IACxB,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,KAAU,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACxH,OAAO,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACxD,CAAC,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { FieldFailure, Fields, Name, Submitter } from "@shivaedev/effect-form";
2
+ import { Effect, type Schema, type Scope } from "effect";
3
+ import type { AtomRegistry } from "effect/unstable/reactivity/AtomRegistry";
4
+ import type { Reactivity } from "effect/unstable/reactivity/Reactivity";
5
+ export interface FieldRejection<N extends string> {
6
+ readonly field: N;
7
+ readonly message: string;
8
+ }
9
+ export type AtomServices<R> = R | Scope.Scope | AtomRegistry | Reactivity;
10
+ export type RejectField<F extends Fields, E> = (error: E) => FieldRejection<Name<F>> | undefined;
11
+ type Unmatched<F extends Fields, E> = E extends {
12
+ readonly _tag: string;
13
+ readonly message: string;
14
+ } ? "field" extends keyof E ? Exclude<E["field"], Name<F> | undefined> : never : never;
15
+ export type FieldRejectionMapping<F extends Fields, E> = {
16
+ readonly rejectField?: RejectField<F, E>;
17
+ } & ([Unmatched<F, E>] extends [never] ? unknown : {
18
+ readonly rejectField: RejectField<F, E>;
19
+ });
20
+ export declare const fieldRejectionOf: <F extends Fields>(schema: Schema.Struct<F>) => (error: unknown) => FieldRejection<Name<F>> | undefined;
21
+ export declare const rejecting: <F extends Fields, V, A, E, R>(schema: Schema.Struct<F>, save: (values: V) => Effect.Effect<A, E, R>, reject: RejectField<F, E> | undefined) => (values: V, submitter: Submitter<Name<F>>) => Effect.Effect<A, E | FieldFailure, R>;
22
+ export {};
23
+ //# sourceMappingURL=field-rejection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-rejection.d.ts","sourceRoot":"","sources":["../src/field-rejection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACpF,OAAO,EAAE,MAAM,EAAa,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AAExE,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,MAAM;IAC/C,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,YAAY,GAAG,UAAU,CAAC;AAE1E,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;AAEjG,KAAK,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,IAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChG,OAAO,SAAS,MAAM,CAAC,GACtB,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,GACxC,KAAK,GACN,KAAK,CAAC;AAET,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,IAAI;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;CAAE,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACvI,OAAO,GACP;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;CAAE,CAAC,CAAC;AAEhD,eAAO,MAAM,gBAAgB,GAC3B,CAAC,SAAS,MAAM,EAAE,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAC1C,OAAO,OAAO,KAAG,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,SAK3C,CAAC;AAEH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EACrD,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EACxB,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC3C,QAAQ,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,MAG7B,QAAQ,CAAC,EAAE,WAAW,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,CAAC,CAKvF,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { Effect, Predicate } from "effect";
2
+ export const fieldRejectionOf = (schema) => (error) => {
3
+ const isName = (name) => typeof name === "string" && Object.hasOwn(schema.fields, name);
4
+ if (!Predicate.hasProperty(error, "_tag") || !Predicate.hasProperty(error, "field") || !Predicate.hasProperty(error, "message"))
5
+ return undefined;
6
+ const { _tag, field, message } = error;
7
+ return typeof _tag === "string" && isName(field) && typeof message === "string" ? { field, message } : undefined;
8
+ };
9
+ export const rejecting = (schema, save, reject) => {
10
+ const rejection = reject ?? fieldRejectionOf(schema);
11
+ return (values, submitter) => Effect.catch(save(values), (error) => {
12
+ const found = rejection(error);
13
+ return found === undefined ? Effect.fail(error) : submitter.fail(found.field, found.message);
14
+ });
15
+ };
16
+ //# sourceMappingURL=field-rejection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-rejection.js","sourceRoot":"","sources":["../src/field-rejection.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,SAAS,EAA2B,MAAM,QAAQ,CAAC;AAuBpE,MAAM,CAAC,MAAM,gBAAgB,GAC5B,CAAmB,MAAwB,EAAE,EAAE,CAC/C,CAAC,KAAc,EAAuC,EAAE;IACvD,MAAM,MAAM,GAAG,CAAC,IAAa,EAAmB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAClH,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAClJ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;IACvC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAClH,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,CACxB,MAAwB,EACxB,IAA2C,EAC3C,MAAqC,EACpC,EAAE;IACH,MAAM,SAAS,GAAG,MAAM,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACrD,OAAO,CAAC,MAAS,EAAE,SAA6B,EAAyC,EAAE,CAC1F,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAA0C,EAAE;QAC5E,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QAC/B,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC"}
package/dist/form.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type { Create, CreateConfig } from "./create.ts";
2
+ export { useCreate } from "./create.ts";
3
+ export type { Editor, EditorConfig } from "./editor.ts";
4
+ export { useEditor } from "./editor.ts";
5
+ export type { FieldRejection, FieldRejectionMapping, RejectField } from "./field-rejection.ts";
6
+ export type { SaveState } from "./save-state.ts";
7
+ //# sourceMappingURL=form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC/F,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/form.js ADDED
@@ -0,0 +1,3 @@
1
+ export { useCreate } from "./create.js";
2
+ export { useEditor } from "./editor.js";
3
+ //# sourceMappingURL=form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.js","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,7 @@
1
+ export type { ActionState, QueryState, ResultState } from "./result-state.ts";
2
+ export { isUnauthorized, useAction, useQuery } from "./result-state.ts";
3
+ export type { ResumeOptions, ResumeSource, ResumeWindow } from "./resume-signal.ts";
4
+ export { resumeSignal } from "./resume-signal.ts";
5
+ export type { SessionBoundaryProps } from "./session-boundary.ts";
6
+ export { SessionBoundary, useSessionRecheck } from "./session-boundary.ts";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { isUnauthorized, useAction, useQuery } from "./result-state.js";
2
+ export { resumeSignal } from "./resume-signal.js";
3
+ export { SessionBoundary, useSessionRecheck } from "./session-boundary.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAExE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { Cause, Option } from "effect";
2
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
3
+ import type * as Atom from "effect/unstable/reactivity/Atom";
4
+ export interface ResultState<A, E> {
5
+ readonly result: AsyncResult.AsyncResult<A, E>;
6
+ readonly data: Option.Option<A>;
7
+ readonly cause: Option.Option<Cause.Cause<E>>;
8
+ readonly pending: boolean;
9
+ readonly refreshing: boolean;
10
+ }
11
+ export declare const isUnauthorized: <E>(cause: Cause.Cause<E>) => boolean;
12
+ export interface QueryState<A, E> extends ResultState<A, E> {
13
+ readonly refresh: () => void;
14
+ }
15
+ export declare const useQuery: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => QueryState<A, E>;
16
+ export interface ActionState<Input, A, E> extends ResultState<A, E> {
17
+ readonly dispatch: (input: Input) => void;
18
+ }
19
+ /** Dispatches an action and reads its shared native state without claiming a per-invocation completion result. */
20
+ export declare const useAction: <Input, A, E>(atom: Atom.AtomResultFn<Input, A, E>) => ActionState<Input, A, E>;
21
+ //# sourceMappingURL=result-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result-state.d.ts","sourceRoot":"","sources":["../src/result-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAa,MAAM,QAAQ,CAAC;AAClD,OAAO,KAAK,WAAW,MAAM,wCAAwC,CAAC;AACtE,OAAO,KAAK,KAAK,IAAI,MAAM,iCAAiC,CAAC;AAI7D,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC7B;AAaD,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAG,OACwE,CAAC;AASnI,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,CAAC,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAG,UAAU,CAAC,CAAC,EAAE,CAAC,CAI9F,CAAC;AAEF,MAAM,WAAW,WAAW,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC;IAClE,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC1C;AAED,kHAAkH;AAClH,eAAO,MAAM,SAAS,GAAI,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,KAAG,WAAW,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAIpG,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react";
2
+ import { Cause, Option, Predicate } from "effect";
3
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
+ import { useEffect } from "react";
5
+ import { useSessionRecheck } from "./session-boundary.js";
6
+ const state = (result) => {
7
+ const data = AsyncResult.value(result);
8
+ return {
9
+ result,
10
+ data,
11
+ cause: AsyncResult.cause(result),
12
+ pending: result.waiting,
13
+ refreshing: result.waiting && Option.isSome(data),
14
+ };
15
+ };
16
+ export const isUnauthorized = (cause) => Option.match(Cause.findErrorOption(cause), { onNone: () => false, onSome: (error) => Predicate.isTagged(error, "Unauthorized") });
17
+ const useRecheckOnUnauthorized = (result) => {
18
+ const recheck = useSessionRecheck();
19
+ useEffect(() => {
20
+ if (AsyncResult.isFailure(result) && isUnauthorized(result.cause))
21
+ recheck();
22
+ }, [recheck, result]);
23
+ };
24
+ export const useQuery = (atom) => {
25
+ const result = useAtomValue(atom);
26
+ useRecheckOnUnauthorized(result);
27
+ return { ...state(result), refresh: useAtomRefresh(atom) };
28
+ };
29
+ /** Dispatches an action and reads its shared native state without claiming a per-invocation completion result. */
30
+ export const useAction = (atom) => {
31
+ const result = useAtomValue(atom);
32
+ useRecheckOnUnauthorized(result);
33
+ return { ...state(result), dispatch: useAtomSet(atom) };
34
+ };
35
+ //# sourceMappingURL=result-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result-state.js","sourceRoot":"","sources":["../src/result-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,KAAK,WAAW,MAAM,wCAAwC,CAAC;AAEtE,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAU1D,MAAM,KAAK,GAAG,CAAO,MAAqC,EAAqB,EAAE;IAChF,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO;QACN,MAAM;QACN,IAAI;QACJ,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC;QAChC,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;KACjD,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAI,KAAqB,EAAW,EAAE,CACnE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC;AAEnI,MAAM,wBAAwB,GAAG,CAAO,MAAqC,EAAQ,EAAE;IACtF,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;IACpC,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;IAC9E,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;AACvB,CAAC,CAAC;AAMF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAO,IAA8C,EAAoB,EAAE;IAClG,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,OAAO,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;AAC5D,CAAC,CAAC;AAMF,kHAAkH;AAClH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAc,IAAoC,EAA4B,EAAE;IACxG,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,OAAO,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;AACzD,CAAC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import * as Atom from "effect/unstable/reactivity/Atom";
2
+ export type ResumeSource = (resume: () => void) => () => void;
3
+ type ResumeEvent = "online" | "visibilitychange";
4
+ export interface ResumeWindow {
5
+ readonly addEventListener: (type: ResumeEvent, listener: () => void) => void;
6
+ readonly removeEventListener: (type: ResumeEvent, listener: () => void) => void;
7
+ readonly document: {
8
+ readonly visibilityState: string;
9
+ };
10
+ }
11
+ export interface ResumeOptions {
12
+ readonly window?: ResumeWindow;
13
+ readonly native?: ResumeSource;
14
+ }
15
+ export declare const resumeSignal: ({ window, native }: ResumeOptions) => Atom.Atom<number>;
16
+ export {};
17
+ //# sourceMappingURL=resume-signal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resume-signal.d.ts","sourceRoot":"","sources":["../src/resume-signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,iCAAiC,CAAC;AAExD,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;AAE9D,KAAK,WAAW,GAAG,QAAQ,GAAG,kBAAkB,CAAC;AAEjD,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAC7E,QAAQ,CAAC,mBAAmB,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAChF,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAC/B;AAYD,eAAO,MAAM,YAAY,GAAI,oBAAoB,aAAa,KAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAShF,CAAC"}
@@ -0,0 +1,21 @@
1
+ import * as Atom from "effect/unstable/reactivity/Atom";
2
+ const whileVisible = (target, type) => (resume) => {
3
+ const visible = () => {
4
+ if (target.document.visibilityState === "visible")
5
+ resume();
6
+ };
7
+ target.addEventListener(type, visible);
8
+ return () => target.removeEventListener(type, visible);
9
+ };
10
+ export const resumeSignal = ({ window, native }) => {
11
+ const browser = window === undefined ? [] : [whileVisible(window, "visibilitychange"), whileVisible(window, "online")];
12
+ const sources = native === undefined ? browser : [...browser, native];
13
+ return Atom.readable((get) => {
14
+ let count = 0;
15
+ const resume = () => get.setSelf(++count);
16
+ for (const source of sources)
17
+ get.addFinalizer(source(resume));
18
+ return count;
19
+ });
20
+ };
21
+ //# sourceMappingURL=resume-signal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resume-signal.js","sourceRoot":"","sources":["../src/resume-signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,iCAAiC,CAAC;AAiBxD,MAAM,YAAY,GACjB,CAAC,MAAoB,EAAE,IAAiB,EAAgB,EAAE,CAC1D,CAAC,MAAM,EAAE,EAAE;IACV,MAAM,OAAO,GAAG,GAAG,EAAE;QACpB,IAAI,MAAM,CAAC,QAAQ,CAAC,eAAe,KAAK,SAAS;YAAE,MAAM,EAAE,CAAC;IAC7D,CAAC,CAAC;IACF,MAAM,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACvC,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AACxD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,EAAiB,EAAqB,EAAE;IACpF,MAAM,OAAO,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,kBAAkB,CAAC,EAAE,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IACvH,MAAM,OAAO,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,EAAE;QAC5B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;QAC1C,KAAK,MAAM,MAAM,IAAI,OAAO;YAAE,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/D,OAAO,KAAK,CAAC;IACd,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { FieldFailure, type Fields, type Form } from "@shivaedev/effect-form";
2
+ import { Option } from "effect";
3
+ export interface SaveState<E> {
4
+ readonly dirty: boolean;
5
+ readonly failure: Option.Option<E>;
6
+ readonly saving: boolean;
7
+ readonly save: () => void;
8
+ readonly revert: () => void;
9
+ }
10
+ export declare const useSaveState: <F extends Fields, A, E, ER>(form: Form<F, A, E | FieldFailure, ER> | undefined, settled: (result: A) => void) => SaveState<E | ER>;
11
+ //# sourceMappingURL=save-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-state.d.ts","sourceRoot":"","sources":["../src/save-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,KAAK,IAAI,EAAW,MAAM,wBAAwB,CAAC;AACvF,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAUhC,MAAM,WAAW,SAAS,CAAC,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EACtD,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,EAAE,CAAC,GAAG,SAAS,EAClD,SAAS,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,KAC1B,SAAS,CAAC,CAAC,GAAG,EAAE,CA0BlB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { RegistryContext, useAtomValue } from "@effect/atom-react";
2
+ import { FieldFailure, Invalid } from "@shivaedev/effect-form";
3
+ import { Option } from "effect";
4
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5
+ import * as Atom from "effect/unstable/reactivity/Atom";
6
+ import { useCallback, useContext, useEffect } from "react";
7
+ import { isUnauthorized } from "./result-state.js";
8
+ import { useSessionRecheck } from "./session-boundary.js";
9
+ const idle = Atom.make(AsyncResult.initial());
10
+ const clean = Atom.make(false);
11
+ export const useSaveState = (form, settled) => {
12
+ const registry = useContext(RegistryContext);
13
+ const recheck = useSessionRecheck();
14
+ const result = useAtomValue(form?.submit ?? idle);
15
+ const dirty = useAtomValue(form?.dirty ?? clean);
16
+ useEffect(() => {
17
+ if (AsyncResult.isFailure(result) && isUnauthorized(result.cause))
18
+ recheck();
19
+ }, [recheck, result]);
20
+ useEffect(() => form &&
21
+ registry.subscribe(form.submit, (next) => {
22
+ if (AsyncResult.isSuccess(next) && !next.waiting)
23
+ settled(next.value);
24
+ }), [form, registry, settled]);
25
+ const save = useCallback(() => {
26
+ if (form === undefined || registry.get(form.submit).waiting)
27
+ return;
28
+ registry.set(form.submit, undefined);
29
+ }, [form, registry]);
30
+ const revert = useCallback(() => form?.revert(), [form]);
31
+ const failure = Option.filter(AsyncResult.error(result), (error) => !(error instanceof FieldFailure) && !(error instanceof Invalid));
32
+ return { dirty, failure, saving: result.waiting, save, revert };
33
+ };
34
+ //# sourceMappingURL=save-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-state.js","sourceRoot":"","sources":["../src/save-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,YAAY,EAA0B,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACvF,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,WAAW,MAAM,wCAAwC,CAAC;AACtE,OAAO,KAAK,IAAI,MAAM,iCAAiC,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;AAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAU/B,MAAM,CAAC,MAAM,YAAY,GAAG,CAC3B,IAAkD,EAClD,OAA4B,EACR,EAAE;IACtB,MAAM,QAAQ,GAAG,UAAU,CAAC,eAAe,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;IACpC,MAAM,MAAM,GAAgE,YAAY,CAAC,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,CAAC;IAC/G,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,KAAK,IAAI,KAAK,CAAC,CAAC;IACjD,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;IAC9E,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IACtB,SAAS,CACR,GAAG,EAAE,CACJ,IAAI;QACJ,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACxC,IAAI,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvE,CAAC,CAAC,EACH,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CACzB,CAAC;IACF,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,IAAI,IAAI,KAAK,SAAS,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO;YAAE,OAAO;QACpE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACtC,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IACrB,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACzD,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAC5B,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,EACzB,CAAC,KAAK,EAAmB,EAAE,CAAC,CAAC,CAAC,KAAK,YAAY,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,YAAY,OAAO,CAAC,CAC3F,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACjE,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ export interface SessionBoundaryProps<S, Client> {
3
+ readonly session: S | undefined;
4
+ readonly identify: (session: S) => string;
5
+ readonly connect: (session: S) => Client;
6
+ readonly recheck: () => void;
7
+ readonly signedOut?: ReactNode;
8
+ readonly children: (client: Client) => ReactNode;
9
+ }
10
+ export declare const useSessionRecheck: () => (() => void);
11
+ export declare const SessionBoundary: <S, Client>({ session, identify, connect, recheck, signedOut, children, }: SessionBoundaryProps<S, Client>) => ReactNode;
12
+ //# sourceMappingURL=session-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-boundary.d.ts","sourceRoot":"","sources":["../src/session-boundary.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0C,KAAK,SAAS,EAAmC,MAAM,OAAO,CAAC;AAEhH,MAAM,WAAW,oBAAoB,CAAC,CAAC,EAAE,MAAM;IAC9C,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,CAAC;CACjD;AAID,eAAO,MAAM,iBAAiB,QAAO,CAAC,MAAM,IAAI,CAAwB,CAAC;AAoDzE,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,MAAM,EAAE,+DAOxC,oBAAoB,CAAC,CAAC,EAAE,MAAM,CAAC,KAAG,SAGoE,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { RegistryContext } from "@effect/atom-react";
2
+ import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry";
3
+ import { createContext, createElement, Fragment, useContext, useEffect, useState } from "react";
4
+ const Recheck = createContext(() => { });
5
+ export const useSessionRecheck = () => useContext(Recheck);
6
+ const owning = (client) => ({ client, registry: AtomRegistry.make(), mounted: 0 });
7
+ // No cleanup runs when a hidden <Activity> subtree is unmounted, so nodes a hidden render creates must not outlive it.
8
+ const hidden = (client) => {
9
+ const owned = owning(client);
10
+ let reaping = false;
11
+ owned.registry.onNodeAdded = () => {
12
+ if (reaping || owned.mounted > 0)
13
+ return;
14
+ reaping = true;
15
+ setTimeout(() => {
16
+ reaping = false;
17
+ if (owned.mounted === 0)
18
+ owned.registry.reset();
19
+ }, 0);
20
+ };
21
+ return owned;
22
+ };
23
+ const Generation = ({ session, connect, recheck, children }) => {
24
+ const [owned, own] = useState(() => owning(connect(session)));
25
+ useEffect(() => {
26
+ owned.mounted += 1;
27
+ return () => {
28
+ owned.mounted -= 1;
29
+ queueMicrotask(() => {
30
+ if (owned.mounted > 0)
31
+ return;
32
+ owned.registry.dispose();
33
+ own((current) => (current === owned ? hidden(owned.client) : current));
34
+ });
35
+ };
36
+ }, [owned]);
37
+ return createElement(Recheck.Provider, { value: recheck }, createElement(RegistryContext.Provider, { value: owned.registry }, children(owned.client)));
38
+ };
39
+ export const SessionBoundary = ({ session, identify, connect, recheck, signedOut, children, }) => session === undefined
40
+ ? createElement(Fragment, null, signedOut)
41
+ : createElement((Generation), { key: identify(session), session, connect, recheck, children });
42
+ //# sourceMappingURL=session-boundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-boundary.js","sourceRoot":"","sources":["../src/session-boundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,YAAY,MAAM,yCAAyC,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAkB,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAWhH,MAAM,OAAO,GAAG,aAAa,CAAa,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AAEpD,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAiB,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AAQzE,MAAM,MAAM,GAAG,CAAS,MAAc,EAAiB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;AAElH,uHAAuH;AACvH,MAAM,MAAM,GAAG,CAAS,MAAc,EAAiB,EAAE;IACxD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7B,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,KAAK,CAAC,QAAQ,CAAC,WAAW,GAAG,GAAG,EAAE;QACjC,IAAI,OAAO,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC;YAAE,OAAO;QACzC,OAAO,GAAG,IAAI,CAAC;QACf,UAAU,CAAC,GAAG,EAAE;YACf,OAAO,GAAG,KAAK,CAAC;YAChB,IAAI,KAAK,CAAC,OAAO,KAAK,CAAC;gBAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACjD,CAAC,EAAE,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;IACF,OAAO,KAAK,CAAC;AACd,CAAC,CAAC;AASF,MAAM,UAAU,GAAG,CAAY,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAA8B,EAAa,EAAE;IAChH,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9D,SAAS,CAAC,GAAG,EAAE;QACd,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;QACnB,OAAO,GAAG,EAAE;YACX,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;YACnB,cAAc,CAAC,GAAG,EAAE;gBACnB,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC;oBAAE,OAAO;gBAC9B,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;gBACzB,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;YACxE,CAAC,CAAC,CAAC;QACJ,CAAC,CAAC;IACH,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IACZ,OAAO,aAAa,CACnB,OAAO,CAAC,QAAQ,EAChB,EAAE,KAAK,EAAE,OAAO,EAAE,EAClB,aAAa,CAAC,eAAe,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAC1F,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAY,EAC1C,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,GACyB,EAAa,EAAE,CAChD,OAAO,KAAK,SAAS;IACpB,CAAC,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,CAAC;IAC1C,CAAC,CAAC,aAAa,CAAC,CAAA,UAAqB,CAAA,EAAE,EAAE,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { type Decoded, type Encoded, type FieldFailure, type Fields, type Form, type Services } from "@shivaedev/effect-form";
2
+ import type { Effect, Schema } from "effect";
3
+ import type * as Atom from "effect/unstable/reactivity/Atom";
4
+ import { type AtomServices, type RejectField } from "./field-rejection.ts";
5
+ export interface SubmissionConfig<F extends Fields, A, E, R, ER> {
6
+ readonly fields: Schema.Struct<F>;
7
+ readonly initialValues: Encoded<F>;
8
+ readonly save: (values: Decoded<F>) => Effect.Effect<A, E, AtomServices<R>>;
9
+ readonly runtime: Atom.AtomRuntime<Services<F, R>, ER>;
10
+ readonly rejectField: RejectField<F, E> | undefined;
11
+ }
12
+ export interface Submission<F extends Fields, A, E, ER> {
13
+ readonly form: Form<F, A, E | FieldFailure, ER>;
14
+ readonly submitted: () => Encoded<F> | undefined;
15
+ }
16
+ export declare const submission: <F extends Fields, A, E, R, ER>(config: SubmissionConfig<F, A, E, R, ER>) => Submission<F, A, E, ER>;
17
+ //# sourceMappingURL=submission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission.d.ts","sourceRoot":"","sources":["../src/submission.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,MAAM,EAAE,KAAK,IAAI,EAAQ,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACpI,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,KAAK,IAAI,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAa,MAAM,sBAAsB,CAAC;AAEtF,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;IAC9D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvD,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC;CACpD;AAED,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;IACrD,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,EAAE,CAAC,CAAC;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;CACjD;AAED,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,QAAQ,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAG,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAa1H,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { make } from "@shivaedev/effect-form";
2
+ import { rejecting } from "./field-rejection.js";
3
+ export const submission = (config) => {
4
+ const { fields, initialValues, runtime } = config;
5
+ const save = rejecting(fields, config.save, config.rejectField);
6
+ let submitted;
7
+ const form = make(fields, {
8
+ initialValues,
9
+ runtime,
10
+ onSubmit: (values, submitter, snapshot) => {
11
+ submitted = snapshot;
12
+ return save(values, submitter);
13
+ },
14
+ });
15
+ return { form, submitted: () => submitted };
16
+ };
17
+ //# sourceMappingURL=submission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission.js","sourceRoot":"","sources":["../src/submission.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyE,IAAI,EAAiB,MAAM,wBAAwB,CAAC;AAGpI,OAAO,EAAuC,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAetF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAgC,MAAwC,EAA2B,EAAE;IAC9H,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IAClD,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAChE,IAAI,SAAiC,CAAC;IACtC,MAAM,IAAI,GAAqC,IAAI,CAAC,MAAM,EAAE;QAC3D,aAAa;QACb,OAAO;QACP,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;YACzC,SAAS,GAAG,QAAQ,CAAC;YACrB,OAAO,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAChC,CAAC;KACD,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AAC7C,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,14 +1,83 @@
1
1
  {
2
2
  "name": "@shivaedev/effect-react",
3
- "version": "0.0.0",
4
- "description": "Name reservation. The first release is published from https://github.com/ShivaeDev/platform.",
3
+ "version": "0.1.0",
4
+ "description": "React query, action, editor and session hooks for native Effect atoms",
5
+ "type": "module",
5
6
  "license": "MIT",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/ShivaeDev/platform.git",
9
10
  "directory": "packages/effect-react"
10
11
  },
12
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-react#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/ShivaeDev/platform/issues"
15
+ },
16
+ "engines": {
17
+ "node": ">=24"
18
+ },
19
+ "sideEffects": false,
20
+ "files": [
21
+ "dist",
22
+ "src",
23
+ "CHANGELOG.md",
24
+ "README.md"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "source": "./src/index.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./form": {
34
+ "types": "./dist/form.d.ts",
35
+ "source": "./src/form.ts",
36
+ "import": "./dist/form.js",
37
+ "default": "./dist/form.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
11
41
  "publishConfig": {
12
- "access": "public"
42
+ "access": "public",
43
+ "provenance": true
44
+ },
45
+ "peerDependencies": {
46
+ "@effect/atom-react": "4.0.0-rc.112",
47
+ "@shivaedev/effect-form": "^0.1.0",
48
+ "effect": "4.0.0-rc.112",
49
+ "react": "19.2.8"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "@shivaedev/effect-form": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@effect/atom-react": "4.0.0-rc.112",
58
+ "@types/node": "24.10.1",
59
+ "@types/react": "19.2.18",
60
+ "@types/react-dom": "19.2.4",
61
+ "@typescript/native": "npm:typescript@7.0.2",
62
+ "effect": "4.0.0-rc.112",
63
+ "happy-dom": "20.11.2",
64
+ "react": "19.2.8",
65
+ "react-dom": "19.2.8",
66
+ "typescript": "npm:@typescript/typescript6@6.0.2",
67
+ "vitest": "4.1.9",
68
+ "@shivaedev/effect-sql": "0.1.0",
69
+ "@effect/sql-sqlite-node": "4.0.0-rc.112",
70
+ "@shivaedev/effect-service": "0.1.0",
71
+ "@shivaedev/effect-form": "0.1.0",
72
+ "@shivaedev/effect-contract": "0.1.0"
73
+ },
74
+ "scripts": {
75
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
76
+ "check": "biome check .",
77
+ "test": "vitest run",
78
+ "test:package": "node scripts/test-package.mjs",
79
+ "typecheck": "tsc --noEmit",
80
+ "typecheck:compat": "tsc6 --noEmit",
81
+ "ready": "pnpm check && pnpm typecheck && pnpm typecheck:compat && pnpm test && pnpm build && pnpm test:package"
13
82
  }
14
- }
83
+ }
package/src/create.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { Decoded, Encoded, FieldFailure, Fields, Form, Services } from "@shivaedev/effect-form";
2
+ import { type Effect, Equal, Option, type Schema } from "effect";
3
+ import type * as Atom from "effect/unstable/reactivity/Atom";
4
+ import { useCallback, useState } from "react";
5
+ import type { AtomServices, FieldRejectionMapping } from "./field-rejection.ts";
6
+ import { type SaveState, useSaveState } from "./save-state.ts";
7
+ import { submission } from "./submission.ts";
8
+
9
+ export type CreateConfig<F extends Fields, Created, SE, R, ER> = {
10
+ readonly fields: Schema.Struct<F>;
11
+ readonly initialValues: Encoded<F>;
12
+ readonly create: (values: Decoded<F>) => Effect.Effect<Created, SE, AtomServices<R>>;
13
+ readonly runtime: Atom.AtomRuntime<Services<F, R>, ER>;
14
+ } & FieldRejectionMapping<F, SE>;
15
+
16
+ export interface Create<F extends Fields, Created, SE, ER> extends SaveState<SE | ER> {
17
+ readonly form: Form<F, Created, SE | FieldFailure, ER>;
18
+ readonly created: Option.Option<Created>;
19
+ }
20
+
21
+ const editedSince = <V extends object>(current: V, submitted: V | undefined): Readonly<Record<string, unknown>> => {
22
+ const before = new Map(Object.entries(submitted ?? current));
23
+ return Object.fromEntries(Object.entries(current).filter(([name, value]) => !Equal.equals(value, before.get(name))));
24
+ };
25
+
26
+ export const useCreate = <F extends Fields, Created, SE, R, ER>(config: CreateConfig<F, Created, SE, R, ER>): Create<F, Created, SE, ER> => {
27
+ const [{ initialValues, fresh }] = useState(() => {
28
+ const { fields, initialValues, runtime, create, rejectField } = config;
29
+ return { initialValues, fresh: () => submission({ fields, initialValues, runtime, save: create, rejectField }) };
30
+ });
31
+ const [current, replace] = useState(fresh);
32
+ const [created, record] = useState<Option.Option<Created>>(Option.none());
33
+ const settled = useCallback(
34
+ (value: Created) => {
35
+ const next = fresh();
36
+ next.form.values.set({ ...initialValues, ...editedSince(current.form.values.value, current.submitted()) });
37
+ record(Option.some(value));
38
+ replace(next);
39
+ },
40
+ [current, fresh, initialValues],
41
+ );
42
+ return { ...useSaveState(current.form, settled), form: current.form, created };
43
+ };
package/src/editor.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { Decoded, Encoded, FieldFailure, Fields, Form, Services } from "@shivaedev/effect-form";
2
+ import { type Effect, Option, type Schema } from "effect";
3
+ import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
+ import type * as Atom from "effect/unstable/reactivity/Atom";
5
+ import { useCallback, useEffect, useState } from "react";
6
+ import type { AtomServices, FieldRejectionMapping } from "./field-rejection.ts";
7
+ import { type QueryState, useQuery } from "./result-state.ts";
8
+ import { type SaveState, useSaveState } from "./save-state.ts";
9
+ import { submission } from "./submission.ts";
10
+
11
+ export type EditorConfig<F extends Fields, Row, QE, SE, R, ER> = {
12
+ readonly query: Atom.Atom<AsyncResult.AsyncResult<Row, QE>>;
13
+ readonly fields: Schema.Struct<F>;
14
+ readonly values: (row: Row) => Encoded<F>;
15
+ readonly save: (values: Decoded<F>) => Effect.Effect<Row, SE, AtomServices<R>>;
16
+ readonly runtime: Atom.AtomRuntime<Services<F, R>, ER>;
17
+ } & FieldRejectionMapping<F, SE>;
18
+
19
+ export interface Editor<F extends Fields, Row, QE, SE, ER> extends SaveState<SE | ER> {
20
+ readonly query: QueryState<Row, QE>;
21
+ readonly form: Form<F, Row, SE | FieldFailure, ER> | undefined;
22
+ }
23
+
24
+ interface Held<F extends Fields, Row, SE, ER> {
25
+ readonly query: Atom.Atom<unknown>;
26
+ readonly form: Form<F, Row, SE | FieldFailure, ER>;
27
+ readonly values: (row: Row) => Encoded<F>;
28
+ }
29
+
30
+ export const useEditor = <F extends Fields, Row, QE, SE, R, ER>(config: EditorConfig<F, Row, QE, SE, R, ER>): Editor<F, Row, QE, SE, ER> => {
31
+ const query = useQuery(config.query);
32
+ const row = Option.getOrUndefined(query.data);
33
+ const [held, hold] = useState<Held<F, Row, SE, ER> | undefined>(undefined);
34
+ let current = held?.query === config.query ? held : undefined;
35
+ if (current === undefined && row !== undefined) {
36
+ current = {
37
+ query: config.query,
38
+ values: config.values,
39
+ form: submission({
40
+ fields: config.fields,
41
+ initialValues: config.values(row),
42
+ runtime: config.runtime,
43
+ save: config.save,
44
+ rejectField: config.rejectField,
45
+ }).form,
46
+ };
47
+ hold(current);
48
+ }
49
+ const form = current?.form;
50
+ const values = current?.values;
51
+ useEffect(() => {
52
+ if (form && values && row !== undefined) form.receive(values(row));
53
+ }, [form, values, row]);
54
+ const settled = useCallback((saved: Row) => (form && values ? form.receive(values(saved)) : undefined), [form, values]);
55
+ return { ...useSaveState(form, settled), query, form };
56
+ };
@@ -0,0 +1,45 @@
1
+ import type { FieldFailure, Fields, Name, Submitter } from "@shivaedev/effect-form";
2
+ import { Effect, Predicate, type Schema, type Scope } from "effect";
3
+ import type { AtomRegistry } from "effect/unstable/reactivity/AtomRegistry";
4
+ import type { Reactivity } from "effect/unstable/reactivity/Reactivity";
5
+
6
+ export interface FieldRejection<N extends string> {
7
+ readonly field: N;
8
+ readonly message: string;
9
+ }
10
+
11
+ export type AtomServices<R> = R | Scope.Scope | AtomRegistry | Reactivity;
12
+
13
+ export type RejectField<F extends Fields, E> = (error: E) => FieldRejection<Name<F>> | undefined;
14
+
15
+ type Unmatched<F extends Fields, E> = E extends { readonly _tag: string; readonly message: string }
16
+ ? "field" extends keyof E
17
+ ? Exclude<E["field"], Name<F> | undefined>
18
+ : never
19
+ : never;
20
+
21
+ export type FieldRejectionMapping<F extends Fields, E> = { readonly rejectField?: RejectField<F, E> } & ([Unmatched<F, E>] extends [never]
22
+ ? unknown
23
+ : { readonly rejectField: RejectField<F, E> });
24
+
25
+ export const fieldRejectionOf =
26
+ <F extends Fields>(schema: Schema.Struct<F>) =>
27
+ (error: unknown): FieldRejection<Name<F>> | undefined => {
28
+ const isName = (name: unknown): name is Name<F> => typeof name === "string" && Object.hasOwn(schema.fields, name);
29
+ if (!Predicate.hasProperty(error, "_tag") || !Predicate.hasProperty(error, "field") || !Predicate.hasProperty(error, "message")) return undefined;
30
+ const { _tag, field, message } = error;
31
+ return typeof _tag === "string" && isName(field) && typeof message === "string" ? { field, message } : undefined;
32
+ };
33
+
34
+ export const rejecting = <F extends Fields, V, A, E, R>(
35
+ schema: Schema.Struct<F>,
36
+ save: (values: V) => Effect.Effect<A, E, R>,
37
+ reject: RejectField<F, E> | undefined,
38
+ ) => {
39
+ const rejection = reject ?? fieldRejectionOf(schema);
40
+ return (values: V, submitter: Submitter<Name<F>>): Effect.Effect<A, E | FieldFailure, R> =>
41
+ Effect.catch(save(values), (error): Effect.Effect<never, E | FieldFailure> => {
42
+ const found = rejection(error);
43
+ return found === undefined ? Effect.fail(error) : submitter.fail(found.field, found.message);
44
+ });
45
+ };
package/src/form.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type { Create, CreateConfig } from "./create.ts";
2
+ export { useCreate } from "./create.ts";
3
+ export type { Editor, EditorConfig } from "./editor.ts";
4
+ export { useEditor } from "./editor.ts";
5
+ export type { FieldRejection, FieldRejectionMapping, RejectField } from "./field-rejection.ts";
6
+ export type { SaveState } from "./save-state.ts";
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type { ActionState, QueryState, ResultState } from "./result-state.ts";
2
+ export { isUnauthorized, useAction, useQuery } from "./result-state.ts";
3
+ export type { ResumeOptions, ResumeSource, ResumeWindow } from "./resume-signal.ts";
4
+ export { resumeSignal } from "./resume-signal.ts";
5
+ export type { SessionBoundaryProps } from "./session-boundary.ts";
6
+ export { SessionBoundary, useSessionRecheck } from "./session-boundary.ts";
@@ -0,0 +1,56 @@
1
+ import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react";
2
+ import { Cause, Option, Predicate } from "effect";
3
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
+ import type * as Atom from "effect/unstable/reactivity/Atom";
5
+ import { useEffect } from "react";
6
+ import { useSessionRecheck } from "./session-boundary.ts";
7
+
8
+ export interface ResultState<A, E> {
9
+ readonly result: AsyncResult.AsyncResult<A, E>;
10
+ readonly data: Option.Option<A>;
11
+ readonly cause: Option.Option<Cause.Cause<E>>;
12
+ readonly pending: boolean;
13
+ readonly refreshing: boolean;
14
+ }
15
+
16
+ const state = <A, E>(result: AsyncResult.AsyncResult<A, E>): ResultState<A, E> => {
17
+ const data = AsyncResult.value(result);
18
+ return {
19
+ result,
20
+ data,
21
+ cause: AsyncResult.cause(result),
22
+ pending: result.waiting,
23
+ refreshing: result.waiting && Option.isSome(data),
24
+ };
25
+ };
26
+
27
+ export const isUnauthorized = <E>(cause: Cause.Cause<E>): boolean =>
28
+ Option.match(Cause.findErrorOption(cause), { onNone: () => false, onSome: (error) => Predicate.isTagged(error, "Unauthorized") });
29
+
30
+ const useRecheckOnUnauthorized = <A, E>(result: AsyncResult.AsyncResult<A, E>): void => {
31
+ const recheck = useSessionRecheck();
32
+ useEffect(() => {
33
+ if (AsyncResult.isFailure(result) && isUnauthorized(result.cause)) recheck();
34
+ }, [recheck, result]);
35
+ };
36
+
37
+ export interface QueryState<A, E> extends ResultState<A, E> {
38
+ readonly refresh: () => void;
39
+ }
40
+
41
+ export const useQuery = <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>): QueryState<A, E> => {
42
+ const result = useAtomValue(atom);
43
+ useRecheckOnUnauthorized(result);
44
+ return { ...state(result), refresh: useAtomRefresh(atom) };
45
+ };
46
+
47
+ export interface ActionState<Input, A, E> extends ResultState<A, E> {
48
+ readonly dispatch: (input: Input) => void;
49
+ }
50
+
51
+ /** Dispatches an action and reads its shared native state without claiming a per-invocation completion result. */
52
+ export const useAction = <Input, A, E>(atom: Atom.AtomResultFn<Input, A, E>): ActionState<Input, A, E> => {
53
+ const result = useAtomValue(atom);
54
+ useRecheckOnUnauthorized(result);
55
+ return { ...state(result), dispatch: useAtomSet(atom) };
56
+ };
@@ -0,0 +1,37 @@
1
+ import * as Atom from "effect/unstable/reactivity/Atom";
2
+
3
+ export type ResumeSource = (resume: () => void) => () => void;
4
+
5
+ type ResumeEvent = "online" | "visibilitychange";
6
+
7
+ export interface ResumeWindow {
8
+ readonly addEventListener: (type: ResumeEvent, listener: () => void) => void;
9
+ readonly removeEventListener: (type: ResumeEvent, listener: () => void) => void;
10
+ readonly document: { readonly visibilityState: string };
11
+ }
12
+
13
+ export interface ResumeOptions {
14
+ readonly window?: ResumeWindow;
15
+ readonly native?: ResumeSource;
16
+ }
17
+
18
+ const whileVisible =
19
+ (target: ResumeWindow, type: ResumeEvent): ResumeSource =>
20
+ (resume) => {
21
+ const visible = () => {
22
+ if (target.document.visibilityState === "visible") resume();
23
+ };
24
+ target.addEventListener(type, visible);
25
+ return () => target.removeEventListener(type, visible);
26
+ };
27
+
28
+ export const resumeSignal = ({ window, native }: ResumeOptions): Atom.Atom<number> => {
29
+ const browser = window === undefined ? [] : [whileVisible(window, "visibilitychange"), whileVisible(window, "online")];
30
+ const sources = native === undefined ? browser : [...browser, native];
31
+ return Atom.readable((get) => {
32
+ let count = 0;
33
+ const resume = () => get.setSelf(++count);
34
+ for (const source of sources) get.addFinalizer(source(resume));
35
+ return count;
36
+ });
37
+ };
@@ -0,0 +1,50 @@
1
+ import { RegistryContext, useAtomValue } from "@effect/atom-react";
2
+ import { FieldFailure, type Fields, type Form, Invalid } from "@shivaedev/effect-form";
3
+ import { Option } from "effect";
4
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5
+ import * as Atom from "effect/unstable/reactivity/Atom";
6
+ import { useCallback, useContext, useEffect } from "react";
7
+ import { isUnauthorized } from "./result-state.ts";
8
+ import { useSessionRecheck } from "./session-boundary.ts";
9
+
10
+ const idle = Atom.make(AsyncResult.initial());
11
+ const clean = Atom.make(false);
12
+
13
+ export interface SaveState<E> {
14
+ readonly dirty: boolean;
15
+ readonly failure: Option.Option<E>;
16
+ readonly saving: boolean;
17
+ readonly save: () => void;
18
+ readonly revert: () => void;
19
+ }
20
+
21
+ export const useSaveState = <F extends Fields, A, E, ER>(
22
+ form: Form<F, A, E | FieldFailure, ER> | undefined,
23
+ settled: (result: A) => void,
24
+ ): SaveState<E | ER> => {
25
+ const registry = useContext(RegistryContext);
26
+ const recheck = useSessionRecheck();
27
+ const result: AsyncResult.AsyncResult<A, E | ER | FieldFailure | Invalid> = useAtomValue(form?.submit ?? idle);
28
+ const dirty = useAtomValue(form?.dirty ?? clean);
29
+ useEffect(() => {
30
+ if (AsyncResult.isFailure(result) && isUnauthorized(result.cause)) recheck();
31
+ }, [recheck, result]);
32
+ useEffect(
33
+ () =>
34
+ form &&
35
+ registry.subscribe(form.submit, (next) => {
36
+ if (AsyncResult.isSuccess(next) && !next.waiting) settled(next.value);
37
+ }),
38
+ [form, registry, settled],
39
+ );
40
+ const save = useCallback(() => {
41
+ if (form === undefined || registry.get(form.submit).waiting) return;
42
+ registry.set(form.submit, undefined);
43
+ }, [form, registry]);
44
+ const revert = useCallback(() => form?.revert(), [form]);
45
+ const failure = Option.filter(
46
+ AsyncResult.error(result),
47
+ (error): error is E | ER => !(error instanceof FieldFailure) && !(error instanceof Invalid),
48
+ );
49
+ return { dirty, failure, saving: result.waiting, save, revert };
50
+ };
@@ -0,0 +1,78 @@
1
+ import { RegistryContext } from "@effect/atom-react";
2
+ import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry";
3
+ import { createContext, createElement, Fragment, type ReactNode, useContext, useEffect, useState } from "react";
4
+
5
+ export interface SessionBoundaryProps<S, Client> {
6
+ readonly session: S | undefined;
7
+ readonly identify: (session: S) => string;
8
+ readonly connect: (session: S) => Client;
9
+ readonly recheck: () => void;
10
+ readonly signedOut?: ReactNode;
11
+ readonly children: (client: Client) => ReactNode;
12
+ }
13
+
14
+ const Recheck = createContext<() => void>(() => {});
15
+
16
+ export const useSessionRecheck = (): (() => void) => useContext(Recheck);
17
+
18
+ interface Owned<Client> {
19
+ readonly client: Client;
20
+ readonly registry: AtomRegistry.AtomRegistry;
21
+ mounted: number;
22
+ }
23
+
24
+ const owning = <Client>(client: Client): Owned<Client> => ({ client, registry: AtomRegistry.make(), mounted: 0 });
25
+
26
+ // No cleanup runs when a hidden <Activity> subtree is unmounted, so nodes a hidden render creates must not outlive it.
27
+ const hidden = <Client>(client: Client): Owned<Client> => {
28
+ const owned = owning(client);
29
+ let reaping = false;
30
+ owned.registry.onNodeAdded = () => {
31
+ if (reaping || owned.mounted > 0) return;
32
+ reaping = true;
33
+ setTimeout(() => {
34
+ reaping = false;
35
+ if (owned.mounted === 0) owned.registry.reset();
36
+ }, 0);
37
+ };
38
+ return owned;
39
+ };
40
+
41
+ interface GenerationProps<S, Client> {
42
+ readonly session: S;
43
+ readonly connect: (session: S) => Client;
44
+ readonly recheck: () => void;
45
+ readonly children: (client: Client) => ReactNode;
46
+ }
47
+
48
+ const Generation = <S, Client>({ session, connect, recheck, children }: GenerationProps<S, Client>): ReactNode => {
49
+ const [owned, own] = useState(() => owning(connect(session)));
50
+ useEffect(() => {
51
+ owned.mounted += 1;
52
+ return () => {
53
+ owned.mounted -= 1;
54
+ queueMicrotask(() => {
55
+ if (owned.mounted > 0) return;
56
+ owned.registry.dispose();
57
+ own((current) => (current === owned ? hidden(owned.client) : current));
58
+ });
59
+ };
60
+ }, [owned]);
61
+ return createElement(
62
+ Recheck.Provider,
63
+ { value: recheck },
64
+ createElement(RegistryContext.Provider, { value: owned.registry }, children(owned.client)),
65
+ );
66
+ };
67
+
68
+ export const SessionBoundary = <S, Client>({
69
+ session,
70
+ identify,
71
+ connect,
72
+ recheck,
73
+ signedOut,
74
+ children,
75
+ }: SessionBoundaryProps<S, Client>): ReactNode =>
76
+ session === undefined
77
+ ? createElement(Fragment, null, signedOut)
78
+ : createElement(Generation<S, Client>, { key: identify(session), session, connect, recheck, children });
@@ -0,0 +1,32 @@
1
+ import { type Decoded, type Encoded, type FieldFailure, type Fields, type Form, make, type Services } from "@shivaedev/effect-form";
2
+ import type { Effect, Schema } from "effect";
3
+ import type * as Atom from "effect/unstable/reactivity/Atom";
4
+ import { type AtomServices, type RejectField, rejecting } from "./field-rejection.ts";
5
+
6
+ export interface SubmissionConfig<F extends Fields, A, E, R, ER> {
7
+ readonly fields: Schema.Struct<F>;
8
+ readonly initialValues: Encoded<F>;
9
+ readonly save: (values: Decoded<F>) => Effect.Effect<A, E, AtomServices<R>>;
10
+ readonly runtime: Atom.AtomRuntime<Services<F, R>, ER>;
11
+ readonly rejectField: RejectField<F, E> | undefined;
12
+ }
13
+
14
+ export interface Submission<F extends Fields, A, E, ER> {
15
+ readonly form: Form<F, A, E | FieldFailure, ER>;
16
+ readonly submitted: () => Encoded<F> | undefined;
17
+ }
18
+
19
+ export const submission = <F extends Fields, A, E, R, ER>(config: SubmissionConfig<F, A, E, R, ER>): Submission<F, A, E, ER> => {
20
+ const { fields, initialValues, runtime } = config;
21
+ const save = rejecting(fields, config.save, config.rejectField);
22
+ let submitted: Encoded<F> | undefined;
23
+ const form: Form<F, A, E | FieldFailure, ER> = make(fields, {
24
+ initialValues,
25
+ runtime,
26
+ onSubmit: (values, submitter, snapshot) => {
27
+ submitted = snapshot;
28
+ return save(values, submitter);
29
+ },
30
+ });
31
+ return { form, submitted: () => submitted };
32
+ };