@shivaedev/effect-react 0.7.0 → 0.9.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.9.0 - 2026-10-19
4
+
5
+ ### Added
6
+
7
+ - `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.
8
+
9
+ ## 0.8.0 - 2026-10-18
10
+
11
+ ### Added
12
+
13
+ - Session boundaries mount automatic live connections on bound contract APIs returned by `connect`, either directly or as properties such as `client.api`. Connections follow the session registry lifetime and query keys. `client/mountLive.ts` also exports `mountLive(registry, client)`, returning the release function for a custom owner.
14
+
15
+ ### Breaking
16
+
17
+ - `SessionBoundary` now connects live queries automatically when its client contains a contract binding. Remove application `live()` calls and connection mounts; serve the contract through a Platform database host with NDJSON serialization.
18
+
3
19
  ## 0.7.0 - 2026-10-18
4
20
 
5
21
  ### Added
package/README.md CHANGED
@@ -28,7 +28,7 @@ State belongs to an **atom in a registry**. A query hook reads that atom's nativ
28
28
 
29
29
  An **editor** joins a query atom, an effect-form field schema and a save Effect. The query provides the row; `values(row)` converts it to input values; the schema decodes those values before the save runs. effect-form owns the draft and the per-field merge. A **create form** uses the same submission rules without a query and starts a fresh form after success.
30
30
 
31
- A **session generation** is the string returned by `identify(session)`. `SessionBoundary` owns its client, registry and React subtree. Your auth owner supplies the session and decides when to replace or end it. Cache policy, RPC declarations and transport stay with native Effect and the application; this package supplies their React state and lifecycle boundary.
31
+ A **session generation** is the string returned by `identify(session)`. `SessionBoundary` owns its client, registry and React subtree. Your auth owner supplies the session and decides when to replace or end it. The boundary automatically mounts live connections on bound contract APIs returned by `connect`, directly or as client properties such as `api`. Use a Platform database host and NDJSON transport; the application needs no subscribe handler or `live()` call. Cache policy, RPC declarations and transport stay with native Effect and the application; this package supplies their React state and lifecycle boundary.
32
32
 
33
33
  `useEditor` and `useCreate` opt into persisted input with `persistedDraft: { key }`. Choose a stable key for the record or create request, independent of its server revision. The adapters use effect-form's browser storage and inherit ownership from `SessionBoundary`: reload and query refresh preserve edits, successful submission clears accepted input, and a committed session replacement drops tracked drafts. A suspended replacement that is canceled keeps the visible session's draft and future writes. Later edits made during a save remain persisted. Optional `storage`, `maxBytes` and `maxAgeMs` customize the provider and bounds; defaults are `localStorage`, 256 KiB of UTF-8 JSON and seven days. See [effect-form's persisted input API](https://github.com/ShivaeDev/platform/tree/main/packages/effect-form#persist-input-across-reloads) for direct form hooks and limits.
34
34
 
@@ -525,6 +525,8 @@ screenStories.it("shows the route failure", [], async () => {
525
525
 
526
526
  The RPC layer stays application-owned: replace these in-memory handlers with the service Layer your component needs.
527
527
 
528
+ 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.
529
+
528
530
  ### Install, setup and limits
529
531
 
530
532
  ```sh
@@ -0,0 +1,3 @@
1
+ import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
2
+ export declare function mountLive(registry: AtomRegistry.AtomRegistry, client: unknown): () => void;
3
+ //# sourceMappingURL=mountLive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mountLive.d.ts","sourceRoot":"","sources":["../../src/client/mountLive.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAGpE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,cAwB7E"}
@@ -0,0 +1,29 @@
1
+ import { Predicate } from "effect";
2
+ import * as Atom from "effect/reactivity/Atom";
3
+ import { LiveConnection } from "@shivaedev/effect-contract/automaticLive.ts";
4
+ export function mountLive(registry, client) {
5
+ const releases = [];
6
+ const found = new Set();
7
+ function mount(value) {
8
+ if (!Predicate.hasProperty(value, LiveConnection)) {
9
+ return;
10
+ }
11
+ const live = value[LiveConnection];
12
+ if (Predicate.hasProperty(live, "connection") && Atom.isAtom(live.connection) && !found.has(live.connection)) {
13
+ found.add(live.connection);
14
+ releases.push(registry.mount(live.connection));
15
+ }
16
+ }
17
+ mount(client);
18
+ if (typeof client === "object" && client !== null) {
19
+ for (const value of Object.values(client)) {
20
+ mount(value);
21
+ }
22
+ }
23
+ return () => {
24
+ for (const release of releases) {
25
+ release();
26
+ }
27
+ };
28
+ }
29
+ //# sourceMappingURL=mountLive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mountLive.js","sourceRoot":"","sources":["../../src/client/mountLive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,6CAA6C,CAAC;AAE7E,MAAM,UAAU,SAAS,CAAC,QAAmC,EAAE,MAAe;IAC7E,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAW,CAAC;IACjC,SAAS,KAAK,CAAC,KAAc;QAC5B,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,cAAc,CAAC,EAAE,CAAC;YACnD,OAAO;QACR,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC;QACnC,IAAI,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAC9G,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC3B,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAChD,CAAC;IACF,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,CAAC;IACd,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACnD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3C,KAAK,CAAC,KAAK,CAAC,CAAC;QACd,CAAC;IACF,CAAC;IACD,OAAO,GAAG,EAAE;QACX,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAChC,OAAO,EAAE,CAAC;QACX,CAAC;IACF,CAAC,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sessionBoundary.d.ts","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0C,KAAK,SAAS,EAAmC,MAAM,OAAO,CAAC;AAIhH,MAAM,WAAW,oBAAoB,CAAC,QAAQ,EAAE,OAAO;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,SAAS,CAAC;IAClD,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAC/B;AAED,wBAAgB,iBAAiB,IAAI,MAAM,IAAI,CAE9C;AAuDD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAClD,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,GACR,EAAE,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,SAAS,CAUrD"}
1
+ {"version":3,"file":"sessionBoundary.d.ts","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0C,KAAK,SAAS,EAAmC,MAAM,OAAO,CAAC;AAKhH,MAAM,WAAW,oBAAoB,CAAC,QAAQ,EAAE,OAAO;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,SAAS,CAAC;IAClD,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAC/B;AAED,wBAAgB,iBAAiB,IAAI,MAAM,IAAI,CAE9C;AAyDD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAClD,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,GACR,EAAE,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,SAAS,CAUrD"}
@@ -3,6 +3,7 @@ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
3
  import { createContext, createElement, Fragment, useContext, useEffect, useState } from "react";
4
4
  import { DraftSessionContext } from "@shivaedev/effect-form/client/react.ts";
5
5
  import { registerSessionRecheck } from "#internal/sessionRecheck.ts";
6
+ import { mountLive } from "./mountLive.js";
6
7
  import { useDraftOwner } from "./useDraftOwner.js";
7
8
  const RecheckContext = createContext(() => undefined);
8
9
  export function useSessionRecheck() {
@@ -34,7 +35,9 @@ function Generation({ session, connect, recheck, children }) {
34
35
  useEffect(() => registerSessionRecheck(owned.registry, recheck), [owned, recheck]);
35
36
  useEffect(() => {
36
37
  owned.mounted += 1;
38
+ const stopLive = mountLive(owned.registry, owned.client);
37
39
  return () => {
40
+ stopLive();
38
41
  owned.mounted -= 1;
39
42
  queueMicrotask(() => {
40
43
  if (owned.mounted > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"sessionBoundary.js","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAkB,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAChH,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AASnD,MAAM,cAAc,GAAG,aAAa,CAAa,GAAS,EAAE,CAAC,SAAS,CAAC,CAAC;AACxE,MAAM,UAAU,iBAAiB;IAChC,OAAO,UAAU,CAAC,cAAc,CAAC,CAAC;AACnC,CAAC;AAMD,SAAS,MAAM,CAAU,MAAe;IACvC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;AACvF,CAAC;AACD,uHAAuH;AACvH,SAAS,MAAM,CAAU,MAAe;IACvC,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,EAAE,CAAC;YAClC,OAAO;QACR,CAAC;QACD,OAAO,GAAG,IAAI,CAAC;QACf,UAAU,CAAC,GAAG,EAAE;YACf,OAAO,GAAG,KAAK,CAAC;YAChB,IAAI,KAAK,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;gBACzB,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACxB,CAAC;QACF,CAAC,EAAE,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;IACF,OAAO,KAAK,CAAC;AACd,CAAC;AAOD,SAAS,UAAU,CAAoB,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAsC;IACjH,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,CAAC,sBAAsB,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;IACnF,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,EAAE,CAAC;oBACvB,OAAO;gBACR,CAAC;gBACD,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,cAAc,CAAC,QAAQ,EACvB,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;AACD,MAAM,UAAU,eAAe,CAAoB,EAClD,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,GACiC;IACzC,MAAM,EAAE,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjE,MAAM,OAAO,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;IAClC,OAAO,OAAO,KAAK,SAAS;QAC3B,CAAC,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,CAAC;QAC1C,CAAC,CAAC,aAAa,CACb,mBAAmB,CAAC,QAAQ,EAC5B,EAAE,KAAK,EAAE,OAAO,EAAE,EAClB,aAAa,CAAC,UAA6B,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAC9F,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"sessionBoundary.js","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"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"}
@@ -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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/effect-react",
3
- "version": "0.7.0",
3
+ "version": "0.9.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": {
@@ -43,7 +43,7 @@
43
43
  "README.md"
44
44
  ],
45
45
  "dependencies": {
46
- "@shivaedev/effect-contract": "0.5.1",
46
+ "@shivaedev/effect-contract": "0.6.0",
47
47
  "@shivaedev/effect-form": "0.5.0"
48
48
  },
49
49
  "devDependencies": {
@@ -51,14 +51,15 @@
51
51
  "@effect/platform-node": "4.0.4",
52
52
  "@effect/sql-pg": "4.0.4",
53
53
  "@effect/sql-sqlite-node": "4.0.4",
54
- "@shivaedev/effect-pg-boss": "0.10.1",
54
+ "@shivaedev/effect-pg-boss": "0.12.0",
55
55
  "@shivaedev/effect-service": "0.4.0",
56
- "@shivaedev/effect-sql": "0.6.0",
57
- "@shivaedev/effect-test": "0.6.0",
56
+ "@shivaedev/effect-sql": "0.7.0",
57
+ "@shivaedev/effect-test": "0.7.1",
58
+ "@shivaedev/effect-test-sql": "0.7.0",
58
59
  "@shivaedev/local-postgres": "0.4.2",
59
- "@shivaedev/platform": "0.18.0",
60
- "@shivaedev/quality": "0.27.1",
61
- "@shivaedev/test-story": "0.10.0",
60
+ "@shivaedev/platform": "0.20.0",
61
+ "@shivaedev/quality": "0.28.1",
62
+ "@shivaedev/test-story": "0.13.0",
62
63
  "@tanstack/react-router": "1.170.42",
63
64
  "@types/node": "24.19.2",
64
65
  "@types/react": "19.3.0",
@@ -0,0 +1,30 @@
1
+ import { Predicate } from "effect";
2
+ import * as Atom from "effect/reactivity/Atom";
3
+ import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
+ import { LiveConnection } from "@shivaedev/effect-contract/automaticLive.ts";
5
+
6
+ export function mountLive(registry: AtomRegistry.AtomRegistry, client: unknown) {
7
+ const releases: Array<() => void> = [];
8
+ const found = new Set<unknown>();
9
+ function mount(value: unknown) {
10
+ if (!Predicate.hasProperty(value, LiveConnection)) {
11
+ return;
12
+ }
13
+ const live = value[LiveConnection];
14
+ if (Predicate.hasProperty(live, "connection") && Atom.isAtom(live.connection) && !found.has(live.connection)) {
15
+ found.add(live.connection);
16
+ releases.push(registry.mount(live.connection));
17
+ }
18
+ }
19
+ mount(client);
20
+ if (typeof client === "object" && client !== null) {
21
+ for (const value of Object.values(client)) {
22
+ mount(value);
23
+ }
24
+ }
25
+ return () => {
26
+ for (const release of releases) {
27
+ release();
28
+ }
29
+ };
30
+ }
@@ -3,6 +3,7 @@ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
3
  import { createContext, createElement, Fragment, type ReactNode, useContext, useEffect, useState } from "react";
4
4
  import { DraftSessionContext } from "@shivaedev/effect-form/client/react.ts";
5
5
  import { registerSessionRecheck } from "#internal/sessionRecheck.ts";
6
+ import { mountLive } from "./mountLive.ts";
6
7
  import { useDraftOwner } from "./useDraftOwner.ts";
7
8
  export interface SessionBoundaryProps<TSession, TClient> {
8
9
  readonly children: (client: TClient) => ReactNode;
@@ -53,7 +54,9 @@ function Generation<TSession, TClient>({ session, connect, recheck, children }:
53
54
  useEffect(() => registerSessionRecheck(owned.registry, recheck), [owned, recheck]);
54
55
  useEffect(() => {
55
56
  owned.mounted += 1;
57
+ const stopLive = mountLive(owned.registry, owned.client);
56
58
  return () => {
59
+ stopLive();
57
60
  owned.mounted -= 1;
58
61
  queueMicrotask(() => {
59
62
  if (owned.mounted > 0) {
@@ -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
  }