@shivaedev/effect-react 0.11.0 → 0.11.1
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 +6 -0
- package/README.md +2 -2
- package/dist/client/mountLive.d.ts.map +1 -1
- package/dist/client/mountLive.js +13 -2
- package/dist/client/mountLive.js.map +1 -1
- package/dist/client/resultState.d.ts +2 -1
- package/dist/client/resultState.d.ts.map +1 -1
- package/dist/client/resultState.js +9 -0
- package/dist/client/resultState.js.map +1 -1
- package/package.json +9 -9
- package/src/client/mountLive.ts +22 -3
- package/src/client/resultState.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.1 - 2026-10-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Recheck the session when an automatic live connection fails with `Unauthorized`, so `SessionBoundary` shows its signed-out content without another query read, stops reconnecting while signed out and restores live updates after sign-in.
|
|
8
|
+
|
|
3
9
|
## 0.11.0 - 2026-10-19
|
|
4
10
|
|
|
5
11
|
### 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. 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. Live changed hints coalesce over 20 ms by default. Set `liveCoalesceWindow` on `SessionBoundary` to a native Duration to choose the window for that session, or `0` for immediate per-hint refreshes. A refetch in flight runs once more after settling when further hints arrive. 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. Live changed hints coalesce over 20 ms by default. Set `liveCoalesceWindow` on `SessionBoundary` to a native Duration to choose the window for that session, or `0` for immediate per-hint refreshes. A bound query can override that default with `useQuery(atom, { coalesceWindow: "2 seconds" })`. Changing this option applies to subsequent windows without reconnecting or refetching any query; a window already started keeps its deadline. A refetch in flight runs once more after settling when further hints arrive. 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
|
|
|
@@ -436,7 +436,7 @@ const query = Atom.swr(atom, {
|
|
|
436
436
|
| `pending` | The native result is waiting. |
|
|
437
437
|
| `refreshing` | The result is waiting and successful data is available. |
|
|
438
438
|
|
|
439
|
-
`useQuery(atom, options?)` adds `refresh(): void`; `useAction` adds `dispatch(input): void`. `QueryOptions.keepPreviousData` defaults to `false` and controls retention across query atom changes. Same-identity refresh retention is always enabled. `result` and `cause` describe the current native query even when `data` displays a previous atom's success. Success, input and typed failure types are inferred from the supplied atom.
|
|
439
|
+
`useQuery(atom, options?)` adds `refresh(): void`; `useAction` adds `dispatch(input): void`. `QueryOptions.coalesceWindow` accepts a native Duration for live changed hints on contract-bound queries; it belongs to the query atom in its registry, so readers sharing that atom share the window. Omitting it uses the session default. `QueryOptions.keepPreviousData` defaults to `false` and controls retention across query atom changes. Same-identity refresh retention is always enabled. `result` and `cause` describe the current native query even when `data` displays a previous atom's success. Success, input and typed failure types are inferred from the supplied atom.
|
|
440
440
|
|
|
441
441
|
Import the defining `.ts` module, such as `@shivaedev/effect-react/client/editor.ts`. There is no root import. `useSessionRecheck()` from `client/sessionBoundary.ts` reads the boundary's `recheck` callback.
|
|
442
442
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mountLive.d.ts","sourceRoot":"","sources":["../../src/client/mountLive.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"mountLive.d.ts","sourceRoot":"","sources":["../../src/client/mountLive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,QAAQ,EAAqB,MAAM,QAAQ,CAAC;AAEjE,OAAO,KAAK,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAepE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,CAAC,EAAE,QAAQ,CAAC,KAAK,cAuC9G"}
|
package/dist/client/mountLive.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import { Predicate } from "effect";
|
|
1
|
+
import { Cause, Option, Predicate } from "effect";
|
|
2
2
|
import * as Atom from "effect/reactivity/Atom";
|
|
3
3
|
import { LiveConnection } from "@shivaedev/effect-contract/automaticLive.ts";
|
|
4
|
+
import { recheckUnauthorized } from "#internal/sessionRecheck.ts";
|
|
5
|
+
function isLiveBound(value) {
|
|
6
|
+
return Predicate.hasProperty(value, LiveConnection);
|
|
7
|
+
}
|
|
8
|
+
function recheckFailure(registry, failure) {
|
|
9
|
+
const error = Option.flatMap(failure, Cause.findErrorOption);
|
|
10
|
+
if (Option.isSome(error)) {
|
|
11
|
+
void recheckUnauthorized(registry, error.value);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
export function mountLive(registry, client, coalesceWindow) {
|
|
5
15
|
const releases = [];
|
|
6
16
|
const found = new Set();
|
|
7
17
|
function mount(value) {
|
|
8
|
-
if (!
|
|
18
|
+
if (!isLiveBound(value)) {
|
|
9
19
|
return;
|
|
10
20
|
}
|
|
11
21
|
const live = value[LiveConnection];
|
|
@@ -17,6 +27,7 @@ export function mountLive(registry, client, coalesceWindow) {
|
|
|
17
27
|
registry.set(live.coalesceWindow, coalesceWindow);
|
|
18
28
|
}
|
|
19
29
|
found.add(live.connection);
|
|
30
|
+
releases.push(registry.subscribe(Atom.map(live.status, (status) => status.failure), (failure) => recheckFailure(registry, failure), { immediate: true }));
|
|
20
31
|
releases.push(registry.mount(live.connection));
|
|
21
32
|
}
|
|
22
33
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mountLive.js","sourceRoot":"","sources":["../../src/client/mountLive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,SAAS,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"mountLive.js","sourceRoot":"","sources":["../../src/client/mountLive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAiB,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACjE,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAE/C,OAAO,EAAkB,cAAc,EAAE,MAAM,6CAA6C,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE,SAAS,WAAW,CAAC,KAAc;IAClC,OAAO,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;AACrD,CAAC;AAED,SAAS,cAAc,CAAC,QAAmC,EAAE,OAA4C;IACxG,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IAC7D,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,KAAK,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;AACF,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,QAAmC,EAAE,MAAe,EAAE,cAA+B;IAC9G,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAW,CAAC;IACjC,SAAS,KAAK,CAAC,KAAc;QAC5B,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,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,IACC,cAAc,KAAK,SAAS;mBACzB,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,gBAAgB,CAAC;mBAC7C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC;mBAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,EACtC,CAAC;gBACF,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC;YACnD,CAAC;YACD,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC3B,QAAQ,CAAC,IAAI,CACZ,QAAQ,CAAC,SAAS,CACjB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,EACjD,CAAC,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,EAC9C,EAAE,SAAS,EAAE,IAAI,EAAE,CACnB,CACD,CAAC;YACF,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,4 +1,4 @@
|
|
|
1
|
-
import { Cause, Option } from "effect";
|
|
1
|
+
import { Cause, type Duration, Option } from "effect";
|
|
2
2
|
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
3
3
|
import type * as Atom from "effect/reactivity/Atom";
|
|
4
4
|
export interface ResultState<A, E> {
|
|
@@ -13,6 +13,7 @@ export interface QueryState<A, E> extends ResultState<A, E> {
|
|
|
13
13
|
readonly refresh: () => void;
|
|
14
14
|
}
|
|
15
15
|
export interface QueryOptions {
|
|
16
|
+
readonly coalesceWindow?: Duration.Input | undefined;
|
|
16
17
|
readonly keepPreviousData?: boolean;
|
|
17
18
|
}
|
|
18
19
|
export declare function useQuery<A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>, options?: QueryOptions): QueryState<A, E>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resultState.d.ts","sourceRoot":"","sources":["../../src/client/resultState.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAa,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"resultState.d.ts","sourceRoot":"","sources":["../../src/client/resultState.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,KAAK,QAAQ,EAAE,MAAM,EAAa,MAAM,QAAQ,CAAC;AACjE,OAAO,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAMpD,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC/C;AAUD,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAEhE;AASD,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC7B;AACD,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACpC;AASD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAwB3H;AACD,MAAM,WAAW,WAAW,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC;IACnE,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAIxG"}
|
|
@@ -2,6 +2,7 @@ import { RegistryContext, useAtomRefresh, useAtomSet, useAtomValue } from "@effe
|
|
|
2
2
|
import { Cause, Option, Predicate } from "effect";
|
|
3
3
|
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
4
|
import { useContext, useEffect, useState } from "react";
|
|
5
|
+
import { queryCoalesceWindow } from "@shivaedev/effect-contract/queryCoalesceWindow.ts";
|
|
5
6
|
import { acceptLoaderQuery } from "#internal/queryHandoff.ts";
|
|
6
7
|
import { useSessionRecheck } from "./sessionBoundary.js";
|
|
7
8
|
function state(result, data = AsyncResult.value(result)) {
|
|
@@ -28,6 +29,14 @@ export function useQuery(atom, options = {}) {
|
|
|
28
29
|
const result = useAtomValue(atom);
|
|
29
30
|
const registry = useContext(RegistryContext);
|
|
30
31
|
useEffect(() => acceptLoaderQuery(registry, atom), [registry, atom]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (options.coalesceWindow === undefined) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const window = queryCoalesceWindow(atom);
|
|
37
|
+
registry.set(window, options.coalesceWindow);
|
|
38
|
+
return () => registry.set(window, undefined);
|
|
39
|
+
}, [registry, atom, options.coalesceWindow]);
|
|
31
40
|
const keepPreviousData = options.keepPreviousData === true;
|
|
32
41
|
const nativeData = AsyncResult.value(result);
|
|
33
42
|
const [held, hold] = useState(() => ({ atom, data: nativeData, dataAtom: atom, keepPreviousData, registry, result }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resultState.js","sourceRoot":"","sources":["../../src/client/resultState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"resultState.js","sourceRoot":"","sources":["../../src/client/resultState.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.11.
|
|
3
|
+
"version": "0.11.1",
|
|
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.
|
|
46
|
+
"@shivaedev/effect-contract": "0.9.0",
|
|
47
47
|
"@shivaedev/effect-form": "0.5.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
@@ -51,15 +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.
|
|
54
|
+
"@shivaedev/effect-pg-boss": "0.14.0",
|
|
55
55
|
"@shivaedev/effect-service": "0.4.0",
|
|
56
|
-
"@shivaedev/effect-sql": "0.
|
|
56
|
+
"@shivaedev/effect-sql": "0.12.0",
|
|
57
57
|
"@shivaedev/effect-test": "0.7.1",
|
|
58
|
-
"@shivaedev/effect-test-sql": "0.
|
|
59
|
-
"@shivaedev/local-postgres": "0.
|
|
60
|
-
"@shivaedev/platform": "0.
|
|
61
|
-
"@shivaedev/quality": "0.
|
|
62
|
-
"@shivaedev/test-story": "0.
|
|
58
|
+
"@shivaedev/effect-test-sql": "0.8.1",
|
|
59
|
+
"@shivaedev/local-postgres": "0.6.0",
|
|
60
|
+
"@shivaedev/platform": "0.22.0",
|
|
61
|
+
"@shivaedev/quality": "0.31.0",
|
|
62
|
+
"@shivaedev/test-story": "0.17.0",
|
|
63
63
|
"@tanstack/react-router": "1.170.42",
|
|
64
64
|
"@types/node": "24.19.2",
|
|
65
65
|
"@types/react": "19.3.0",
|
package/src/client/mountLive.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import { type Duration, Predicate } from "effect";
|
|
1
|
+
import { Cause, type Duration, Option, Predicate } from "effect";
|
|
2
2
|
import * as Atom from "effect/reactivity/Atom";
|
|
3
3
|
import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
4
|
-
import { LiveConnection } from "@shivaedev/effect-contract/automaticLive.ts";
|
|
4
|
+
import { type LiveBound, LiveConnection } from "@shivaedev/effect-contract/automaticLive.ts";
|
|
5
|
+
import { recheckUnauthorized } from "#internal/sessionRecheck.ts";
|
|
6
|
+
|
|
7
|
+
function isLiveBound(value: unknown): value is LiveBound {
|
|
8
|
+
return Predicate.hasProperty(value, LiveConnection);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function recheckFailure(registry: AtomRegistry.AtomRegistry, failure: Option.Option<Cause.Cause<unknown>>) {
|
|
12
|
+
const error = Option.flatMap(failure, Cause.findErrorOption);
|
|
13
|
+
if (Option.isSome(error)) {
|
|
14
|
+
void recheckUnauthorized(registry, error.value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
5
17
|
|
|
6
18
|
export function mountLive(registry: AtomRegistry.AtomRegistry, client: unknown, coalesceWindow?: Duration.Input) {
|
|
7
19
|
const releases: Array<() => void> = [];
|
|
8
20
|
const found = new Set<unknown>();
|
|
9
21
|
function mount(value: unknown) {
|
|
10
|
-
if (!
|
|
22
|
+
if (!isLiveBound(value)) {
|
|
11
23
|
return;
|
|
12
24
|
}
|
|
13
25
|
const live = value[LiveConnection];
|
|
@@ -21,6 +33,13 @@ export function mountLive(registry: AtomRegistry.AtomRegistry, client: unknown,
|
|
|
21
33
|
registry.set(live.coalesceWindow, coalesceWindow);
|
|
22
34
|
}
|
|
23
35
|
found.add(live.connection);
|
|
36
|
+
releases.push(
|
|
37
|
+
registry.subscribe(
|
|
38
|
+
Atom.map(live.status, (status) => status.failure),
|
|
39
|
+
(failure) => recheckFailure(registry, failure),
|
|
40
|
+
{ immediate: true },
|
|
41
|
+
),
|
|
42
|
+
);
|
|
24
43
|
releases.push(registry.mount(live.connection));
|
|
25
44
|
}
|
|
26
45
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { RegistryContext, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react";
|
|
2
|
-
import { Cause, Option, Predicate } from "effect";
|
|
2
|
+
import { Cause, type Duration, Option, Predicate } from "effect";
|
|
3
3
|
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
4
|
import type * as Atom from "effect/reactivity/Atom";
|
|
5
5
|
import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
6
6
|
import { useContext, useEffect, useState } from "react";
|
|
7
|
+
import { queryCoalesceWindow } from "@shivaedev/effect-contract/queryCoalesceWindow.ts";
|
|
7
8
|
import { acceptLoaderQuery } from "#internal/queryHandoff.ts";
|
|
8
9
|
import { useSessionRecheck } from "./sessionBoundary.ts";
|
|
9
10
|
export interface ResultState<A, E> {
|
|
@@ -37,6 +38,7 @@ export interface QueryState<A, E> extends ResultState<A, E> {
|
|
|
37
38
|
readonly refresh: () => void;
|
|
38
39
|
}
|
|
39
40
|
export interface QueryOptions {
|
|
41
|
+
readonly coalesceWindow?: Duration.Input | undefined;
|
|
40
42
|
readonly keepPreviousData?: boolean;
|
|
41
43
|
}
|
|
42
44
|
interface HeldQuery<A, E> {
|
|
@@ -51,6 +53,14 @@ export function useQuery<A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>, o
|
|
|
51
53
|
const result = useAtomValue(atom);
|
|
52
54
|
const registry = useContext(RegistryContext);
|
|
53
55
|
useEffect(() => acceptLoaderQuery(registry, atom), [registry, atom]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (options.coalesceWindow === undefined) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const window = queryCoalesceWindow(atom);
|
|
61
|
+
registry.set(window, options.coalesceWindow);
|
|
62
|
+
return () => registry.set(window, undefined);
|
|
63
|
+
}, [registry, atom, options.coalesceWindow]);
|
|
54
64
|
const keepPreviousData = options.keepPreviousData === true;
|
|
55
65
|
const nativeData = AsyncResult.value(result);
|
|
56
66
|
const [held, hold] = useState<HeldQuery<A, E>>(() => ({ atom, data: nativeData, dataAtom: atom, keepPreviousData, registry, result }));
|