@shivaedev/effect-react 0.8.0 → 0.10.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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 - 2026-10-19
4
+
5
+ ### Added
6
+
7
+ - effect-react exports `actionTrace(effect, name?)` from `actionTrace.ts` to group an action's RPC Effects under a fresh trace. Editor saves and create submissions automatically group their save Effects in a `ui.submit` trace. Native HTTP RPC clients send W3C trace context through the host to SQL and job enqueueing; pg-boss worker attempts retain their separate, linked traces.
8
+
9
+ ### Fixed
10
+
11
+ - Platform appHost preserves incoming W3C `traceparent` context for RPC handling even without an installed tracer or exporter. Request ids stay annotated on the RPC span.
12
+
13
+ ## 0.9.0 - 2026-10-19
14
+
15
+ ### Added
16
+
17
+ - `captureReactWarnings` in `client/testing.ts` acquires a scoped, silent capture for story kits. Its `ReactWarnings` handle reads console calls and unexpected caught React errors as a `ReactWarningsReadout`, excludes declared caught errors, and restores `console.error` on scope closure, including failed stories. `expectNoReactWarnings` uses the same capture mechanism.
18
+
3
19
  ## 0.8.0 - 2026-10-18
4
20
 
5
21
  ### Added
package/README.md CHANGED
@@ -78,6 +78,26 @@ export function App({ session, recheck }: {
78
78
 
79
79
  A query that fails with a tagged `Unauthorized` asks `recheck` to run and keeps its retained data. An action does the same once per failed result, retaining its last successful `data` and the typed failure in `cause`; rerendering that result does not repeat the recheck. The auth owner can retry the same session or remove it; the hook does not sign the user out. The boundary also handles React StrictMode's effect replay and preserves component state across hiding and revealing an `<Activity>` subtree, supplying a live registry when revealed.
80
80
 
81
+ ### Trace an action across RPCs
82
+
83
+ Editor saves and create submissions automatically run their save Effect in a fresh `ui.submit` trace. For custom actions, wrap the Effect that performs the action with `actionTrace` before giving it to a native atom:
84
+
85
+ ```ts
86
+ import { Effect } from "effect";
87
+ import { actionTrace } from "@shivaedev/effect-react/actionTrace.ts";
88
+
89
+ const save = OrdersClient.runtime.fn((input: OrderInput) =>
90
+ actionTrace(Effect.gen(function* () {
91
+ yield* api.validate.run(input);
92
+ return yield* api.save.run(input);
93
+ }), "ui.saveOrder"),
94
+ );
95
+ ```
96
+
97
+ `OrdersClient`, `OrderInput` and `api` belong to the application. Dispatch `save` with `useAction(save)`. `actionTrace(effect, name?)` preserves the Effect's result, typed error and service requirements; its default span name is `ui.action`. Each execution starts a fresh trace, and RPCs run inside that Effect share it, including across asynchronous boundaries. Native atoms continue to own execution and cancellation. `useAction` also accepts ordinary native action atoms; wrap composite action Effects to group their RPCs.
98
+
99
+ Use native `RpcClient.layerProtocolHttp` with `FetchHttpClient.layer` for the browser RPC protocol. Effect sends the trace in the W3C `traceparent` HTTP header. Platform's appHost carries it into the RPC span and its SQL, and annotates the RPC span with `request.id`. Enqueued pg-boss jobs retain that span context; worker attempts start separate traces linked back to the sender.
100
+
81
101
  ### 2. Once per feature: native queries and actions
82
102
 
83
103
  The hooks accept native atoms directly. This complete component module shows query and action state without RPC setup:
@@ -525,6 +545,8 @@ screenStories.it("shows the route failure", [], async () => {
525
545
 
526
546
  The RPC layer stays application-owned: replace these in-memory handlers with the service Layer your component needs.
527
547
 
548
+ Story kits can acquire `captureReactWarnings` from `client/testing.ts` in their `create` effect with `const warnings = yield* captureReactWarnings`. The scope owns the capture and restores `console.error` when it closes, including after a failed story. Pass `warnings.onCaughtError` to `render` to capture boundary errors, or `warnings.expectCaughtError(predicate)` to exclude a declared error. Both handlers silence caught errors; the capture also silences and records `console.error` calls. `warnings.read()` returns `{ consoleErrors, caughtErrors }`, with each console call's arguments and the unexpected caught errors. It asserts nothing, leaving the story spec to state its expectation. The Vitest guard uses the same capture mechanism and additionally checks that every declared error occurred.
549
+
528
550
  ### Install, setup and limits
529
551
 
530
552
  ```sh
@@ -0,0 +1,3 @@
1
+ import { Effect } from "effect";
2
+ export declare function actionTrace<A, E, R>(effect: Effect.Effect<A, E, R>, name?: string): Effect.Effect<A, E, R>;
3
+ //# sourceMappingURL=actionTrace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actionTrace.d.ts","sourceRoot":"","sources":["../src/actionTrace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,SAAc,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAE/G"}
@@ -0,0 +1,5 @@
1
+ import { Effect } from "effect";
2
+ export function actionTrace(effect, name = "ui.action") {
3
+ return Effect.withSpan(effect, name, { root: true });
4
+ }
5
+ //# sourceMappingURL=actionTrace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actionTrace.js","sourceRoot":"","sources":["../src/actionTrace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,UAAU,WAAW,CAAU,MAA8B,EAAE,IAAI,GAAG,WAAW;IACtF,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AACtD,CAAC"}
@@ -1,6 +1,18 @@
1
+ import { Effect, Scope } from "effect";
1
2
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
2
3
  import { type ReactNode } from "react";
3
4
  import { type RootOptions } from "react-dom/client";
5
+ type CaughtErrorHandler = NonNullable<RootOptions["onCaughtError"]>;
6
+ export interface ReactWarningsReadout {
7
+ readonly caughtErrors: readonly unknown[];
8
+ readonly consoleErrors: readonly (readonly unknown[])[];
9
+ }
10
+ export interface ReactWarnings {
11
+ readonly expectCaughtError: (matches: (error: unknown) => boolean) => CaughtErrorHandler;
12
+ readonly onCaughtError: CaughtErrorHandler;
13
+ readonly read: () => ReactWarningsReadout;
14
+ }
15
+ export declare const captureReactWarnings: Effect.Effect<ReactWarnings, never, Scope.Scope>;
4
16
  export declare function expectNoReactWarnings(): {
5
17
  expectCaughtError(matches: (error: unknown) => boolean): NonNullable<RootOptions["onCaughtError"]>;
6
18
  };
@@ -12,4 +24,5 @@ export interface Rendered {
12
24
  readonly unmount: () => Promise<void>;
13
25
  }
14
26
  export declare function render(node: ReactNode, options?: RootOptions): Promise<Rendered>;
27
+ export {};
15
28
  //# sourceMappingURL=testing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/client/testing.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEhE,wBAAgB,qBAAqB;IAmBnC,iBAAiB,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,WAAW,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;EAYnG;AACD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,IAAI,iBAgBlD;AACD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AACD,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CA4BtF"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/client/testing.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,MAAM,EAAQ,KAAK,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE,KAAK,kBAAkB,GAAG,WAAW,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,CAAC;AAEpE,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,YAAY,EAAE,SAAS,OAAO,EAAE,CAAC;IAC1C,QAAQ,CAAC,aAAa,EAAE,SAAS,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;CACxD;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,iBAAiB,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,KAAK,kBAAkB,CAAC;IACzF,QAAQ,CAAC,aAAa,EAAE,kBAAkB,CAAC;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,oBAAoB,CAAC;CAC1C;AAqCD,eAAO,MAAM,oBAAoB,kDAA6D,CAAC;AAE/F,wBAAgB,qBAAqB;IAkBnC,iBAAiB,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,WAAW,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;EAInG;AACD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,IAAI,iBAgBlD;AACD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AACD,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CA4BtF"}
@@ -1,26 +1,16 @@
1
1
  import { RegistryContext } from "@effect/atom-react";
2
- import { Clock, Effect } from "effect";
2
+ import { Clock, Effect, Exit, Scope } from "effect";
3
3
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
4
  import { act, createElement } from "react";
5
5
  import { createRoot } from "react-dom/client";
6
6
  import { beforeEach, expect, vi } from "vitest";
7
- export function expectNoReactWarnings() {
8
- let expectations = [];
9
- let unexpected = [];
10
- beforeEach(({ onTestFinished }) => {
11
- expectations = [];
12
- unexpected = [];
13
- const errors = vi.spyOn(console, "error");
14
- onTestFinished(() => {
15
- const calls = errors.mock.calls;
16
- errors.mockRestore();
17
- expect(calls).toEqual([]);
18
- expect(unexpected, "Unexpected caught React errors").toEqual([]);
19
- expect(expectations.every(({ caught }) => caught), "Expected caught React error did not occur").toBe(true);
20
- });
21
- });
22
- return {
23
- expectCaughtError(matches) {
7
+ const acquireReactWarnings = Effect.acquireRelease(Effect.sync(() => {
8
+ const errors = vi.spyOn(console, "error").mockImplementation(() => undefined);
9
+ let consoleErrors;
10
+ const caughtErrors = [];
11
+ const expectations = [];
12
+ const capture = {
13
+ expectCaughtError: (matches) => {
24
14
  const expectation = { caught: false };
25
15
  expectations.push(expectation);
26
16
  return (error) => {
@@ -28,10 +18,43 @@ export function expectNoReactWarnings() {
28
18
  expectation.caught = true;
29
19
  }
30
20
  else {
31
- unexpected.push(error);
21
+ caughtErrors.push(error);
32
22
  }
33
23
  };
34
24
  },
25
+ onCaughtError: (error) => {
26
+ caughtErrors.push(error);
27
+ },
28
+ read: () => ({ caughtErrors: [...caughtErrors], consoleErrors: consoleErrors ?? errors.mock.calls.map((args) => [...args]) }),
29
+ };
30
+ return {
31
+ capture,
32
+ expectations,
33
+ restore: () => {
34
+ consoleErrors = capture.read().consoleErrors;
35
+ errors.mockRestore();
36
+ },
37
+ };
38
+ }), ({ restore }) => Effect.sync(restore));
39
+ export const captureReactWarnings = Effect.map(acquireReactWarnings, ({ capture }) => capture);
40
+ export function expectNoReactWarnings() {
41
+ let current;
42
+ beforeEach(({ onTestFinished }) => {
43
+ const scope = Effect.runSync(Scope.make());
44
+ current = Effect.runSync(Effect.provideService(acquireReactWarnings, Scope.Scope, scope));
45
+ const { capture, expectations } = current;
46
+ onTestFinished(() => {
47
+ Effect.runSync(Scope.close(scope, Exit.void));
48
+ const { consoleErrors, caughtErrors } = capture.read();
49
+ expect(consoleErrors).toEqual([]);
50
+ expect(caughtErrors, "Unexpected caught React errors").toEqual([]);
51
+ expect(expectations.every(({ caught }) => caught), "Expected caught React error did not occur").toBe(true);
52
+ });
53
+ });
54
+ return {
55
+ expectCaughtError(matches) {
56
+ return current.capture.expectCaughtError(matches);
57
+ },
35
58
  };
36
59
  }
37
60
  export async function eventually(assert) {
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/client/testing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/client/testing.ts"],"names":[],"mappings":"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"}
@@ -3,17 +3,17 @@ import type * as Atom from "effect/reactivity/Atom";
3
3
  import type { PersistedDraftConfig } from "@shivaedev/effect-form/persistence.ts";
4
4
  import type { Decoded, Encoded, FieldFailure, Fields, Form, Services } from "@shivaedev/effect-form/shape.ts";
5
5
  import { type AtomServices, type RejectField } from "./fieldRejection.ts";
6
- export interface SubmissionConfig<F extends Fields, A, E, R, ER> {
7
- readonly fields: Schema.Struct<F>;
8
- readonly initialValues: Encoded<F>;
6
+ export interface SubmissionConfig<TFields extends Fields, A, E, R, TRuntimeError> {
7
+ readonly fields: Schema.Struct<TFields>;
8
+ readonly initialValues: Encoded<TFields>;
9
9
  readonly persistedDraft?: PersistedDraftConfig | undefined;
10
- readonly rejectField: RejectField<F, E> | undefined;
11
- readonly runtime: Atom.AtomRuntime<Services<F, R>, ER>;
12
- readonly save: (values: Decoded<F>) => Effect.Effect<A, E, AtomServices<R>>;
10
+ readonly rejectField: RejectField<TFields, E> | undefined;
11
+ readonly runtime: Atom.AtomRuntime<Services<TFields, R>, TRuntimeError>;
12
+ readonly save: (values: Decoded<TFields>) => Effect.Effect<A, E, AtomServices<R>>;
13
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;
14
+ export interface Submission<TFields extends Fields, A, E, TRuntimeError> {
15
+ readonly form: Form<TFields, A, E | FieldFailure, TRuntimeError>;
16
+ readonly submitted: () => Encoded<TFields> | undefined;
17
17
  }
18
- export declare const submission: <F extends Fields, A, E, R, ER>(config: SubmissionConfig<F, A, E, R, ER>) => Submission<F, A, E, ER>;
18
+ export declare function submission<TFields extends Fields, A, E, R, TRuntimeError>(config: SubmissionConfig<TFields, A, E, R, TRuntimeError>): Submission<TFields, A, E, TRuntimeError>;
19
19
  //# sourceMappingURL=submission.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"submission.d.ts","sourceRoot":"","sources":["../src/submission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAEpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAClF,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAC9G,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAa,MAAM,qBAAqB,CAAC;AAErF,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,cAAc,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAC3D,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvD,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;CAC5E;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,UAAU,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAG,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAc1H,CAAC"}
1
+ {"version":3,"file":"submission.d.ts","sourceRoot":"","sources":["../src/submission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAEpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAClF,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAE9G,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAa,MAAM,qBAAqB,CAAC;AAErF,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAC3D,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC;IAC1D,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;IACxE,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;CAClF;AAED,MAAM,WAAW,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa;IACtE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,aAAa,CAAC,CAAC;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;CACvD;AAED,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EACxE,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,GACvD,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,CAc1C"}
@@ -1,6 +1,7 @@
1
1
  import { make } from "@shivaedev/effect-form/form.ts";
2
+ import { actionTrace } from "#actionTrace.ts";
2
3
  import { rejecting } from "./fieldRejection.js";
3
- export const submission = (config) => {
4
+ export function submission(config) {
4
5
  const { fields, initialValues, runtime } = config;
5
6
  const save = rejecting(fields, config.save, config.rejectField);
6
7
  let submitted;
@@ -8,11 +9,11 @@ export const submission = (config) => {
8
9
  initialValues,
9
10
  onSubmit: (values, submitter, snapshot) => {
10
11
  submitted = snapshot;
11
- return save(values, submitter);
12
+ return actionTrace(save(values, submitter), "ui.submit");
12
13
  },
13
14
  persistedDraft: config.persistedDraft,
14
15
  runtime,
15
16
  });
16
17
  return { form, submitted: () => submitted };
17
- };
18
+ }
18
19
  //# sourceMappingURL=submission.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"submission.js","sourceRoot":"","sources":["../src/submission.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AAGtD,OAAO,EAAuC,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAgBrF,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,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;QACD,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,OAAO;KACP,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AAC7C,CAAC,CAAC"}
1
+ {"version":3,"file":"submission.js","sourceRoot":"","sources":["../src/submission.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AAGtD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAuC,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAgBrF,MAAM,UAAU,UAAU,CACzB,MAAyD;IAEzD,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,SAAuC,CAAC;IAC5C,MAAM,IAAI,GAAsD,IAAI,CAAC,MAAM,EAAE;QAC5E,aAAa;QACb,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;YACzC,SAAS,GAAG,QAAQ,CAAC;YACrB,OAAO,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC;QAC1D,CAAC;QACD,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,OAAO;KACP,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AAC7C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/effect-react",
3
- "version": "0.8.0",
3
+ "version": "0.10.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": {
@@ -53,13 +53,13 @@
53
53
  "@effect/sql-sqlite-node": "4.0.4",
54
54
  "@shivaedev/effect-pg-boss": "0.12.0",
55
55
  "@shivaedev/effect-service": "0.4.0",
56
- "@shivaedev/effect-sql": "0.7.0",
56
+ "@shivaedev/effect-sql": "0.8.0",
57
57
  "@shivaedev/effect-test": "0.7.1",
58
- "@shivaedev/effect-test-sql": "0.6.1",
58
+ "@shivaedev/effect-test-sql": "0.7.1",
59
59
  "@shivaedev/local-postgres": "0.4.2",
60
- "@shivaedev/platform": "0.19.0",
61
- "@shivaedev/quality": "0.28.0",
62
- "@shivaedev/test-story": "0.12.0",
60
+ "@shivaedev/platform": "0.20.1",
61
+ "@shivaedev/quality": "0.28.2",
62
+ "@shivaedev/test-story": "0.13.0",
63
63
  "@tanstack/react-router": "1.170.42",
64
64
  "@types/node": "24.19.2",
65
65
  "@types/react": "19.3.0",
@@ -0,0 +1,5 @@
1
+ import { Effect } from "effect";
2
+
3
+ export function actionTrace<A, E, R>(effect: Effect.Effect<A, E, R>, name = "ui.action"): Effect.Effect<A, E, R> {
4
+ return Effect.withSpan(effect, name, { root: true });
5
+ }
@@ -1,21 +1,71 @@
1
1
  import { RegistryContext } from "@effect/atom-react";
2
- import { Clock, Effect } from "effect";
2
+ import { Clock, Effect, Exit, Scope } from "effect";
3
3
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
4
  import { act, createElement, type ReactNode } from "react";
5
5
  import { createRoot, type RootOptions } from "react-dom/client";
6
6
  import { beforeEach, expect, vi } from "vitest";
7
+
8
+ type CaughtErrorHandler = NonNullable<RootOptions["onCaughtError"]>;
9
+
10
+ export interface ReactWarningsReadout {
11
+ readonly caughtErrors: readonly unknown[];
12
+ readonly consoleErrors: readonly (readonly unknown[])[];
13
+ }
14
+
15
+ export interface ReactWarnings {
16
+ readonly expectCaughtError: (matches: (error: unknown) => boolean) => CaughtErrorHandler;
17
+ readonly onCaughtError: CaughtErrorHandler;
18
+ readonly read: () => ReactWarningsReadout;
19
+ }
20
+
21
+ const acquireReactWarnings = Effect.acquireRelease(
22
+ Effect.sync(() => {
23
+ const errors = vi.spyOn(console, "error").mockImplementation(() => undefined);
24
+ let consoleErrors: readonly (readonly unknown[])[] | undefined;
25
+ const caughtErrors: unknown[] = [];
26
+ const expectations: { caught: boolean }[] = [];
27
+ const capture: ReactWarnings = {
28
+ expectCaughtError: (matches) => {
29
+ const expectation = { caught: false };
30
+ expectations.push(expectation);
31
+ return (error) => {
32
+ if (matches(error)) {
33
+ expectation.caught = true;
34
+ } else {
35
+ caughtErrors.push(error);
36
+ }
37
+ };
38
+ },
39
+ onCaughtError: (error) => {
40
+ caughtErrors.push(error);
41
+ },
42
+ read: () => ({ caughtErrors: [...caughtErrors], consoleErrors: consoleErrors ?? errors.mock.calls.map((args) => [...args]) }),
43
+ };
44
+ return {
45
+ capture,
46
+ expectations,
47
+ restore: () => {
48
+ consoleErrors = capture.read().consoleErrors;
49
+ errors.mockRestore();
50
+ },
51
+ };
52
+ }),
53
+ ({ restore }) => Effect.sync(restore),
54
+ );
55
+
56
+ export const captureReactWarnings = Effect.map(acquireReactWarnings, ({ capture }) => capture);
57
+
7
58
  export function expectNoReactWarnings() {
8
- let expectations: { caught: boolean }[] = [];
9
- let unexpected: unknown[] = [];
59
+ let current: Effect.Success<typeof acquireReactWarnings>;
10
60
  beforeEach(({ onTestFinished }) => {
11
- expectations = [];
12
- unexpected = [];
13
- const errors = vi.spyOn(console, "error");
61
+ const scope = Effect.runSync(Scope.make());
62
+ current = Effect.runSync(Effect.provideService(acquireReactWarnings, Scope.Scope, scope));
63
+ const { capture, expectations } = current;
14
64
  onTestFinished(() => {
15
- const calls = errors.mock.calls;
16
- errors.mockRestore();
17
- expect(calls).toEqual([]);
18
- expect(unexpected, "Unexpected caught React errors").toEqual([]);
65
+ Effect.runSync(Scope.close(scope, Exit.void));
66
+ const { consoleErrors, caughtErrors } = capture.read();
67
+ expect(consoleErrors).toEqual([]);
68
+ expect(caughtErrors, "Unexpected caught React errors").toEqual([]);
19
69
  expect(
20
70
  expectations.every(({ caught }) => caught),
21
71
  "Expected caught React error did not occur",
@@ -24,15 +74,7 @@ export function expectNoReactWarnings() {
24
74
  });
25
75
  return {
26
76
  expectCaughtError(matches: (error: unknown) => boolean): NonNullable<RootOptions["onCaughtError"]> {
27
- const expectation = { caught: false };
28
- expectations.push(expectation);
29
- return (error) => {
30
- if (matches(error)) {
31
- expectation.caught = true;
32
- } else {
33
- unexpected.push(error);
34
- }
35
- };
77
+ return current.capture.expectCaughtError(matches);
36
78
  },
37
79
  };
38
80
  }
package/src/submission.ts CHANGED
@@ -3,34 +3,37 @@ import type * as Atom from "effect/reactivity/Atom";
3
3
  import { make } from "@shivaedev/effect-form/form.ts";
4
4
  import type { PersistedDraftConfig } from "@shivaedev/effect-form/persistence.ts";
5
5
  import type { Decoded, Encoded, FieldFailure, Fields, Form, Services } from "@shivaedev/effect-form/shape.ts";
6
+ import { actionTrace } from "#actionTrace.ts";
6
7
  import { type AtomServices, type RejectField, rejecting } from "./fieldRejection.ts";
7
8
 
8
- export interface SubmissionConfig<F extends Fields, A, E, R, ER> {
9
- readonly fields: Schema.Struct<F>;
10
- readonly initialValues: Encoded<F>;
9
+ export interface SubmissionConfig<TFields extends Fields, A, E, R, TRuntimeError> {
10
+ readonly fields: Schema.Struct<TFields>;
11
+ readonly initialValues: Encoded<TFields>;
11
12
  readonly persistedDraft?: PersistedDraftConfig | undefined;
12
- readonly rejectField: RejectField<F, E> | undefined;
13
- readonly runtime: Atom.AtomRuntime<Services<F, R>, ER>;
14
- readonly save: (values: Decoded<F>) => Effect.Effect<A, E, AtomServices<R>>;
13
+ readonly rejectField: RejectField<TFields, E> | undefined;
14
+ readonly runtime: Atom.AtomRuntime<Services<TFields, R>, TRuntimeError>;
15
+ readonly save: (values: Decoded<TFields>) => Effect.Effect<A, E, AtomServices<R>>;
15
16
  }
16
17
 
17
- export interface Submission<F extends Fields, A, E, ER> {
18
- readonly form: Form<F, A, E | FieldFailure, ER>;
19
- readonly submitted: () => Encoded<F> | undefined;
18
+ export interface Submission<TFields extends Fields, A, E, TRuntimeError> {
19
+ readonly form: Form<TFields, A, E | FieldFailure, TRuntimeError>;
20
+ readonly submitted: () => Encoded<TFields> | undefined;
20
21
  }
21
22
 
22
- export const submission = <F extends Fields, A, E, R, ER>(config: SubmissionConfig<F, A, E, R, ER>): Submission<F, A, E, ER> => {
23
+ export function submission<TFields extends Fields, A, E, R, TRuntimeError>(
24
+ config: SubmissionConfig<TFields, A, E, R, TRuntimeError>,
25
+ ): Submission<TFields, A, E, TRuntimeError> {
23
26
  const { fields, initialValues, runtime } = config;
24
27
  const save = rejecting(fields, config.save, config.rejectField);
25
- let submitted: Encoded<F> | undefined;
26
- const form: Form<F, A, E | FieldFailure, ER> = make(fields, {
28
+ let submitted: Encoded<TFields> | undefined;
29
+ const form: Form<TFields, A, E | FieldFailure, TRuntimeError> = make(fields, {
27
30
  initialValues,
28
31
  onSubmit: (values, submitter, snapshot) => {
29
32
  submitted = snapshot;
30
- return save(values, submitter);
33
+ return actionTrace(save(values, submitter), "ui.submit");
31
34
  },
32
35
  persistedDraft: config.persistedDraft,
33
36
  runtime,
34
37
  });
35
38
  return { form, submitted: () => submitted };
36
- };
39
+ }