@nifrajs/web-preact 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/await.d.ts +18 -0
- package/dist/await.d.ts.map +1 -0
- package/dist/await.js +52 -0
- package/dist/await.js.map +1 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +40 -0
- package/dist/client.js.map +1 -0
- package/dist/compose.d.ts +10 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +22 -0
- package/dist/compose.js.map +1 -0
- package/dist/content.d.ts +18 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +20 -0
- package/dist/content.js.map +1 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +32 -0
- package/dist/error.js.map +1 -0
- package/dist/fetcher.d.ts +23 -0
- package/dist/fetcher.d.ts.map +1 -0
- package/dist/fetcher.js +48 -0
- package/dist/fetcher.js.map +1 -0
- package/dist/i18n.d.ts +20 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +31 -0
- package/dist/i18n.js.map +1 -0
- package/dist/image.d.ts +22 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +23 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/query.d.ts +17 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +58 -0
- package/dist/query.js.map +1 -0
- package/dist/read-deferred.d.ts +15 -0
- package/dist/read-deferred.d.ts.map +1 -0
- package/dist/read-deferred.js +18 -0
- package/dist/read-deferred.js.map +1 -0
- package/package.json +93 -0
- package/src/await.ts +78 -0
- package/src/client.ts +43 -0
- package/src/compose.ts +23 -0
- package/src/content.ts +28 -0
- package/src/error.ts +33 -0
- package/src/fetcher.ts +67 -0
- package/src/i18n.ts +47 -0
- package/src/image.ts +42 -0
- package/src/index.ts +30 -0
- package/src/query.ts +74 -0
- package/src/read-deferred.ts +32 -0
package/src/query.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createQueryClient,
|
|
3
|
+
type QueryClient,
|
|
4
|
+
type QueryHandle,
|
|
5
|
+
type QueryState,
|
|
6
|
+
} from "@nifrajs/web"
|
|
7
|
+
/**
|
|
8
|
+
* `@nifrajs/web-preact/query` — Preact bindings for the keyed query-cache. `useQuery(key, fn)` subscribes a
|
|
9
|
+
* component to a query (via `useSyncExternalStore` from preact/compat) and fetches on mount / key change;
|
|
10
|
+
* `useQueryClient` exposes `invalidateQueries` for after a mutation. Imports only `preact/compat` +
|
|
11
|
+
* `preact/hooks` + `@nifrajs/web`, so route components use it on the server *and* client. No JSX.
|
|
12
|
+
*
|
|
13
|
+
* The query client is created lazily **client-side only** (the `typeof window` guard) — on the server
|
|
14
|
+
* there is none, so `useQuery` renders the idle/pending state (queries are client-first; loaders are
|
|
15
|
+
* the SSR data source). The first client render uses the same idle snapshot, so no mismatch.
|
|
16
|
+
*/
|
|
17
|
+
import { useSyncExternalStore } from "preact/compat"
|
|
18
|
+
import { useEffect } from "preact/hooks"
|
|
19
|
+
|
|
20
|
+
let client: QueryClient | undefined
|
|
21
|
+
function getClient(): QueryClient | undefined {
|
|
22
|
+
if (typeof window === "undefined") return undefined // SSR → no client (queries render idle)
|
|
23
|
+
if (client === undefined) client = createQueryClient({ now: () => Date.now() })
|
|
24
|
+
return client
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Access the query client to imperatively `invalidateQueries(keyOrPrefix)` (e.g. after a mutation). */
|
|
28
|
+
export function useQueryClient(): Pick<QueryClient, "invalidateQueries"> {
|
|
29
|
+
return getClient() ?? { invalidateQueries: () => {} }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Stable idle snapshot for the server / pre-fetch render (stable ref → no loop, no hydration mismatch).
|
|
33
|
+
const IDLE: QueryState<never> = Object.freeze({
|
|
34
|
+
status: "pending",
|
|
35
|
+
data: undefined,
|
|
36
|
+
error: undefined,
|
|
37
|
+
isFetching: false,
|
|
38
|
+
updatedAt: Number.NEGATIVE_INFINITY,
|
|
39
|
+
})
|
|
40
|
+
const idleSnapshot = (): QueryState<never> => IDLE
|
|
41
|
+
const noopSubscribe = (): (() => void) => () => {}
|
|
42
|
+
const noopAsync = async (): Promise<never> => {
|
|
43
|
+
throw new Error("[nifra/web-preact] useQuery.refetch called with no query client (server?)")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A query's reactive {@link QueryState} plus `isPending` + `refetch`. */
|
|
47
|
+
export interface UseQueryResult<T> extends QueryState<T> {
|
|
48
|
+
/** `status === "pending"` — no data yet (initial load). */
|
|
49
|
+
readonly isPending: boolean
|
|
50
|
+
/** Force a refetch (ignores `staleTime`). */
|
|
51
|
+
readonly refetch: () => Promise<T>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Subscribe to the keyed query for `key`, fetched via `fn`. Returns `{ status, data, error, isFetching,
|
|
56
|
+
* updatedAt, isPending, refetch }`. Concurrent `useQuery`s with the same key share one cache entry +
|
|
57
|
+
* one in-flight fetch (dedup). Refetches on mount and when the key changes; SSR-idle.
|
|
58
|
+
*/
|
|
59
|
+
export function useQuery<T>(key: unknown, fn: () => Promise<T>): UseQueryResult<T> {
|
|
60
|
+
const handle: QueryHandle<T> | undefined = getClient()?.query<T>(key, fn)
|
|
61
|
+
const state = useSyncExternalStore<QueryState<T>>(
|
|
62
|
+
handle?.subscribe ?? noopSubscribe,
|
|
63
|
+
handle ? handle.snapshot : idleSnapshot,
|
|
64
|
+
)
|
|
65
|
+
// Fetch on mount + whenever the key changes (the handle is stable per key, so it changes with the key).
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (handle !== undefined) handle.fetch().catch(() => {})
|
|
68
|
+
}, [handle])
|
|
69
|
+
return {
|
|
70
|
+
...state,
|
|
71
|
+
isPending: state.status === "pending",
|
|
72
|
+
refetch: handle ? handle.refetch : (noopAsync as () => Promise<T>),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `readDeferred` — Preact's analogue of React 19's `use()` for a deferred promise (preact/compat has
|
|
3
|
+
* no `use`). Reads a React-style tagged thenable synchronously: returns the value if fulfilled, throws
|
|
4
|
+
* the reason if rejected, else throws the promise so `<Suspense>` awaits it. The core tags
|
|
5
|
+
* `status`/`value`/`reason` via the streamed `__nifraResolve`/`__nifraReject`; this also self-tags on
|
|
6
|
+
* settle so a server re-render (which has no `__nifraResolve`) reads "fulfilled" and stops re-throwing
|
|
7
|
+
* the already-resolved promise. Pure + isomorphic — no `preact` import, so it unit-tests directly.
|
|
8
|
+
*/
|
|
9
|
+
export type Thenable<T> = Promise<T> & {
|
|
10
|
+
status?: "pending" | "fulfilled" | "rejected"
|
|
11
|
+
value?: T
|
|
12
|
+
reason?: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readDeferred<T>(promise: Thenable<T>): T {
|
|
16
|
+
if (promise.status === "fulfilled") return promise.value as T
|
|
17
|
+
if (promise.status === "rejected") throw promise.reason
|
|
18
|
+
if (promise.status === undefined) {
|
|
19
|
+
promise.status = "pending"
|
|
20
|
+
promise.then(
|
|
21
|
+
(v) => {
|
|
22
|
+
promise.status = "fulfilled"
|
|
23
|
+
promise.value = v
|
|
24
|
+
},
|
|
25
|
+
(e) => {
|
|
26
|
+
promise.status = "rejected"
|
|
27
|
+
promise.reason = e
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
throw promise // pending — <Suspense> awaits it; the self-tag above wakes the re-render.
|
|
32
|
+
}
|