@shivaedev/react 0.20.0 → 0.21.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 +38 -1
- package/dist/client/authQuery.d.ts +5 -0
- package/dist/client/authQuery.d.ts.map +1 -0
- package/dist/client/authQuery.js +10 -0
- package/dist/client/authQuery.js.map +1 -0
- package/dist/client/sessionBoundary.d.ts.map +1 -1
- package/dist/client/sessionBoundary.js +4 -2
- package/dist/client/sessionBoundary.js.map +1 -1
- package/dist/client/useNow.d.ts +5 -0
- package/dist/client/useNow.d.ts.map +1 -0
- package/dist/client/useNow.js +20 -0
- package/dist/client/useNow.js.map +1 -0
- package/dist/client/useSignOut.d.ts +18 -0
- package/dist/client/useSignOut.d.ts.map +1 -0
- package/dist/client/useSignOut.js +26 -0
- package/dist/client/useSignOut.js.map +1 -0
- package/dist/internal/useLatestCallback.d.ts +2 -0
- package/dist/internal/useLatestCallback.d.ts.map +1 -0
- package/dist/internal/useLatestCallback.js +9 -0
- package/dist/internal/useLatestCallback.js.map +1 -0
- package/dist/permissionGuard.d.ts +5 -5
- package/dist/permissionGuard.d.ts.map +1 -1
- package/dist/permissionGuard.js +3 -2
- package/dist/permissionGuard.js.map +1 -1
- package/dist/signInSearch.d.ts +8 -0
- package/dist/signInSearch.d.ts.map +1 -0
- package/dist/signInSearch.js +5 -0
- package/dist/signInSearch.js.map +1 -0
- package/package.json +11 -10
- package/src/client/authQuery.ts +17 -0
- package/src/client/sessionBoundary.ts +4 -2
- package/src/client/useNow.ts +26 -0
- package/src/client/useSignOut.ts +33 -0
- package/src/internal/useLatestCallback.ts +9 -0
- package/src/permissionGuard.ts +9 -9
- package/src/signInSearch.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0 - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `@shivaedev/contract/bind.ts` builds a bound HTTP RPC client and atom runtime from a contract and URL, with an inferred request transform for cookies or bearer tokens.
|
|
8
|
+
- `@shivaedev/react/client/useNow.ts` exports `useNow(interval, runtime?)` to render the runtime's Effect clock and refresh on the requested interval, including with TestClock.
|
|
9
|
+
- `useSignOut` coordinates explicit sign-out with TanStack Router so `permissionGuard` and `signInSearch` omit the return target during the following session refresh.
|
|
10
|
+
- `authQuery` turns Better Auth reads into input-keyed native query atoms, preserving typed auth errors, mapping rejected promises to `AuthActionUnavailable`, and aborting pending requests when their readers unmount.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Session boundaries deliver the latest committed `recheck` to query, action and save hooks without repeating a refused result when the callback changes identity.
|
|
15
|
+
|
|
3
16
|
## 0.20.0 - 2026-10-27
|
|
4
17
|
|
|
5
18
|
### Added
|
package/README.md
CHANGED
|
@@ -32,6 +32,19 @@ A **session generation** is the string returned by `identify(session)`. `Session
|
|
|
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
|
|
|
35
|
+
### Read the current time
|
|
36
|
+
|
|
37
|
+
`useNow(interval, runtime?)` from `@shivaedev/react/client/useNow.ts` returns epoch milliseconds from Effect's `Clock` and re-renders at the given interval in milliseconds. Pass the component's atom runtime to use its clock:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { useNow } from "@shivaedev/react/client/useNow.ts";
|
|
41
|
+
|
|
42
|
+
const now = useNow(60_000, client.runtime);
|
|
43
|
+
const minutesAgo = Math.floor((now - savedAt) / 60_000);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Without a runtime it uses a native empty atom runtime. Stories supply a runtime with their TestClock service so advancing that clock updates the rendered time.
|
|
47
|
+
|
|
35
48
|
### 1. Once per application: own the registry
|
|
36
49
|
|
|
37
50
|
For a screen without a session boundary, wrap it in the native provider:
|
|
@@ -384,6 +397,30 @@ function SignInButton({ email, password }: { email: string; password: string })
|
|
|
384
397
|
|
|
385
398
|
The example uses `authAction`, `useAction`, `effect/reactivity/AsyncResult` and `effect/Option`, with an application-owned `authClient`. Returned auth errors keep their fields in the typed failure channel. Rejected promises become `AuthActionUnavailable` with the original rejection in `cause`. The application chooses error text and what happens after success.
|
|
386
399
|
|
|
400
|
+
`authQuery` from `@shivaedev/react/client/authQuery.ts` creates an input-keyed query atom family for Better Auth reads. Render it with `useQuery`, just like a contract query atom:
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
const authSession = authQuery((disableCookieCache: boolean, signal) =>
|
|
404
|
+
authClient.getSession({ query: { disableCookieCache }, fetchOptions: { signal } }),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
function Account() {
|
|
408
|
+
const query = useQuery(authSession(true));
|
|
409
|
+
const failure = AsyncResult.error(query.result);
|
|
410
|
+
return (
|
|
411
|
+
<>
|
|
412
|
+
{query.pending && <p>Loading…</p>}
|
|
413
|
+
{Option.isSome(query.data) && <p>{query.data.value?.user.name ?? "Signed out"}</p>}
|
|
414
|
+
{Option.isSome(failure) && <p role="alert">Could not read the session</p>}
|
|
415
|
+
</>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The example uses the application's Better Auth client. Reads preserve returned auth errors and use the same `AuthActionUnavailable` failure as actions for rejected promises. Releasing a pending read interrupts its Effect and aborts the supplied signal.
|
|
421
|
+
|
|
422
|
+
For an explicit sign-out in a TanStack route, use `useSignOut(signOut, { signIn: "/sign-in" })` from `@shivaedev/react/client/useSignOut.ts` in place of `useAction`. It exposes the same state and a `dispatch()` handler. The hook records sign-out intent before the request and navigates to sign-in after success. `permissionGuard` and `signInSearch(location)` omit `returnTo` for that intent, including when the auth owner's session refresh reruns guards first. Use `signInSearch` for module sign-in links too; ordinary visits retain the full requested location. Applications do not need history markers or Better Auth completion timing hooks.
|
|
423
|
+
|
|
387
424
|
### Commands with a pending display value
|
|
388
425
|
|
|
389
426
|
Use `usePendingCommand` for a command acting on a query row, such as a list toggle. Give each row a stable action atom with the existing bound command, so the command's declared invalidation keys still drive refetches:
|
|
@@ -816,7 +853,7 @@ import { act, createElement } from "react";
|
|
|
816
853
|
import { expect } from "vitest";
|
|
817
854
|
import { bind } from "@shivaedev/contract/bind.ts";
|
|
818
855
|
import { contract } from "@shivaedev/contract/contract.ts";
|
|
819
|
-
import { collection } from "@shivaedev/
|
|
856
|
+
import { collection } from "@shivaedev/changes/keys.ts";
|
|
820
857
|
import { command, query } from "@shivaedev/contract/operation.ts";
|
|
821
858
|
import { useAction, useQuery } from "@shivaedev/react/client/resultState.ts";
|
|
822
859
|
import { eventually, expectNoReactWarnings, render } from "@shivaedev/react/client/testing.ts";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
2
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import { type AuthActionResult, AuthActionUnavailable } from "./authAction.ts";
|
|
4
|
+
export declare function authQuery<TInput, A, E extends object = never>(run: (input: TInput, signal: AbortSignal) => Promise<AuthActionResult<A, E>>): (input: TInput) => Atom.Atom<AsyncResult.AsyncResult<A, E | AuthActionUnavailable>>;
|
|
5
|
+
//# sourceMappingURL=authQuery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authQuery.d.ts","sourceRoot":"","sources":["../../src/client/authQuery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAC/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,wBAAgB,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,KAAK,EAC5D,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAC1E,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,qBAAqB,CAAC,CAAC,CASrF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import { AuthActionUnavailable } from "./authAction.js";
|
|
4
|
+
export function authQuery(run) {
|
|
5
|
+
return Atom.family((input) => Atom.make(Effect.tryPromise({
|
|
6
|
+
catch: (cause) => new AuthActionUnavailable({ cause }),
|
|
7
|
+
try: (signal) => run(input, signal),
|
|
8
|
+
}).pipe(Effect.flatMap((result) => (result.error === null ? Effect.succeed(result.data) : Effect.fail(result.error))))));
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=authQuery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authQuery.js","sourceRoot":"","sources":["../../src/client/authQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAC/C,OAAO,EAAyB,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,MAAM,UAAU,SAAS,CACxB,GAA4E;IAE5E,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,KAAa,EAAE,EAAE,CACpC,IAAI,CAAC,IAAI,CACR,MAAM,CAAC,UAAU,CAAC;QACjB,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,qBAAqB,CAAC,EAAE,KAAK,EAAE,CAAC;QACtD,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC;KACnC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CACtH,CACD,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;AAMhH,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;AAqED,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"}
|
|
@@ -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/form/client/react.ts";
|
|
5
5
|
import { registerSessionRecheck } from "#internal/sessionRecheck.ts";
|
|
6
|
+
import { useLatestCallback } from "#internal/useLatestCallback.ts";
|
|
6
7
|
import { mountLive } from "./mountLive.js";
|
|
7
8
|
import { useDraftOwner } from "./useDraftOwner.js";
|
|
8
9
|
const RecheckContext = createContext(() => undefined);
|
|
@@ -40,9 +41,10 @@ function hidden(connect) {
|
|
|
40
41
|
return owned;
|
|
41
42
|
}
|
|
42
43
|
function Generation({ session, connect, recheck, children, liveCoalesceWindow }) {
|
|
44
|
+
const recheckLatest = useLatestCallback(recheck);
|
|
43
45
|
const [open] = useState(() => (registry) => connect(session, registry));
|
|
44
46
|
const [owned, own] = useState(() => owning(open));
|
|
45
|
-
useEffect(() => registerSessionRecheck(owned.registry,
|
|
47
|
+
useEffect(() => registerSessionRecheck(owned.registry, recheckLatest), [owned, recheckLatest]);
|
|
46
48
|
useEffect(() => {
|
|
47
49
|
owned.mounted += 1;
|
|
48
50
|
const stopLive = mountLive(owned.registry, owned.client, liveCoalesceWindow);
|
|
@@ -58,7 +60,7 @@ function Generation({ session, connect, recheck, children, liveCoalesceWindow })
|
|
|
58
60
|
});
|
|
59
61
|
};
|
|
60
62
|
}, [owned, liveCoalesceWindow, open]);
|
|
61
|
-
return createElement(RecheckContext.Provider, { value:
|
|
63
|
+
return createElement(RecheckContext.Provider, { value: recheckLatest }, createElement(RegistryContext.Provider, { value: owned.registry }, children(owned.client)));
|
|
62
64
|
}
|
|
63
65
|
export function SessionBoundary({ session, liveCoalesceWindow, identify, connect, recheck, signedOut, children, }) {
|
|
64
66
|
const id = session === undefined ? undefined : identify(session);
|
|
@@ -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,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;IACjF,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC;IAC9D,IAAI,MAA+C,CAAC;IACpD,OAAO;QACN,IAAI,MAAM;YACT,MAAM,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxC,OAAO,MAAM,CAAC,KAAK,CAAC;QACrB,CAAC;QACD,OAAO,EAAE,CAAC;QACV,QAAQ;KACR,CAAC;AACH,CAAC;AACD,uHAAuH;AACvH,SAAS,MAAM,CAAU,OAAyD;IACjF,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,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;AAQD,SAAS,UAAU,CAAoB,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAsC;IACrI,MAAM,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,QAAmC,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IACnG,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAClD,SAAS,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC,KAAK,CAAC,QAAQ,EAAE,
|
|
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":"useNow.d.ts","sourceRoot":"","sources":["../../src/client/useNow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAI/C,KAAK,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAgBnE,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,YAA6B,GAAG,MAAM,CAEvF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useAtomSuspense } from "@effect/atom-react";
|
|
2
|
+
import { Clock, Effect, Layer } from "effect";
|
|
3
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
5
|
+
const defaultRuntime = Atom.runtime(Layer.empty);
|
|
6
|
+
function ticking(runtime, interval) {
|
|
7
|
+
return runtime.atom((get) => Effect.gen(function* () {
|
|
8
|
+
const update = Effect.gen(function* () {
|
|
9
|
+
const time = yield* Clock.currentTimeMillis;
|
|
10
|
+
get.setSelf(AsyncResult.success(time));
|
|
11
|
+
});
|
|
12
|
+
yield* Effect.forkScoped(Effect.forever(Effect.sleep(interval).pipe(Effect.andThen(update))));
|
|
13
|
+
return yield* Clock.currentTimeMillis;
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
const nowAtom = Atom.family((runtime) => Atom.family((interval) => ticking(runtime, interval)));
|
|
17
|
+
export function useNow(interval, runtime = defaultRuntime) {
|
|
18
|
+
return useAtomSuspense(nowAtom(runtime)(interval)).value;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=useNow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNow.js","sourceRoot":"","sources":["../../src/client/useNow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAE/C,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAIjD,SAAS,OAAO,CAAO,OAA6C,EAAE,QAAgB;IACrF,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAC3B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;YAC5C,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9F,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;IACvC,CAAC,CAAC,CACF,CAAC;AACH,CAAC;AACD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,OAAqB,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,QAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEtH,MAAM,UAAU,MAAM,CAAC,QAAgB,EAAE,OAAO,GAAiB,cAAc;IAC9E,OAAO,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
2
|
+
import type * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
declare module "@tanstack/react-router" {
|
|
4
|
+
interface HistoryState {
|
|
5
|
+
readonly __platformSignOut?: boolean;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export declare function useSignOut<A, E>(action: Atom.AtomResultFn<void, A, E>, options: {
|
|
9
|
+
readonly signIn: string;
|
|
10
|
+
}): {
|
|
11
|
+
cause: import("effect/Option").Option<import("effect/Cause").Cause<E>>;
|
|
12
|
+
data: import("effect/Option").Option<A>;
|
|
13
|
+
pending: boolean;
|
|
14
|
+
refreshing: boolean;
|
|
15
|
+
result: AsyncResult.AsyncResult<A, E>;
|
|
16
|
+
dispatch: () => void;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=useSignOut.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSignOut.d.ts","sourceRoot":"","sources":["../../src/client/useSignOut.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAIpD,OAAO,QAAQ,wBAAwB,CAAC;IACvC,UAAU,YAAY;QACrB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;KACrC;CACD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;;;;;;;EAoB3G"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useRouter } from "@tanstack/react-router";
|
|
2
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useAction } from "./resultState.js";
|
|
5
|
+
export function useSignOut(action, options) {
|
|
6
|
+
const router = useRouter({ warn: false });
|
|
7
|
+
const state = useAction(action);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (router && AsyncResult.isSuccess(state.result) && !state.pending) {
|
|
10
|
+
void router.navigate({ replace: true, state: { __platformSignOut: true }, to: options.signIn });
|
|
11
|
+
}
|
|
12
|
+
if (router && AsyncResult.isFailure(state.result) && !state.pending) {
|
|
13
|
+
router.history.replace(router.history.location.href, { ...router.history.location.state, __platformSignOut: false });
|
|
14
|
+
}
|
|
15
|
+
}, [router, state.result, state.pending, options.signIn]);
|
|
16
|
+
return {
|
|
17
|
+
...state,
|
|
18
|
+
dispatch: () => {
|
|
19
|
+
if (router) {
|
|
20
|
+
router.history.replace(router.history.location.href, { ...router.history.location.state, __platformSignOut: true });
|
|
21
|
+
}
|
|
22
|
+
state.dispatch();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=useSignOut.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSignOut.js","sourceRoot":"","sources":["../../src/client/useSignOut.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAQ7C,MAAM,UAAU,UAAU,CAAO,MAAqC,EAAE,OAAoC;IAC3G,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAChC,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,MAAM,IAAI,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;YACrE,KAAK,MAAM,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACjG,CAAC;QACD,IAAI,MAAM,IAAI,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;YACrE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC,CAAC;QACtH,CAAC;IACF,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1D,OAAO;QACN,GAAG,KAAK;QACR,QAAQ,EAAE,GAAG,EAAE;YACd,IAAI,MAAM,EAAE,CAAC;gBACZ,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC;YACrH,CAAC;YACD,KAAK,CAAC,QAAQ,EAAE,CAAC;QAClB,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLatestCallback.d.ts","sourceRoot":"","sources":["../../src/internal/useLatestCallback.ts"],"names":[],"mappings":"AAEA,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAMlE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
2
|
+
export function useLatestCallback(callback) {
|
|
3
|
+
const latestRef = useRef(callback);
|
|
4
|
+
useLayoutEffect(() => {
|
|
5
|
+
latestRef.current = callback;
|
|
6
|
+
}, [callback]);
|
|
7
|
+
return useCallback(() => latestRef.current(), []);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=useLatestCallback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLatestCallback.js","sourceRoot":"","sources":["../../src/internal/useLatestCallback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE7D,MAAM,UAAU,iBAAiB,CAAC,QAAoB;IACrD,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IACnC,eAAe,CAAC,GAAG,EAAE;QACpB,SAAS,CAAC,OAAO,GAAG,QAAQ,CAAC;IAC9B,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IACf,OAAO,WAAW,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
2
2
|
import { type PermissionSession } from "./permissions.ts";
|
|
3
|
+
import { type SignInLocation } from "./signInSearch.ts";
|
|
3
4
|
declare module "@tanstack/react-router" {
|
|
4
5
|
interface StaticDataRouteOption {
|
|
5
6
|
readonly permission?: Permission;
|
|
@@ -9,14 +10,13 @@ export interface PermissionGuardOptions {
|
|
|
9
10
|
readonly session: () => PermissionSession | null | undefined;
|
|
10
11
|
readonly signIn: string;
|
|
11
12
|
}
|
|
12
|
-
export
|
|
13
|
-
readonly location:
|
|
14
|
-
readonly href: string;
|
|
15
|
-
};
|
|
13
|
+
export interface PermissionGuardContext {
|
|
14
|
+
readonly location: SignInLocation;
|
|
16
15
|
readonly matches: readonly {
|
|
17
16
|
readonly staticData: {
|
|
18
17
|
readonly permission?: Permission;
|
|
19
18
|
};
|
|
20
19
|
}[];
|
|
21
|
-
}
|
|
20
|
+
}
|
|
21
|
+
export declare function permissionGuard({ session, signIn }: PermissionGuardOptions): (context: PermissionGuardContext) => void;
|
|
22
22
|
//# sourceMappingURL=permissionGuard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissionGuard.d.ts","sourceRoot":"","sources":["../src/permissionGuard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EAAO,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"permissionGuard.d.ts","sourceRoot":"","sources":["../src/permissionGuard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EAAO,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,KAAK,cAAc,EAAgB,MAAM,mBAAmB,CAAC;AAEtE,OAAO,QAAQ,wBAAwB,CAAC;IACvC,UAAU,qBAAqB;QAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;KACjC;CACD;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAC;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS;QAAE,QAAQ,CAAC,UAAU,EAAE;YAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;SAAE,CAAA;KAAE,EAAE,CAAC;CAC3F;AAED,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,sBAAsB,GAAG,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,CAgBtH"}
|
package/dist/permissionGuard.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { redirect } from "@tanstack/react-router";
|
|
2
2
|
import { Forbidden } from "@shivaedev/errors/taxonomy.ts";
|
|
3
3
|
import { can } from "./permissions.js";
|
|
4
|
+
import { signInSearch } from "./signInSearch.js";
|
|
4
5
|
export function permissionGuard({ session, signIn }) {
|
|
5
|
-
return ({ matches, location
|
|
6
|
+
return ({ matches, location }) => {
|
|
6
7
|
const required = matches.flatMap((match) => (match.staticData.permission === undefined ? [] : [match.staticData.permission]));
|
|
7
8
|
if (required.length === 0) {
|
|
8
9
|
return;
|
|
9
10
|
}
|
|
10
11
|
const current = session();
|
|
11
12
|
if (current === null || current === undefined) {
|
|
12
|
-
throw redirect({ search:
|
|
13
|
+
throw redirect({ search: signInSearch(location), to: signIn });
|
|
13
14
|
}
|
|
14
15
|
for (const permission of required) {
|
|
15
16
|
if (!can(current, permission)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissionGuard.js","sourceRoot":"","sources":["../src/permissionGuard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,GAAG,EAA0B,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"permissionGuard.js","sourceRoot":"","sources":["../src/permissionGuard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,GAAG,EAA0B,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAuB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAkBtE,MAAM,UAAU,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,EAA0B;IAC1E,OAAO,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE;QAChC,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC9H,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO;QACR,CAAC;QACD,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC;QAC1B,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/C,MAAM,QAAQ,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QAChE,CAAC;QACD,KAAK,MAAM,UAAU,IAAI,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;gBAC/B,MAAM,IAAI,SAAS,CAAC,EAAE,OAAO,EAAE,wBAAwB,UAAU,CAAC,IAAI,KAAK,UAAU,CAAC,QAAQ,GAAG,EAAE,CAAC,CAAC;YACtG,CAAC;QACF,CAAC;IACF,CAAC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signInSearch.d.ts","sourceRoot":"","sources":["../src/signInSearch.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,cAAc,GAAG;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAGrF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signInSearch.js","sourceRoot":"","sources":["../src/signInSearch.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,YAAY,CAAC,QAAwB;IACpD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC;IAC7B,OAAO,KAAK,KAAK,SAAS,IAAI,mBAAmB,IAAI,KAAK,IAAI,KAAK,CAAC,iBAAiB,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;AACnI,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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.19.0",
|
|
51
51
|
"@shivaedev/errors": "0.1.0",
|
|
52
52
|
"@shivaedev/form": "0.7.0",
|
|
53
53
|
"@shivaedev/test": "0.10.0"
|
|
@@ -57,21 +57,22 @@
|
|
|
57
57
|
"@effect/platform-node": "4.0.5",
|
|
58
58
|
"@effect/sql-pg": "4.0.5",
|
|
59
59
|
"@effect/sql-sqlite-node": "4.0.5",
|
|
60
|
-
"@shivaedev/
|
|
61
|
-
"@shivaedev/
|
|
60
|
+
"@shivaedev/changes": "0.5.0",
|
|
61
|
+
"@shivaedev/identity": "0.3.0",
|
|
62
|
+
"@shivaedev/jobs": "0.22.0",
|
|
62
63
|
"@shivaedev/local-postgres": "0.7.0",
|
|
63
|
-
"@shivaedev/quality": "0.
|
|
64
|
-
"@shivaedev/server": "0.
|
|
64
|
+
"@shivaedev/quality": "0.43.0",
|
|
65
|
+
"@shivaedev/server": "0.3.0",
|
|
65
66
|
"@shivaedev/service": "0.5.0",
|
|
66
|
-
"@shivaedev/sql": "0.
|
|
67
|
-
"@shivaedev/test-sql": "0.10.
|
|
68
|
-
"@shivaedev/test-story": "0.
|
|
67
|
+
"@shivaedev/sql": "0.24.0",
|
|
68
|
+
"@shivaedev/test-sql": "0.10.2",
|
|
69
|
+
"@shivaedev/test-story": "0.22.0",
|
|
69
70
|
"@tanstack/react-router": "1.170.45",
|
|
70
71
|
"@types/node": "24.19.2",
|
|
71
72
|
"@types/react": "19.3.0",
|
|
72
73
|
"@types/react-dom": "19.3.0",
|
|
73
74
|
"effect": "4.0.5",
|
|
74
|
-
"happy-dom": "21.0.
|
|
75
|
+
"happy-dom": "21.0.4",
|
|
75
76
|
"react": "19.3.1",
|
|
76
77
|
"react-dom": "19.3.1",
|
|
77
78
|
"typescript": "7.0.2",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
3
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
4
|
+
import { type AuthActionResult, AuthActionUnavailable } from "./authAction.ts";
|
|
5
|
+
|
|
6
|
+
export function authQuery<TInput, A, E extends object = never>(
|
|
7
|
+
run: (input: TInput, signal: AbortSignal) => Promise<AuthActionResult<A, E>>,
|
|
8
|
+
): (input: TInput) => Atom.Atom<AsyncResult.AsyncResult<A, E | AuthActionUnavailable>> {
|
|
9
|
+
return Atom.family((input: TInput) =>
|
|
10
|
+
Atom.make(
|
|
11
|
+
Effect.tryPromise({
|
|
12
|
+
catch: (cause) => new AuthActionUnavailable({ cause }),
|
|
13
|
+
try: (signal) => run(input, signal),
|
|
14
|
+
}).pipe(Effect.flatMap((result) => (result.error === null ? Effect.succeed(result.data) : Effect.fail(result.error)))),
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -4,6 +4,7 @@ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
|
4
4
|
import { createContext, createElement, Fragment, type ReactNode, useContext, useEffect, useState } from "react";
|
|
5
5
|
import { DraftSessionContext } from "@shivaedev/form/client/react.ts";
|
|
6
6
|
import { registerSessionRecheck } from "#internal/sessionRecheck.ts";
|
|
7
|
+
import { useLatestCallback } from "#internal/useLatestCallback.ts";
|
|
7
8
|
import { mountLive } from "./mountLive.ts";
|
|
8
9
|
import { useDraftOwner } from "./useDraftOwner.ts";
|
|
9
10
|
export interface SessionBoundaryProps<TSession, TClient> {
|
|
@@ -62,9 +63,10 @@ interface GenerationProps<TSession, TClient> {
|
|
|
62
63
|
readonly session: TSession;
|
|
63
64
|
}
|
|
64
65
|
function Generation<TSession, TClient>({ session, connect, recheck, children, liveCoalesceWindow }: GenerationProps<TSession, TClient>): ReactNode {
|
|
66
|
+
const recheckLatest = useLatestCallback(recheck);
|
|
65
67
|
const [open] = useState(() => (registry: AtomRegistry.AtomRegistry) => connect(session, registry));
|
|
66
68
|
const [owned, own] = useState(() => owning(open));
|
|
67
|
-
useEffect(() => registerSessionRecheck(owned.registry,
|
|
69
|
+
useEffect(() => registerSessionRecheck(owned.registry, recheckLatest), [owned, recheckLatest]);
|
|
68
70
|
useEffect(() => {
|
|
69
71
|
owned.mounted += 1;
|
|
70
72
|
const stopLive = mountLive(owned.registry, owned.client, liveCoalesceWindow);
|
|
@@ -82,7 +84,7 @@ function Generation<TSession, TClient>({ session, connect, recheck, children, li
|
|
|
82
84
|
}, [owned, liveCoalesceWindow, open]);
|
|
83
85
|
return createElement(
|
|
84
86
|
RecheckContext.Provider,
|
|
85
|
-
{ value:
|
|
87
|
+
{ value: recheckLatest },
|
|
86
88
|
createElement(RegistryContext.Provider, { value: owned.registry }, children(owned.client)),
|
|
87
89
|
);
|
|
88
90
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useAtomSuspense } from "@effect/atom-react";
|
|
2
|
+
import { Clock, Effect, Layer } from "effect";
|
|
3
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
5
|
+
|
|
6
|
+
const defaultRuntime = Atom.runtime(Layer.empty);
|
|
7
|
+
|
|
8
|
+
type ClockRuntime = Pick<Atom.AtomRuntime<never, unknown>, "atom">;
|
|
9
|
+
|
|
10
|
+
function ticking<R, E>(runtime: Pick<Atom.AtomRuntime<R, E>, "atom">, interval: number) {
|
|
11
|
+
return runtime.atom((get) =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const update = Effect.gen(function* () {
|
|
14
|
+
const time = yield* Clock.currentTimeMillis;
|
|
15
|
+
get.setSelf(AsyncResult.success(time));
|
|
16
|
+
});
|
|
17
|
+
yield* Effect.forkScoped(Effect.forever(Effect.sleep(interval).pipe(Effect.andThen(update))));
|
|
18
|
+
return yield* Clock.currentTimeMillis;
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const nowAtom = Atom.family((runtime: ClockRuntime) => Atom.family((interval: number) => ticking(runtime, interval)));
|
|
23
|
+
|
|
24
|
+
export function useNow(interval: number, runtime: ClockRuntime = defaultRuntime): number {
|
|
25
|
+
return useAtomSuspense(nowAtom(runtime)(interval)).value;
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useRouter } from "@tanstack/react-router";
|
|
2
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
3
|
+
import type * as Atom from "effect/reactivity/Atom";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { useAction } from "./resultState.ts";
|
|
6
|
+
|
|
7
|
+
declare module "@tanstack/react-router" {
|
|
8
|
+
interface HistoryState {
|
|
9
|
+
readonly __platformSignOut?: boolean;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useSignOut<A, E>(action: Atom.AtomResultFn<void, A, E>, options: { readonly signIn: string }) {
|
|
14
|
+
const router = useRouter({ warn: false });
|
|
15
|
+
const state = useAction(action);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (router && AsyncResult.isSuccess(state.result) && !state.pending) {
|
|
18
|
+
void router.navigate({ replace: true, state: { __platformSignOut: true }, to: options.signIn });
|
|
19
|
+
}
|
|
20
|
+
if (router && AsyncResult.isFailure(state.result) && !state.pending) {
|
|
21
|
+
router.history.replace(router.history.location.href, { ...router.history.location.state, __platformSignOut: false });
|
|
22
|
+
}
|
|
23
|
+
}, [router, state.result, state.pending, options.signIn]);
|
|
24
|
+
return {
|
|
25
|
+
...state,
|
|
26
|
+
dispatch: () => {
|
|
27
|
+
if (router) {
|
|
28
|
+
router.history.replace(router.history.location.href, { ...router.history.location.state, __platformSignOut: true });
|
|
29
|
+
}
|
|
30
|
+
state.dispatch();
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export function useLatestCallback(callback: () => void): () => void {
|
|
4
|
+
const latestRef = useRef(callback);
|
|
5
|
+
useLayoutEffect(() => {
|
|
6
|
+
latestRef.current = callback;
|
|
7
|
+
}, [callback]);
|
|
8
|
+
return useCallback(() => latestRef.current(), []);
|
|
9
|
+
}
|
package/src/permissionGuard.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { redirect } from "@tanstack/react-router";
|
|
|
2
2
|
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
3
3
|
import { Forbidden } from "@shivaedev/errors/taxonomy.ts";
|
|
4
4
|
import { can, type PermissionSession } from "./permissions.ts";
|
|
5
|
+
import { type SignInLocation, signInSearch } from "./signInSearch.ts";
|
|
5
6
|
|
|
6
7
|
declare module "@tanstack/react-router" {
|
|
7
8
|
interface StaticDataRouteOption {
|
|
@@ -14,21 +15,20 @@ export interface PermissionGuardOptions {
|
|
|
14
15
|
readonly signIn: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}): void => {
|
|
18
|
+
export interface PermissionGuardContext {
|
|
19
|
+
readonly location: SignInLocation;
|
|
20
|
+
readonly matches: readonly { readonly staticData: { readonly permission?: Permission } }[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function permissionGuard({ session, signIn }: PermissionGuardOptions): (context: PermissionGuardContext) => void {
|
|
24
|
+
return ({ matches, location }) => {
|
|
25
25
|
const required = matches.flatMap((match) => (match.staticData.permission === undefined ? [] : [match.staticData.permission]));
|
|
26
26
|
if (required.length === 0) {
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
const current = session();
|
|
30
30
|
if (current === null || current === undefined) {
|
|
31
|
-
throw redirect({ search:
|
|
31
|
+
throw redirect({ search: signInSearch(location), to: signIn });
|
|
32
32
|
}
|
|
33
33
|
for (const permission of required) {
|
|
34
34
|
if (!can(current, permission)) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SignInLocation {
|
|
2
|
+
readonly href: string;
|
|
3
|
+
readonly state?: object | undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function signInSearch(location: SignInLocation): { readonly returnTo?: string } {
|
|
7
|
+
const state = location.state;
|
|
8
|
+
return state !== undefined && "__platformSignOut" in state && state.__platformSignOut === true ? {} : { returnTo: location.href };
|
|
9
|
+
}
|