@shivaedev/react 0.0.0 → 0.18.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 +294 -0
- package/LICENSE +21 -0
- package/README.md +844 -1
- package/dist/actionTrace.d.ts +3 -0
- package/dist/actionTrace.d.ts.map +1 -0
- package/dist/actionTrace.js +5 -0
- package/dist/actionTrace.js.map +1 -0
- package/dist/client/QueryView.d.ts +10 -0
- package/dist/client/QueryView.d.ts.map +1 -0
- package/dist/client/QueryView.js +13 -0
- package/dist/client/QueryView.js.map +1 -0
- package/dist/client/actionCompletion.d.ts +12 -0
- package/dist/client/actionCompletion.d.ts.map +1 -0
- package/dist/client/actionCompletion.js +68 -0
- package/dist/client/actionCompletion.js.map +1 -0
- package/dist/client/authAction.d.ts +15 -0
- package/dist/client/authAction.d.ts.map +1 -0
- package/dist/client/authAction.js +11 -0
- package/dist/client/authAction.js.map +1 -0
- package/dist/client/create.d.ts +21 -0
- package/dist/client/create.d.ts.map +1 -0
- package/dist/client/create.js +35 -0
- package/dist/client/create.js.map +1 -0
- package/dist/client/editor.d.ts +33 -0
- package/dist/client/editor.d.ts.map +1 -0
- package/dist/client/editor.js +50 -0
- package/dist/client/editor.js.map +1 -0
- package/dist/client/mountLive.d.ts +4 -0
- package/dist/client/mountLive.d.ts.map +1 -0
- package/dist/client/mountLive.js +46 -0
- package/dist/client/mountLive.js.map +1 -0
- package/dist/client/pendingCommand.d.ts +13 -0
- package/dist/client/pendingCommand.d.ts.map +1 -0
- package/dist/client/pendingCommand.js +30 -0
- package/dist/client/pendingCommand.js.map +1 -0
- package/dist/client/resultState.d.ts +32 -0
- package/dist/client/resultState.d.ts.map +1 -0
- package/dist/client/resultState.js +62 -0
- package/dist/client/resultState.js.map +1 -0
- package/dist/client/saveState.d.ts +11 -0
- package/dist/client/saveState.d.ts.map +1 -0
- package/dist/client/saveState.js +37 -0
- package/dist/client/saveState.js.map +1 -0
- package/dist/client/sessionBoundary.d.ts +15 -0
- package/dist/client/sessionBoundary.d.ts.map +1 -0
- package/dist/client/sessionBoundary.js +70 -0
- package/dist/client/sessionBoundary.js.map +1 -0
- package/dist/client/testing.d.ts +28 -0
- package/dist/client/testing.d.ts.map +1 -0
- package/dist/client/testing.js +108 -0
- package/dist/client/testing.js.map +1 -0
- package/dist/client/useDraftOwner.d.ts +3 -0
- package/dist/client/useDraftOwner.d.ts.map +1 -0
- package/dist/client/useDraftOwner.js +36 -0
- package/dist/client/useDraftOwner.js.map +1 -0
- package/dist/client/useLoaderQuery.d.ts +8 -0
- package/dist/client/useLoaderQuery.d.ts.map +1 -0
- package/dist/client/useLoaderQuery.js +10 -0
- package/dist/client/useLoaderQuery.js.map +1 -0
- package/dist/fieldRejection.d.ts +25 -0
- package/dist/fieldRejection.d.ts.map +1 -0
- package/dist/fieldRejection.js +21 -0
- package/dist/fieldRejection.js.map +1 -0
- package/dist/internal/NoticeContext.d.ts +7 -0
- package/dist/internal/NoticeContext.d.ts.map +1 -0
- package/dist/internal/NoticeContext.js +3 -0
- package/dist/internal/NoticeContext.js.map +1 -0
- package/dist/internal/loadedQuery.d.ts +16 -0
- package/dist/internal/loadedQuery.d.ts.map +1 -0
- package/dist/internal/loadedQuery.js +11 -0
- package/dist/internal/loadedQuery.js.map +1 -0
- package/dist/internal/noticeEntry.d.ts +21 -0
- package/dist/internal/noticeEntry.d.ts.map +1 -0
- package/dist/internal/noticeEntry.js +45 -0
- package/dist/internal/noticeEntry.js.map +1 -0
- package/dist/internal/noticeStore.d.ts +12 -0
- package/dist/internal/noticeStore.d.ts.map +1 -0
- package/dist/internal/noticeStore.js +37 -0
- package/dist/internal/noticeStore.js.map +1 -0
- package/dist/internal/queryHandoff.d.ts +9 -0
- package/dist/internal/queryHandoff.d.ts.map +1 -0
- package/dist/internal/queryHandoff.js +50 -0
- package/dist/internal/queryHandoff.js.map +1 -0
- package/dist/internal/sessionRecheck.d.ts +4 -0
- package/dist/internal/sessionRecheck.d.ts.map +1 -0
- package/dist/internal/sessionRecheck.js +14 -0
- package/dist/internal/sessionRecheck.js.map +1 -0
- package/dist/internal/useLoaderHandoff.d.ts +4 -0
- package/dist/internal/useLoaderHandoff.d.ts.map +1 -0
- package/dist/internal/useLoaderHandoff.js +18 -0
- package/dist/internal/useLoaderHandoff.js.map +1 -0
- package/dist/internal/useNoticePublisher.d.ts +4 -0
- package/dist/internal/useNoticePublisher.d.ts.map +1 -0
- package/dist/internal/useNoticePublisher.js +8 -0
- package/dist/internal/useNoticePublisher.js.map +1 -0
- package/dist/internal/useQueryNotice.d.ts +8 -0
- package/dist/internal/useQueryNotice.d.ts.map +1 -0
- package/dist/internal/useQueryNotice.js +26 -0
- package/dist/internal/useQueryNotice.js.map +1 -0
- package/dist/internal/useSubmissionCallbacks.d.ts +11 -0
- package/dist/internal/useSubmissionCallbacks.d.ts.map +1 -0
- package/dist/internal/useSubmissionCallbacks.js +45 -0
- package/dist/internal/useSubmissionCallbacks.js.map +1 -0
- package/dist/loadQuery.d.ts +6 -0
- package/dist/loadQuery.d.ts.map +1 -0
- package/dist/loadQuery.js +29 -0
- package/dist/loadQuery.js.map +1 -0
- package/dist/notices.d.ts +32 -0
- package/dist/notices.d.ts.map +1 -0
- package/dist/notices.js +34 -0
- package/dist/notices.js.map +1 -0
- package/dist/queryRoute.d.ts +20 -0
- package/dist/queryRoute.d.ts.map +1 -0
- package/dist/queryRoute.js +20 -0
- package/dist/queryRoute.js.map +1 -0
- package/dist/resumeSignal.d.ts +6 -0
- package/dist/resumeSignal.d.ts.map +1 -0
- package/dist/resumeSignal.js +5 -0
- package/dist/resumeSignal.js.map +1 -0
- package/dist/submission.d.ts +19 -0
- package/dist/submission.d.ts.map +1 -0
- package/dist/submission.js +19 -0
- package/dist/submission.js.map +1 -0
- package/package.json +104 -4
- package/src/actionTrace.ts +5 -0
- package/src/client/QueryView.ts +27 -0
- package/src/client/actionCompletion.ts +82 -0
- package/src/client/authAction.ts +22 -0
- package/src/client/create.ts +55 -0
- package/src/client/editor.ts +89 -0
- package/src/client/mountLive.ts +57 -0
- package/src/client/pendingCommand.ts +42 -0
- package/src/client/resultState.ts +102 -0
- package/src/client/saveState.ts +54 -0
- package/src/client/sessionBoundary.ts +107 -0
- package/src/client/testing.ts +142 -0
- package/src/client/useDraftOwner.ts +42 -0
- package/src/client/useLoaderQuery.ts +16 -0
- package/src/fieldRejection.ts +51 -0
- package/src/internal/NoticeContext.ts +4 -0
- package/src/internal/loadedQuery.ts +18 -0
- package/src/internal/noticeEntry.ts +65 -0
- package/src/internal/noticeStore.ts +46 -0
- package/src/internal/queryHandoff.ts +60 -0
- package/src/internal/sessionRecheck.ts +17 -0
- package/src/internal/useLoaderHandoff.ts +20 -0
- package/src/internal/useNoticePublisher.ts +10 -0
- package/src/internal/useQueryNotice.ts +32 -0
- package/src/internal/useSubmissionCallbacks.ts +57 -0
- package/src/loadQuery.ts +34 -0
- package/src/notices.ts +65 -0
- package/src/queryRoute.ts +30 -0
- package/src/resumeSignal.ts +13 -0
- package/src/submission.ts +39 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Effect, type Schema } from "effect";
|
|
2
|
+
import type * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import { useInsertionEffect, useMemo, useRef } from "react";
|
|
4
|
+
import type { Decoded, Fields } from "@shivaedev/form/shape.ts";
|
|
5
|
+
import { type AtomServices, fieldRejectionOf, type RejectField } from "#fieldRejection.ts";
|
|
6
|
+
import type { NoticeFeedback } from "#notices.ts";
|
|
7
|
+
import { useNoticePublisher } from "./useNoticePublisher.ts";
|
|
8
|
+
|
|
9
|
+
export function useSubmissionCallbacks<TFields extends Fields, A, E, R, TRuntime, TRuntimeError>(
|
|
10
|
+
fields: Schema.Struct<TFields>,
|
|
11
|
+
save: (values: Decoded<TFields>) => Effect.Effect<A, E, AtomServices<R>>,
|
|
12
|
+
rejectField: RejectField<TFields, E> | undefined,
|
|
13
|
+
runtime: Atom.AtomRuntime<TRuntime, TRuntimeError>,
|
|
14
|
+
notices: NoticeFeedback<Decoded<TFields>, A, E, TRuntime> | undefined,
|
|
15
|
+
) {
|
|
16
|
+
const publish = useNoticePublisher(runtime);
|
|
17
|
+
const latestRef = useRef({ notices, rejectField, save });
|
|
18
|
+
useInsertionEffect(() => {
|
|
19
|
+
latestRef.current = { notices, rejectField, save };
|
|
20
|
+
}, [notices, rejectField, save]);
|
|
21
|
+
const callbacks = useMemo(() => {
|
|
22
|
+
const automatic = fieldRejectionOf(fields);
|
|
23
|
+
function rejected(error: E, values: Decoded<TFields>) {
|
|
24
|
+
return Effect.sync(() => {
|
|
25
|
+
if (callbacks.active) {
|
|
26
|
+
publish(latestRef.current.notices?.onFailure?.(error, values));
|
|
27
|
+
}
|
|
28
|
+
}).pipe(Effect.andThen(Effect.fail(error)));
|
|
29
|
+
}
|
|
30
|
+
function completed(value: A, values: Decoded<TFields>) {
|
|
31
|
+
return Effect.sync(() => {
|
|
32
|
+
if (callbacks.active) {
|
|
33
|
+
publish(latestRef.current.notices?.onSuccess?.(value, values));
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
active: false,
|
|
40
|
+
rejectField: (error: E) => (latestRef.current.rejectField ?? automatic)(error),
|
|
41
|
+
save: (values: Decoded<TFields>): Effect.Effect<A, E, AtomServices<R>> =>
|
|
42
|
+
Effect.suspend(() => latestRef.current.save(values)).pipe(
|
|
43
|
+
Effect.matchEffect({
|
|
44
|
+
onFailure: (error) => rejected(error, values),
|
|
45
|
+
onSuccess: (value) => completed(value, values),
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
}, [fields, publish]);
|
|
50
|
+
useInsertionEffect(() => {
|
|
51
|
+
callbacks.active = true;
|
|
52
|
+
return () => {
|
|
53
|
+
callbacks.active = false;
|
|
54
|
+
};
|
|
55
|
+
}, [callbacks]);
|
|
56
|
+
return callbacks;
|
|
57
|
+
}
|
package/src/loadQuery.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { LoaderFnContext } from "@tanstack/react-router";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import type * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
|
+
import type * as Atom from "effect/reactivity/Atom";
|
|
5
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
6
|
+
import { keepLoaderQuery } from "#internal/queryHandoff.ts";
|
|
7
|
+
import { recheckUnauthorized } from "#internal/sessionRecheck.ts";
|
|
8
|
+
|
|
9
|
+
export async function loadQuery<A, E>(
|
|
10
|
+
registry: AtomRegistry.AtomRegistry,
|
|
11
|
+
atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
|
|
12
|
+
{ abortController }: Pick<LoaderFnContext, "abortController">,
|
|
13
|
+
): Promise<A> {
|
|
14
|
+
const { signal } = abortController;
|
|
15
|
+
const handoff = keepLoaderQuery(registry, atom, signal);
|
|
16
|
+
try {
|
|
17
|
+
const result = await Effect.runPromise(Effect.result(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true })), { signal });
|
|
18
|
+
if (result._tag === "Failure") {
|
|
19
|
+
await recheckUnauthorized(registry, result.failure);
|
|
20
|
+
throw result.failure;
|
|
21
|
+
}
|
|
22
|
+
return result.success;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
handoff.release();
|
|
25
|
+
if (signal.aborted) {
|
|
26
|
+
const canceled = new Error("Query loading canceled", { cause: error });
|
|
27
|
+
canceled.name = "AbortError";
|
|
28
|
+
throw canceled;
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
} finally {
|
|
32
|
+
handoff.complete();
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/notices.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { RegistryContext, useAtomValue } from "@effect/atom-react";
|
|
2
|
+
import type { Duration, Effect } from "effect";
|
|
3
|
+
import type * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
|
+
import { createElement, Fragment, type ReactNode, useContext, useLayoutEffect, useMemo } from "react";
|
|
5
|
+
import type { AtomServices } from "#fieldRejection.ts";
|
|
6
|
+
import { NoticeContext } from "#internal/NoticeContext.ts";
|
|
7
|
+
import type { NoticeEntry } from "#internal/noticeEntry.ts";
|
|
8
|
+
import { noticeStore } from "#internal/noticeStore.ts";
|
|
9
|
+
|
|
10
|
+
export interface NoticeDescriptor<R = never> {
|
|
11
|
+
readonly action?:
|
|
12
|
+
| {
|
|
13
|
+
readonly label: ReactNode;
|
|
14
|
+
readonly run: Effect.Effect<unknown, unknown, AtomServices<R>>;
|
|
15
|
+
}
|
|
16
|
+
| undefined;
|
|
17
|
+
readonly expiresIn?: Duration.Input | undefined;
|
|
18
|
+
readonly message: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export interface Notice {
|
|
21
|
+
readonly action?:
|
|
22
|
+
| {
|
|
23
|
+
readonly label: ReactNode;
|
|
24
|
+
readonly pending: boolean;
|
|
25
|
+
readonly result: AsyncResult.AsyncResult<unknown, unknown>;
|
|
26
|
+
readonly run: () => void;
|
|
27
|
+
}
|
|
28
|
+
| undefined;
|
|
29
|
+
readonly dismiss: () => void;
|
|
30
|
+
readonly id: number;
|
|
31
|
+
readonly message: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
export interface NoticeFeedback<TInput, A, E, R = never> {
|
|
34
|
+
readonly onFailure?: (error: E, input: TInput) => NoticeDescriptor<R> | undefined;
|
|
35
|
+
readonly onSuccess?: (value: A, input: TInput) => NoticeDescriptor<R> | undefined;
|
|
36
|
+
}
|
|
37
|
+
function Entry({ entry, render }: { readonly entry: NoticeEntry; readonly render: (notice: Notice) => ReactNode }) {
|
|
38
|
+
const result = useAtomValue(entry.status);
|
|
39
|
+
return render({
|
|
40
|
+
action: entry.action === undefined ? undefined : { ...entry.action, pending: result.waiting, result },
|
|
41
|
+
dismiss: entry.dismiss,
|
|
42
|
+
id: entry.id,
|
|
43
|
+
message: entry.message,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function Entries({ store, render }: { readonly store: ReturnType<typeof noticeStore>; readonly render: (notice: Notice) => ReactNode }) {
|
|
47
|
+
const entries = useAtomValue(store.entries);
|
|
48
|
+
return entries.map((entry) => createElement(Entry, { entry, key: entry.id, render }));
|
|
49
|
+
}
|
|
50
|
+
export function NoticeHost({ children, render }: { readonly children: ReactNode; readonly render: (notice: Notice) => ReactNode }): ReactNode {
|
|
51
|
+
const registry = useContext(RegistryContext);
|
|
52
|
+
const store = useMemo(() => noticeStore(registry), [registry]);
|
|
53
|
+
useLayoutEffect(() => {
|
|
54
|
+
store.active = true;
|
|
55
|
+
return () => {
|
|
56
|
+
store.active = false;
|
|
57
|
+
queueMicrotask(() => {
|
|
58
|
+
if (!store.active) {
|
|
59
|
+
store.close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}, [store]);
|
|
64
|
+
return createElement(NoticeContext.Provider, { value: store }, createElement(Fragment, null, children, createElement(Entries, { render, store })));
|
|
65
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type LoaderFnContext, notFound } from "@tanstack/react-router";
|
|
2
|
+
import { Predicate } from "effect";
|
|
3
|
+
import type * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
|
+
import type * as Atom from "effect/reactivity/Atom";
|
|
5
|
+
import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
6
|
+
import { loadQuery } from "./loadQuery.ts";
|
|
7
|
+
|
|
8
|
+
export function queryRoute<TParams extends object, TSearch extends object, A, E>(
|
|
9
|
+
query: (route: { readonly params: TParams; readonly search: TSearch }) => Atom.Atom<AsyncResult.AsyncResult<A, E>>,
|
|
10
|
+
) {
|
|
11
|
+
return {
|
|
12
|
+
loader: async (
|
|
13
|
+
context: Pick<LoaderFnContext, "abortController"> & {
|
|
14
|
+
readonly context: { readonly registry: AtomRegistry.AtomRegistry };
|
|
15
|
+
readonly deps: TSearch;
|
|
16
|
+
readonly params: TParams;
|
|
17
|
+
},
|
|
18
|
+
): Promise<A> => {
|
|
19
|
+
try {
|
|
20
|
+
return await loadQuery(context.context.registry, query({ params: context.params, search: context.deps }), context);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (Predicate.isTagged(error, "NotFound")) {
|
|
23
|
+
throw notFound();
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
loaderDeps: ({ search }: { readonly search: TSearch }) => search,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ResumeOptions as ContractOptions,
|
|
3
|
+
type ResumeSource as ContractSource,
|
|
4
|
+
type ResumeWindow as ContractWindow,
|
|
5
|
+
resumeSignal as contractResumeSignal,
|
|
6
|
+
} from "@shivaedev/contract/resume.ts";
|
|
7
|
+
|
|
8
|
+
export type ResumeSource = ContractSource;
|
|
9
|
+
export type ResumeWindow = ContractWindow;
|
|
10
|
+
export type ResumeOptions = ContractOptions;
|
|
11
|
+
export function resumeSignal(options: ResumeOptions) {
|
|
12
|
+
return contractResumeSignal(options);
|
|
13
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Effect, Schema } from "effect";
|
|
2
|
+
import type * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import { form } from "@shivaedev/form/form.ts";
|
|
4
|
+
import type { PersistedDraftConfig } from "@shivaedev/form/persistence.ts";
|
|
5
|
+
import type { Decoded, Encoded, FieldFailure, Fields, Form, Services } from "@shivaedev/form/shape.ts";
|
|
6
|
+
import { actionTrace } from "#actionTrace.ts";
|
|
7
|
+
import { type AtomServices, type RejectField, rejecting } from "./fieldRejection.ts";
|
|
8
|
+
|
|
9
|
+
export interface SubmissionConfig<TFields extends Fields, A, E, R, TRuntimeError> {
|
|
10
|
+
readonly fields: Schema.Struct<TFields>;
|
|
11
|
+
readonly initialValues: Encoded<TFields>;
|
|
12
|
+
readonly persistedDraft?: PersistedDraftConfig | undefined;
|
|
13
|
+
readonly rejectField: RejectField<TFields, E> | undefined;
|
|
14
|
+
readonly runtime: Atom.AtomRuntime<Services<TFields, R>, TRuntimeError>;
|
|
15
|
+
readonly save: (values: Decoded<TFields>) => Effect.Effect<A, E, AtomServices<R>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Submission<TFields extends Fields, A, E, TRuntimeError> {
|
|
19
|
+
readonly form: Form<TFields, A, E | FieldFailure, TRuntimeError>;
|
|
20
|
+
readonly submitted: () => Encoded<TFields> | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function submission<TFields extends Fields, A, E, R, TRuntimeError>(
|
|
24
|
+
config: SubmissionConfig<TFields, A, E, R, TRuntimeError>,
|
|
25
|
+
): Submission<TFields, A, E, TRuntimeError> {
|
|
26
|
+
const { fields, initialValues, runtime } = config;
|
|
27
|
+
const save = rejecting(fields, config.save, config.rejectField);
|
|
28
|
+
let submitted: Encoded<TFields> | undefined;
|
|
29
|
+
const currentForm: Form<TFields, A, E | FieldFailure, TRuntimeError> = form(fields, {
|
|
30
|
+
initialValues,
|
|
31
|
+
onSubmit: (values, submitter, snapshot) => {
|
|
32
|
+
submitted = snapshot;
|
|
33
|
+
return actionTrace(save(values, submitter), "ui.submit");
|
|
34
|
+
},
|
|
35
|
+
persistedDraft: config.persistedDraft,
|
|
36
|
+
runtime,
|
|
37
|
+
});
|
|
38
|
+
return { form: currentForm, submitted: () => submitted };
|
|
39
|
+
}
|