@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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/await.d.ts +18 -0
  4. package/dist/await.d.ts.map +1 -0
  5. package/dist/await.js +52 -0
  6. package/dist/await.js.map +1 -0
  7. package/dist/client.d.ts +12 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +40 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/compose.d.ts +10 -0
  12. package/dist/compose.d.ts.map +1 -0
  13. package/dist/compose.js +22 -0
  14. package/dist/compose.js.map +1 -0
  15. package/dist/content.d.ts +18 -0
  16. package/dist/content.d.ts.map +1 -0
  17. package/dist/content.js +20 -0
  18. package/dist/content.js.map +1 -0
  19. package/dist/error.d.ts +8 -0
  20. package/dist/error.d.ts.map +1 -0
  21. package/dist/error.js +32 -0
  22. package/dist/error.js.map +1 -0
  23. package/dist/fetcher.d.ts +23 -0
  24. package/dist/fetcher.d.ts.map +1 -0
  25. package/dist/fetcher.js +48 -0
  26. package/dist/fetcher.js.map +1 -0
  27. package/dist/i18n.d.ts +20 -0
  28. package/dist/i18n.d.ts.map +1 -0
  29. package/dist/i18n.js +31 -0
  30. package/dist/i18n.js.map +1 -0
  31. package/dist/image.d.ts +22 -0
  32. package/dist/image.d.ts.map +1 -0
  33. package/dist/image.js +23 -0
  34. package/dist/image.js.map +1 -0
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +29 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/query.d.ts +17 -0
  40. package/dist/query.d.ts.map +1 -0
  41. package/dist/query.js +58 -0
  42. package/dist/query.js.map +1 -0
  43. package/dist/read-deferred.d.ts +15 -0
  44. package/dist/read-deferred.d.ts.map +1 -0
  45. package/dist/read-deferred.js +18 -0
  46. package/dist/read-deferred.js.map +1 -0
  47. package/package.json +93 -0
  48. package/src/await.ts +78 -0
  49. package/src/client.ts +43 -0
  50. package/src/compose.ts +23 -0
  51. package/src/content.ts +28 -0
  52. package/src/error.ts +33 -0
  53. package/src/fetcher.ts +67 -0
  54. package/src/i18n.ts +47 -0
  55. package/src/image.ts +42 -0
  56. package/src/index.ts +30 -0
  57. package/src/query.ts +74 -0
  58. 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
+ }