@shivaedev/react 0.21.1 → 0.22.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 +13 -0
- package/README.md +3 -3
- package/dist/client/mountLive.d.ts.map +1 -1
- package/dist/client/mountLive.js +3 -14
- package/dist/client/mountLive.js.map +1 -1
- package/dist/client/sessionBoundary.d.ts.map +1 -1
- package/dist/client/sessionBoundary.js +16 -8
- package/dist/client/sessionBoundary.js.map +1 -1
- package/dist/internal/liveBindings.d.ts +3 -0
- package/dist/internal/liveBindings.d.ts.map +1 -0
- package/dist/internal/liveBindings.js +10 -0
- package/dist/internal/liveBindings.js.map +1 -0
- package/dist/internal/sessionRecheck.d.ts +1 -1
- package/dist/internal/sessionRecheck.d.ts.map +1 -1
- package/dist/internal/sessionRecheck.js +2 -2
- package/dist/internal/sessionRecheck.js.map +1 -1
- package/package.json +6 -6
- package/src/client/mountLive.ts +4 -16
- package/src/client/sessionBoundary.ts +24 -8
- package/src/internal/liveBindings.ts +11 -0
- package/src/internal/sessionRecheck.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.22.0 - 2026-10-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Forwarding proxy fixtures relay WebSocket upgrades, including the same origin and trusted proxy headers as HTTP requests. Existing proxy URLs also work for socket clients.
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- Contract clients use Effect RPC over a shared WebSocket by default. Replace `httpClient`, `transformClient` and `serialization` client options with a native `protocol` Layer for custom transports. Node clients can supply upgrade `headers` and a `webSocketConstructor` Layer; browsers use their session cookie automatically.
|
|
12
|
+
- Mounting a bound query subscribes before its first authorized read; unmounting releases its subscription. Use `live: false` when binding an ordinary client without subscriptions. Query preparation alone no longer opens a change stream.
|
|
13
|
+
- Hosts serve live subscriptions on the WebSocket upgrade at the RPC path. Move HTTP `$changes` consumers to Effect RPC's socket protocol; the POST route serves ordinary RPC only. Configure development proxies to forward WebSocket upgrades.
|
|
14
|
+
- React session boundaries bind shared sockets to the identified session, so replacing an account upgrades with its current cookie. They also ask the auth owner to recheck when the live socket closes. Keep `recheck` safe to call after a network interruption; the auth owner still decides whether the session ends.
|
|
15
|
+
|
|
3
16
|
## 0.21.1 - 2026-10-29
|
|
4
17
|
|
|
5
18
|
### Changed
|
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, a 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. 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
|
|
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 the default shared WebSocket client; 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 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 [form's persisted input API](https://github.com/ShivaeDev/platform/tree/main/packages/form#persist-input-across-reloads) for direct form hooks and limits.
|
|
34
34
|
|
|
@@ -79,9 +79,9 @@ export function App({ session, recheck }: { readonly session: Session | undefine
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
`makeOrderClient` and `OrderScreens` belong to the application. A changed generation creates a new client and registry, remounts the screens and disposes the outgoing registry. `undefined` renders `signedOut`. Updating the session object with the same generation keeps the existing client: `connect` does not receive a rotated token until the identity changes. Include the credential generation when the client captures its token, as above.
|
|
82
|
+
`makeOrderClient` and `OrderScreens` belong to the application. A changed generation creates a new client and registry, acquires a socket with the current session cookie, remounts the screens and disposes the outgoing registry. Independent registries for the same generation share the socket. `undefined` renders `signedOut`. Updating the session object with the same generation keeps the existing client: `connect` does not receive a rotated token until the identity changes. Include the credential generation when the client captures its token, as above.
|
|
83
83
|
|
|
84
|
-
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.
|
|
84
|
+
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. A live socket failure also asks the auth owner to recheck, since the host closes revoked sessions. 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.
|
|
85
85
|
|
|
86
86
|
#### Session-scoped bindings
|
|
87
87
|
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAYpE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,CAAC,EAAE,QAAQ,CAAC,KAAK,cA8B9G"}
|
package/dist/client/mountLive.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import { Cause, Option, Predicate } from "effect";
|
|
2
2
|
import * as Atom from "effect/reactivity/Atom";
|
|
3
3
|
import { LiveConnection } from "@shivaedev/contract/automaticLive.ts";
|
|
4
|
+
import { liveBindings } from "#internal/liveBindings.ts";
|
|
4
5
|
import { recheckUnauthorized } from "#internal/sessionRecheck.ts";
|
|
5
|
-
function isLiveBound(value) {
|
|
6
|
-
return Predicate.hasProperty(value, LiveConnection);
|
|
7
|
-
}
|
|
8
6
|
function recheckFailure(registry, failure) {
|
|
9
7
|
const error = Option.flatMap(failure, Cause.findErrorOption);
|
|
10
8
|
if (Option.isSome(error)) {
|
|
11
|
-
void recheckUnauthorized(registry, error.value);
|
|
9
|
+
void recheckUnauthorized(registry, error.value, true);
|
|
12
10
|
}
|
|
13
11
|
}
|
|
14
12
|
export function mountLive(registry, client, coalesceWindow) {
|
|
15
13
|
const releases = [];
|
|
16
14
|
const found = new Set();
|
|
17
|
-
|
|
18
|
-
if (!isLiveBound(value)) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
15
|
+
for (const value of liveBindings(client)) {
|
|
21
16
|
const live = value[LiveConnection];
|
|
22
17
|
if (Predicate.hasProperty(live, "connection") && Atom.isAtom(live.connection) && !found.has(live.connection)) {
|
|
23
18
|
if (coalesceWindow !== undefined
|
|
@@ -31,12 +26,6 @@ export function mountLive(registry, client, coalesceWindow) {
|
|
|
31
26
|
releases.push(registry.mount(live.connection));
|
|
32
27
|
}
|
|
33
28
|
}
|
|
34
|
-
mount(client);
|
|
35
|
-
if (typeof client === "object" && client !== null) {
|
|
36
|
-
for (const value of Object.values(client)) {
|
|
37
|
-
mount(value);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
29
|
return () => {
|
|
41
30
|
for (const release of releases) {
|
|
42
31
|
release();
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE,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,EAAE,IAAI,CAAC,CAAC;IACvD,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,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1C,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,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":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAA0C,KAAK,SAAS,EAAmC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"sessionBoundary.d.ts","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAA0C,KAAK,SAAS,EAAmC,MAAM,OAAO,CAAC;AAQhH,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,EAAE,QAAQ,EAAE,YAAY,CAAC,YAAY,KAAK,OAAO,CAAC;IACtF,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAC;IACzD,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;AAmFD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAClD,OAAO,EACP,kBAAkB,EAClB,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,GACR,EAAE,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,SAAS,CAUrD"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { RegistryContext } from "@effect/atom-react";
|
|
2
2
|
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
3
3
|
import { createContext, createElement, Fragment, useContext, useEffect, useState } from "react";
|
|
4
|
+
import { LiveConnection } from "@shivaedev/contract/automaticLive.ts";
|
|
4
5
|
import { DraftSessionContext } from "@shivaedev/form/client/react.ts";
|
|
6
|
+
import { liveBindings } from "#internal/liveBindings.ts";
|
|
5
7
|
import { registerSessionRecheck } from "#internal/sessionRecheck.ts";
|
|
6
8
|
import { useLatestCallback } from "#internal/useLatestCallback.ts";
|
|
7
9
|
import { mountLive } from "./mountLive.js";
|
|
@@ -10,12 +12,18 @@ const RecheckContext = createContext(() => undefined);
|
|
|
10
12
|
export function useSessionRecheck() {
|
|
11
13
|
return useContext(RecheckContext);
|
|
12
14
|
}
|
|
13
|
-
function owning(connect) {
|
|
15
|
+
function owning(connect, identity) {
|
|
14
16
|
const registry = AtomRegistry.make({ "defaultIdleTTL": 400 });
|
|
15
17
|
let client;
|
|
16
18
|
return {
|
|
17
19
|
get client() {
|
|
18
20
|
client ??= { value: connect(registry) };
|
|
21
|
+
for (const binding of liveBindings(client.value)) {
|
|
22
|
+
const session = binding[LiveConnection].identity;
|
|
23
|
+
if (session !== undefined) {
|
|
24
|
+
registry.set(session, identity);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
19
27
|
return client.value;
|
|
20
28
|
},
|
|
21
29
|
mounted: 0,
|
|
@@ -23,8 +31,8 @@ function owning(connect) {
|
|
|
23
31
|
};
|
|
24
32
|
}
|
|
25
33
|
// No cleanup runs when a hidden <Activity> subtree is unmounted, so nodes a hidden render creates must not outlive it.
|
|
26
|
-
function hidden(connect) {
|
|
27
|
-
const owned = owning(connect);
|
|
34
|
+
function hidden(connect, identity) {
|
|
35
|
+
const owned = owning(connect, identity);
|
|
28
36
|
let reaping = false;
|
|
29
37
|
owned.registry.onNodeAdded = () => {
|
|
30
38
|
if (reaping || owned.mounted > 0) {
|
|
@@ -40,10 +48,10 @@ function hidden(connect) {
|
|
|
40
48
|
};
|
|
41
49
|
return owned;
|
|
42
50
|
}
|
|
43
|
-
function Generation({ session, connect, recheck, children, liveCoalesceWindow }) {
|
|
51
|
+
function Generation({ session, connect, recheck, children, identity, liveCoalesceWindow, }) {
|
|
44
52
|
const recheckLatest = useLatestCallback(recheck);
|
|
45
53
|
const [open] = useState(() => (registry) => connect(session, registry));
|
|
46
|
-
const [owned, own] = useState(() => owning(open));
|
|
54
|
+
const [owned, own] = useState(() => owning(open, identity));
|
|
47
55
|
useEffect(() => registerSessionRecheck(owned.registry, recheckLatest), [owned, recheckLatest]);
|
|
48
56
|
useEffect(() => {
|
|
49
57
|
owned.mounted += 1;
|
|
@@ -56,10 +64,10 @@ function Generation({ session, connect, recheck, children, liveCoalesceWindow })
|
|
|
56
64
|
return;
|
|
57
65
|
}
|
|
58
66
|
owned.registry.dispose();
|
|
59
|
-
own((current) => (current === owned ? hidden(open) : current));
|
|
67
|
+
own((current) => (current === owned ? hidden(open, identity) : current));
|
|
60
68
|
});
|
|
61
69
|
};
|
|
62
|
-
}, [owned, liveCoalesceWindow, open]);
|
|
70
|
+
}, [identity, owned, liveCoalesceWindow, open]);
|
|
63
71
|
return createElement(RecheckContext.Provider, { value: recheckLatest }, createElement(RegistryContext.Provider, { value: owned.registry }, children(owned.client)));
|
|
64
72
|
}
|
|
65
73
|
export function SessionBoundary({ session, liveCoalesceWindow, identify, connect, recheck, signedOut, children, }) {
|
|
@@ -67,6 +75,6 @@ export function SessionBoundary({ session, liveCoalesceWindow, identify, connect
|
|
|
67
75
|
const current = useDraftOwner(id);
|
|
68
76
|
return session === undefined
|
|
69
77
|
? createElement(Fragment, null, signedOut)
|
|
70
|
-
: createElement(DraftSessionContext.Provider, { value: current }, createElement(Generation, { children, connect, key: id, liveCoalesceWindow, recheck, session }));
|
|
78
|
+
: createElement(DraftSessionContext.Provider, { value: current }, createElement(Generation, { children, connect, identity: id, key: id, liveCoalesceWindow, recheck, session }));
|
|
71
79
|
}
|
|
72
80
|
//# sourceMappingURL=sessionBoundary.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionBoundary.js","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,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,iCAAiC,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAUnD,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,OAAyD;
|
|
1
|
+
{"version":3,"file":"sessionBoundary.js","sourceRoot":"","sources":["../../src/client/sessionBoundary.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liveBindings.d.ts","sourceRoot":"","sources":["../../src/internal/liveBindings.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAkB,MAAM,sCAAsC,CAAC;AAMtF,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,SAAS,EAAE,CAGlE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Predicate } from "effect";
|
|
2
|
+
import { LiveConnection } from "@shivaedev/contract/automaticLive.ts";
|
|
3
|
+
function isLiveBound(value) {
|
|
4
|
+
return Predicate.hasProperty(value, LiveConnection);
|
|
5
|
+
}
|
|
6
|
+
export function liveBindings(client) {
|
|
7
|
+
const values = typeof client === "object" && client !== null ? [client, ...Object.values(client)] : [client];
|
|
8
|
+
return values.filter(isLiveBound);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=liveBindings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liveBindings.js","sourceRoot":"","sources":["../../src/internal/liveBindings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAkB,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAEtF,SAAS,WAAW,CAAC,KAAc;IAClC,OAAO,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAe;IAC3C,MAAM,MAAM,GAAG,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC7G,OAAO,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AACnC,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
2
2
|
export declare function registerSessionRecheck(registry: AtomRegistry.AtomRegistry, recheck: () => void): () => void;
|
|
3
|
-
export declare function recheckUnauthorized(registry: AtomRegistry.AtomRegistry, error: unknown): Promise<void>;
|
|
3
|
+
export declare function recheckUnauthorized(registry: AtomRegistry.AtomRegistry, error: unknown, connection?: boolean): Promise<void>;
|
|
4
4
|
//# sourceMappingURL=sessionRecheck.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionRecheck.d.ts","sourceRoot":"","sources":["../../src/internal/sessionRecheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAIpE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK3G;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"sessionRecheck.d.ts","sourceRoot":"","sources":["../../src/internal/sessionRecheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAIpE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK3G;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhI"}
|
|
@@ -6,8 +6,8 @@ export function registerSessionRecheck(registry, recheck) {
|
|
|
6
6
|
rechecks.delete(registry);
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
-
export async function recheckUnauthorized(registry, error) {
|
|
10
|
-
if (Predicate.isTagged(error, "Unauthorized")) {
|
|
9
|
+
export async function recheckUnauthorized(registry, error, connection = false) {
|
|
10
|
+
if (Predicate.isTagged(error, "Unauthorized") || (connection && Predicate.isTagged(error, "RpcClientError"))) {
|
|
11
11
|
await rechecks.get(registry)?.();
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionRecheck.js","sourceRoot":"","sources":["../../src/internal/sessionRecheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAGnC,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAyC,CAAC;AAEtE,MAAM,UAAU,sBAAsB,CAAC,QAAmC,EAAE,OAAmB;IAC9F,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,GAAG,EAAE;QACX,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3B,CAAC,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAmC,EAAE,KAAc;
|
|
1
|
+
{"version":3,"file":"sessionRecheck.js","sourceRoot":"","sources":["../../src/internal/sessionRecheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAGnC,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAyC,CAAC;AAEtE,MAAM,UAAU,sBAAsB,CAAC,QAAmC,EAAE,OAAmB;IAC9F,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,GAAG,EAAE;QACX,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3B,CAAC,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAmC,EAAE,KAAc,EAAE,UAAU,GAAG,KAAK;IAChH,IAAI,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,IAAI,CAAC,UAAU,IAAI,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC;QAC9G,MAAM,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;IAClC,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "React query, action, editor and session hooks for native Effect atoms",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@shivaedev/access": "0.1.1",
|
|
50
|
-
"@shivaedev/contract": "0.
|
|
50
|
+
"@shivaedev/contract": "0.20.0",
|
|
51
51
|
"@shivaedev/errors": "0.1.0",
|
|
52
52
|
"@shivaedev/form": "0.7.0",
|
|
53
53
|
"@shivaedev/test": "0.10.0"
|
|
@@ -58,15 +58,15 @@
|
|
|
58
58
|
"@effect/sql-pg": "4.0.5",
|
|
59
59
|
"@effect/sql-sqlite-node": "4.0.5",
|
|
60
60
|
"@shivaedev/changes": "0.5.0",
|
|
61
|
-
"@shivaedev/identity": "0.3.
|
|
61
|
+
"@shivaedev/identity": "0.3.2",
|
|
62
62
|
"@shivaedev/jobs": "0.22.0",
|
|
63
63
|
"@shivaedev/local-postgres": "0.7.0",
|
|
64
|
-
"@shivaedev/quality": "0.44.
|
|
65
|
-
"@shivaedev/server": "0.
|
|
64
|
+
"@shivaedev/quality": "0.44.1",
|
|
65
|
+
"@shivaedev/server": "0.4.0",
|
|
66
66
|
"@shivaedev/service": "0.5.0",
|
|
67
67
|
"@shivaedev/sql": "0.24.0",
|
|
68
68
|
"@shivaedev/test-sql": "0.10.2",
|
|
69
|
-
"@shivaedev/test-story": "0.
|
|
69
|
+
"@shivaedev/test-story": "0.23.0",
|
|
70
70
|
"@tanstack/react-router": "1.170.45",
|
|
71
71
|
"@types/node": "24.19.2",
|
|
72
72
|
"@types/react": "19.3.0",
|
package/src/client/mountLive.ts
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
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 {
|
|
4
|
+
import { LiveConnection } from "@shivaedev/contract/automaticLive.ts";
|
|
5
|
+
import { liveBindings } from "#internal/liveBindings.ts";
|
|
5
6
|
import { recheckUnauthorized } from "#internal/sessionRecheck.ts";
|
|
6
7
|
|
|
7
|
-
function isLiveBound(value: unknown): value is LiveBound {
|
|
8
|
-
return Predicate.hasProperty(value, LiveConnection);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
8
|
function recheckFailure(registry: AtomRegistry.AtomRegistry, failure: Option.Option<Cause.Cause<unknown>>) {
|
|
12
9
|
const error = Option.flatMap(failure, Cause.findErrorOption);
|
|
13
10
|
if (Option.isSome(error)) {
|
|
14
|
-
void recheckUnauthorized(registry, error.value);
|
|
11
|
+
void recheckUnauthorized(registry, error.value, true);
|
|
15
12
|
}
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
export function mountLive(registry: AtomRegistry.AtomRegistry, client: unknown, coalesceWindow?: Duration.Input) {
|
|
19
16
|
const releases: Array<() => void> = [];
|
|
20
17
|
const found = new Set<unknown>();
|
|
21
|
-
|
|
22
|
-
if (!isLiveBound(value)) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
18
|
+
for (const value of liveBindings(client)) {
|
|
25
19
|
const live = value[LiveConnection];
|
|
26
20
|
if (Predicate.hasProperty(live, "connection") && Atom.isAtom(live.connection) && !found.has(live.connection)) {
|
|
27
21
|
if (
|
|
@@ -43,12 +37,6 @@ export function mountLive(registry: AtomRegistry.AtomRegistry, client: unknown,
|
|
|
43
37
|
releases.push(registry.mount(live.connection));
|
|
44
38
|
}
|
|
45
39
|
}
|
|
46
|
-
mount(client);
|
|
47
|
-
if (typeof client === "object" && client !== null) {
|
|
48
|
-
for (const value of Object.values(client)) {
|
|
49
|
-
mount(value);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
40
|
return () => {
|
|
53
41
|
for (const release of releases) {
|
|
54
42
|
release();
|
|
@@ -2,7 +2,9 @@ import { RegistryContext } from "@effect/atom-react";
|
|
|
2
2
|
import type { Duration } from "effect";
|
|
3
3
|
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
4
4
|
import { createContext, createElement, Fragment, type ReactNode, useContext, useEffect, useState } from "react";
|
|
5
|
+
import { LiveConnection } from "@shivaedev/contract/automaticLive.ts";
|
|
5
6
|
import { DraftSessionContext } from "@shivaedev/form/client/react.ts";
|
|
7
|
+
import { liveBindings } from "#internal/liveBindings.ts";
|
|
6
8
|
import { registerSessionRecheck } from "#internal/sessionRecheck.ts";
|
|
7
9
|
import { useLatestCallback } from "#internal/useLatestCallback.ts";
|
|
8
10
|
import { mountLive } from "./mountLive.ts";
|
|
@@ -25,12 +27,18 @@ interface Owned<TClient> {
|
|
|
25
27
|
mounted: number;
|
|
26
28
|
readonly registry: AtomRegistry.AtomRegistry;
|
|
27
29
|
}
|
|
28
|
-
function owning<TClient>(connect: (registry: AtomRegistry.AtomRegistry) => TClient): Owned<TClient> {
|
|
30
|
+
function owning<TClient>(connect: (registry: AtomRegistry.AtomRegistry) => TClient, identity: string | undefined): Owned<TClient> {
|
|
29
31
|
const registry = AtomRegistry.make({ "defaultIdleTTL": 400 });
|
|
30
32
|
let client: { readonly value: TClient } | undefined;
|
|
31
33
|
return {
|
|
32
34
|
get client() {
|
|
33
35
|
client ??= { value: connect(registry) };
|
|
36
|
+
for (const binding of liveBindings(client.value)) {
|
|
37
|
+
const session = binding[LiveConnection].identity;
|
|
38
|
+
if (session !== undefined) {
|
|
39
|
+
registry.set(session, identity);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
34
42
|
return client.value;
|
|
35
43
|
},
|
|
36
44
|
mounted: 0,
|
|
@@ -38,8 +46,8 @@ function owning<TClient>(connect: (registry: AtomRegistry.AtomRegistry) => TClie
|
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
48
|
// No cleanup runs when a hidden <Activity> subtree is unmounted, so nodes a hidden render creates must not outlive it.
|
|
41
|
-
function hidden<TClient>(connect: (registry: AtomRegistry.AtomRegistry) => TClient): Owned<TClient> {
|
|
42
|
-
const owned = owning(connect);
|
|
49
|
+
function hidden<TClient>(connect: (registry: AtomRegistry.AtomRegistry) => TClient, identity: string | undefined): Owned<TClient> {
|
|
50
|
+
const owned = owning(connect, identity);
|
|
43
51
|
let reaping = false;
|
|
44
52
|
owned.registry.onNodeAdded = () => {
|
|
45
53
|
if (reaping || owned.mounted > 0) {
|
|
@@ -58,14 +66,22 @@ function hidden<TClient>(connect: (registry: AtomRegistry.AtomRegistry) => TClie
|
|
|
58
66
|
interface GenerationProps<TSession, TClient> {
|
|
59
67
|
readonly children: (client: TClient) => ReactNode;
|
|
60
68
|
readonly connect: (session: TSession, registry: AtomRegistry.AtomRegistry) => TClient;
|
|
69
|
+
readonly identity: string | undefined;
|
|
61
70
|
readonly liveCoalesceWindow?: Duration.Input | undefined;
|
|
62
71
|
readonly recheck: () => void;
|
|
63
72
|
readonly session: TSession;
|
|
64
73
|
}
|
|
65
|
-
function Generation<TSession, TClient>({
|
|
74
|
+
function Generation<TSession, TClient>({
|
|
75
|
+
session,
|
|
76
|
+
connect,
|
|
77
|
+
recheck,
|
|
78
|
+
children,
|
|
79
|
+
identity,
|
|
80
|
+
liveCoalesceWindow,
|
|
81
|
+
}: GenerationProps<TSession, TClient>): ReactNode {
|
|
66
82
|
const recheckLatest = useLatestCallback(recheck);
|
|
67
83
|
const [open] = useState(() => (registry: AtomRegistry.AtomRegistry) => connect(session, registry));
|
|
68
|
-
const [owned, own] = useState(() => owning(open));
|
|
84
|
+
const [owned, own] = useState(() => owning(open, identity));
|
|
69
85
|
useEffect(() => registerSessionRecheck(owned.registry, recheckLatest), [owned, recheckLatest]);
|
|
70
86
|
useEffect(() => {
|
|
71
87
|
owned.mounted += 1;
|
|
@@ -78,10 +94,10 @@ function Generation<TSession, TClient>({ session, connect, recheck, children, li
|
|
|
78
94
|
return;
|
|
79
95
|
}
|
|
80
96
|
owned.registry.dispose();
|
|
81
|
-
own((current) => (current === owned ? hidden(open) : current));
|
|
97
|
+
own((current) => (current === owned ? hidden(open, identity) : current));
|
|
82
98
|
});
|
|
83
99
|
};
|
|
84
|
-
}, [owned, liveCoalesceWindow, open]);
|
|
100
|
+
}, [identity, owned, liveCoalesceWindow, open]);
|
|
85
101
|
return createElement(
|
|
86
102
|
RecheckContext.Provider,
|
|
87
103
|
{ value: recheckLatest },
|
|
@@ -104,6 +120,6 @@ export function SessionBoundary<TSession, TClient>({
|
|
|
104
120
|
: createElement(
|
|
105
121
|
DraftSessionContext.Provider,
|
|
106
122
|
{ value: current },
|
|
107
|
-
createElement(Generation<TSession, TClient>, { children, connect, key: id, liveCoalesceWindow, recheck, session }),
|
|
123
|
+
createElement(Generation<TSession, TClient>, { children, connect, identity: id, key: id, liveCoalesceWindow, recheck, session }),
|
|
108
124
|
);
|
|
109
125
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Predicate } from "effect";
|
|
2
|
+
import { type LiveBound, LiveConnection } from "@shivaedev/contract/automaticLive.ts";
|
|
3
|
+
|
|
4
|
+
function isLiveBound(value: unknown): value is LiveBound {
|
|
5
|
+
return Predicate.hasProperty(value, LiveConnection);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function liveBindings(client: unknown): readonly LiveBound[] {
|
|
9
|
+
const values = typeof client === "object" && client !== null ? [client, ...Object.values(client)] : [client];
|
|
10
|
+
return values.filter(isLiveBound);
|
|
11
|
+
}
|
|
@@ -10,8 +10,8 @@ export function registerSessionRecheck(registry: AtomRegistry.AtomRegistry, rech
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export async function recheckUnauthorized(registry: AtomRegistry.AtomRegistry, error: unknown): Promise<void> {
|
|
14
|
-
if (Predicate.isTagged(error, "Unauthorized")) {
|
|
13
|
+
export async function recheckUnauthorized(registry: AtomRegistry.AtomRegistry, error: unknown, connection = false): Promise<void> {
|
|
14
|
+
if (Predicate.isTagged(error, "Unauthorized") || (connection && Predicate.isTagged(error, "RpcClientError"))) {
|
|
15
15
|
await rechecks.get(registry)?.();
|
|
16
16
|
}
|
|
17
17
|
}
|