@raf-query/react 0.0.6 → 0.0.8

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 CHANGED
@@ -1,8 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.8](https://github.com/KATT/raf-query/compare/0.0.7...0.0.8) (2026-10-12)
4
+
5
+ * rename the TanStack Query experiment to make clear it's experimental and unpublished ([#11](https://github.com/KATT/raf-query/issues/11)) ([146c3be](https://github.com/KATT/raf-query/commit/146c3be5034f8c4c642ea9faea693a7558ca4aac))
6
+
7
+ ## [0.0.7](https://github.com/KATT/raf-query/compare/0.0.6...0.0.7) (2026-10-12)
8
+
9
+ ### Features
10
+
11
+ - **react:** add @raf-query/react/devtools ([#7](https://github.com/KATT/raf-query/issues/7)) ([e325d76](https://github.com/KATT/raf-query/commit/e325d76305e29f935228a2dbad6834b0309229e4))
12
+
3
13
  ## [0.0.6](https://github.com/KATT/raf-query/compare/0.0.5...0.0.6) (2026-10-12)
4
14
 
5
- * experimental @raf-query/tanstack-react-query on TanStack Query ([#8](https://github.com/KATT/raf-query/issues/8)) ([9d909c4](https://github.com/KATT/raf-query/commit/9d909c46ca030fd465d7af0fcd9591818356dd80))
15
+ - experimental @raf-query/tanstack-react-query on TanStack Query ([#8](https://github.com/KATT/raf-query/issues/8)) ([9d909c4](https://github.com/KATT/raf-query/commit/9d909c46ca030fd465d7af0fcd9591818356dd80))
6
16
 
7
17
  ## [0.0.5](https://github.com/KATT/raf-query/compare/0.0.4...0.0.5) (2026-10-12)
8
18
 
package/README.md CHANGED
@@ -17,13 +17,14 @@ Early. Both packages are on npm at `0.0.x`, and the API can still change between
17
17
  | `@raf-query/core` | Framework-agnostic: `QueryClient`, cache, `queryOptions` / `infiniteQueryOptions`, handles, dehydrate / hydrate stream, `allKeyed` |
18
18
  | `@raf-query/react` | Hooks and provider. Re-exports everything from `@raf-query/core`, so apps install and import only this package |
19
19
 
20
- `@raf-query/react` subpath entries for framework integrations:
20
+ `@raf-query/react` subpath entries for framework integrations and tooling:
21
21
 
22
22
  | Entry | Use |
23
23
  | ---------------------------------- | ---------------------------------------------------------------------- |
24
24
  | `@raf-query/react/tanstack-router` | Streams the router's query cache to the client during SSR |
25
25
  | `@raf-query/react/rsc` | `HydrationBoundary` for passing server-component queries to the client |
26
26
  | `@raf-query/react/next` | `QueryProvider` for Next.js App Router |
27
+ | `@raf-query/react/devtools` | [Devtools](#devtools) panel for inspecting the cache in development |
27
28
 
28
29
  ## Principles
29
30
 
@@ -470,21 +471,64 @@ const program = Effect.gen(function* () {
470
471
 
471
472
  `watch` counts as a reader while it runs, so invalidation and window focus refetch the entry.
472
473
 
474
+ ## Devtools
475
+
476
+ `@raf-query/react/devtools` shows the browser's query cache as it changes. Render `QueryDevtools` anywhere under the provider:
477
+
478
+ ```tsx
479
+ import { QueryDevtools } from "@raf-query/react/devtools";
480
+
481
+ <QueryClientProvider client={queryClient}>
482
+ {props.children}
483
+ <QueryDevtools />
484
+ </QueryClientProvider>;
485
+ ```
486
+
487
+ In Next.js, put it inside `QueryProvider` in the root layout. It's a client component, so a server-component layout can render it.
488
+
489
+ A button in the corner opens a panel docked to the edge of the viewport. The panel lists every entry with its observer count (mounted readers) and status:
490
+
491
+ | Status | Meaning |
492
+ | ---------- | ------------------------------------------ |
493
+ | `fetching` | A request is in flight |
494
+ | `error` | The last request failed |
495
+ | `stale` | Read, and older than `staleTime` |
496
+ | `fresh` | Read, and within `staleTime` |
497
+ | `inactive` | Nothing reads it; it's removed at `gcTime` |
498
+
499
+ Select an entry to see its key, input, data, error, and last update, and to **Refetch**, **Invalidate**, **Reset** (drop its data, so readers suspend and refetch) or **Remove** it. The header filters entries by key, sorts them, and has **Invalidate all** and **Clear cache**. The panel keeps its open state and size in `localStorage`.
500
+
501
+ `QueryDevtools` props, all optional:
502
+
503
+ | Prop | Values (default first) |
504
+ | ---------------- | -------------------------------------------------------------- |
505
+ | `client` | The nearest `QueryClientProvider`'s client, or another one |
506
+ | `initialIsOpen` | `false`, `true`. Applies until the user opens or closes it |
507
+ | `buttonPosition` | `"bottom-right"`, `"bottom-left"`, `"top-right"`, `"top-left"` |
508
+ | `position` | `"bottom"`, `"top"`, `"left"`, `"right"`: the docked edge |
509
+ | `theme` | `"system"`, `"dark"`, `"light"` |
510
+
511
+ `QueryDevtoolsPanel` is the panel alone, to place in your own layout. It takes `client`, `theme`, `style`, and `onClose`, which adds a close button.
512
+
513
+ Both render nothing on the server, since the server's cache isn't the browser's. They also render nothing when `process.env.NODE_ENV` is `"production"`, and bundlers drop the panel's code from production builds. To ship the panel in production, for example lazily behind a flag, import from `@raf-query/react/devtools/production`.
514
+
473
515
  ## API reference
474
516
 
475
517
  `QueryClient`:
476
518
 
477
- | Member | Use |
478
- | --------------------------------------- | ------------------------------------------------------------------------------- |
479
- | `new QueryClient(config?)` | `config.defaults` (query options below), `config.onError`, `config.isServer` |
480
- | `prefetch`, `ensure`, `fetch` | [Start queries](#starting-queries) |
481
- | `invalidate`, `setData`, `getData` | [Write and read the cache](#mutations) |
482
- | `effect` | [Effect versions](#effect) of `ensure`, `fetch`, `invalidate`, plus `watch` |
483
- | `subscribe(listener)` | Cache events: `added`, `updated`, `removed`, and `failed` (with the `error`) |
484
- | `getAll()`, `getQuery(factory, input?)` | Cache entries, for devtools and integrations |
485
- | `clear()` | Drop every entry and abort in-flight requests |
486
- | `mount()` | Start window-focus and reconnect refetching. The providers call it |
487
- | `isServer` | Detected from `window`. Server clients don't retry, never gc, skip stale timers |
519
+ | Member | Use |
520
+ | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
521
+ | `new QueryClient(config?)` | `config.defaults` (query options below), `config.onError`, `config.isServer` |
522
+ | `prefetch`, `ensure`, `fetch` | [Start queries](#starting-queries) |
523
+ | `invalidate`, `setData`, `getData` | [Write and read the cache](#mutations) |
524
+ | `reset(factory, input)` / `reset(filter)` | Drop data and errors; read queries suspend and refetch. Resolves when they settle |
525
+ | `remove(factory, input)` / `remove(filter)` | Remove entries and abort their requests |
526
+ | `effect` | [Effect versions](#effect) of `ensure`, `fetch`, `invalidate`, plus `watch` |
527
+ | `subscribe(listener)` | Cache events: `added`, `updated`, `observers` (a reader subscribed or left), `removed`, and `failed` (with the `error`) |
528
+ | `getAll()`, `getQuery(factory, input?)` | Cache entries, for devtools and integrations |
529
+ | `clear()` | Drop every entry and abort in-flight requests |
530
+ | `mount()` | Start window-focus and reconnect refetching. The providers call it |
531
+ | `isServer` | Detected from `window`. Server clients don't retry, never gc, skip stale timers |
488
532
 
489
533
  Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.defaults`:
490
534
 
@@ -0,0 +1,38 @@
1
+ import { QueryClient } from "@raf-query/core";
2
+ import { CSSProperties } from "react";
3
+ //#region src/devtools-panel.d.ts
4
+ export type DevtoolsTheme = "dark" | "light" | "system";
5
+ export interface QueryDevtoolsPanelProps {
6
+ /** The client to inspect. Default: the nearest `QueryClientProvider`'s. */
7
+ readonly client?: QueryClient;
8
+ /** Shows a close button that calls this. */
9
+ readonly onClose?: () => void;
10
+ /** Styles for the panel's root element, e.g. its height. */
11
+ readonly style?: CSSProperties;
12
+ /** Default: `"system"`. */
13
+ readonly theme?: DevtoolsTheme;
14
+ }
15
+ export interface QueryDevtoolsProps {
16
+ /** Corner of the toggle button. Default: `"bottom-right"`. */
17
+ readonly buttonPosition?: "bottom-left" | "bottom-right" | "top-left" | "top-right";
18
+ /** The client to inspect. Default: the nearest `QueryClientProvider`'s. */
19
+ readonly client?: QueryClient;
20
+ /** Open on the first visit. Later visits remember the last open state. */
21
+ readonly initialIsOpen?: boolean;
22
+ /** Edge of the viewport the panel docks to. Default: `"bottom"`. */
23
+ readonly position?: "bottom" | "left" | "right" | "top";
24
+ /** Default: `"system"`. */
25
+ readonly theme?: DevtoolsTheme;
26
+ }
27
+ /**
28
+ * The devtools panel on its own, to place anywhere in the page. Renders
29
+ * nothing on the server, since the server's cache isn't the browser's.
30
+ */
31
+ export declare function QueryDevtoolsPanel(props: QueryDevtoolsPanelProps): import("react").JSX.Element | null;
32
+ /**
33
+ * A toggle button that opens the devtools panel docked to an edge of the
34
+ * viewport. Renders nothing on the server. Open state and size are kept in
35
+ * `localStorage`.
36
+ */
37
+ export declare function QueryDevtools(props: QueryDevtoolsProps): import("react").JSX.Element | null;
38
+ //#endregion
@@ -0,0 +1,878 @@
1
+ "use client";
2
+ import { useOptionalQueryClient } from "./provider.mjs";
3
+ import { countByStatus, sortOrders, statuses, useQueryRows, visibleRows } from "./devtools-store.mjs";
4
+ import { useState, useSyncExternalStore } from "react";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { Predicate } from "effect";
7
+ //#region src/devtools-panel.tsx
8
+ const palettes = {
9
+ dark: {
10
+ accent: "#38bdf8",
11
+ background: "#0f172a",
12
+ boolean: "#c084fc",
13
+ border: "#334155",
14
+ muted: "#94a3b8",
15
+ number: "#fbbf24",
16
+ raised: "#1e293b",
17
+ selected: "#1e3a5f",
18
+ string: "#4ade80",
19
+ text: "#e2e8f0"
20
+ },
21
+ light: {
22
+ accent: "#0369a1",
23
+ background: "#ffffff",
24
+ boolean: "#7e22ce",
25
+ border: "#cbd5e1",
26
+ muted: "#64748b",
27
+ number: "#b45309",
28
+ raised: "#f1f5f9",
29
+ selected: "#dbeafe",
30
+ string: "#15803d",
31
+ text: "#0f172a"
32
+ }
33
+ };
34
+ const statusColors = {
35
+ error: "#dc2626",
36
+ fetching: "#2563eb",
37
+ fresh: "#16a34a",
38
+ inactive: "#64748b",
39
+ stale: "#ca8a04"
40
+ };
41
+ const sortLabels = {
42
+ hash: "Key",
43
+ status: "Status",
44
+ updated: "Last updated"
45
+ };
46
+ const fontFamily = "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif";
47
+ const monoFamily = "ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace";
48
+ /** Entries an expanded object, array, map, or set lists before "… more". */
49
+ const maxChildren = 100;
50
+ const minSize = 120;
51
+ const defaultSize = 320;
52
+ const storageKeys = {
53
+ open: "raf-query-devtools:open",
54
+ size: "raf-query-devtools:size"
55
+ };
56
+ const noop = () => {};
57
+ const subscribeNever = () => noop;
58
+ /** `false` on the server and while hydrating, then `true`. */
59
+ function useIsClient() {
60
+ return useSyncExternalStore(subscribeNever, () => true, () => false);
61
+ }
62
+ function subscribeColorScheme(onChange) {
63
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
64
+ media.addEventListener("change", onChange);
65
+ return () => media.removeEventListener("change", onChange);
66
+ }
67
+ function prefersDark() {
68
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
69
+ }
70
+ function usePalette(theme = "system") {
71
+ const systemDark = useSyncExternalStore(subscribeColorScheme, prefersDark, () => true);
72
+ if (theme === "system") return systemDark ? palettes.dark : palettes.light;
73
+ return palettes[theme];
74
+ }
75
+ function useDevtoolsClient(client) {
76
+ const contextClient = useOptionalQueryClient();
77
+ const resolved = client ?? contextClient;
78
+ if (!resolved) throw new Error("raf-query devtools: pass `client` or render inside a QueryClientProvider");
79
+ return resolved;
80
+ }
81
+ function readStored(key) {
82
+ try {
83
+ return window.localStorage.getItem(key);
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+ function writeStored(key, value) {
89
+ try {
90
+ window.localStorage.setItem(key, value);
91
+ } catch {}
92
+ }
93
+ function formatTime(updatedAt) {
94
+ return updatedAt === 0 ? "never" : new Date(updatedAt).toLocaleTimeString();
95
+ }
96
+ /** One-line form of a value, for leaves and collapsed nodes. */
97
+ function preview(value) {
98
+ if (Predicate.isString(value)) return JSON.stringify(value);
99
+ if (Predicate.isBigInt(value)) return `${value}n`;
100
+ if (Predicate.isSymbol(value)) return value.toString();
101
+ if (Predicate.isFunction(value)) return `ƒ ${value.name || "anonymous"}()`;
102
+ if (Predicate.isDate(value)) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
103
+ if (Predicate.isError(value)) return `${value.name}: ${value.message}`;
104
+ if (Predicate.isRegExp(value)) return String(value);
105
+ if (Predicate.isPromiseLike(value)) return "Promise";
106
+ if (Array.isArray(value)) return `Array(${value.length})`;
107
+ if (Predicate.isMap(value)) return `Map(${value.size})`;
108
+ if (Predicate.isSet(value)) return `Set(${value.size})`;
109
+ if (Predicate.isObject(value)) {
110
+ const size = Object.keys(value).length;
111
+ return size === 1 ? "{1 key}" : `{${size} keys}`;
112
+ }
113
+ return String(value);
114
+ }
115
+ /** Labelled children of a value the explorer can expand, else `undefined`. */
116
+ function childrenOf(value) {
117
+ if (Array.isArray(value)) return value.map((item, index) => [String(index), item]);
118
+ if (Predicate.isMap(value)) return [...value].map(([key, item]) => [preview(key), item]);
119
+ if (Predicate.isSet(value)) return [...value].map((item, index) => [String(index), item]);
120
+ if (!Predicate.isObject(value) || Predicate.isDate(value) || Predicate.isError(value) || Predicate.isRegExp(value) || Predicate.isPromiseLike(value)) return;
121
+ return Object.entries(value);
122
+ }
123
+ function colorOf(value, palette) {
124
+ if (Predicate.isString(value)) return palette.string;
125
+ if (Predicate.isNumber(value) || Predicate.isBigInt(value)) return palette.number;
126
+ if (Predicate.isBoolean(value)) return palette.boolean;
127
+ if (Predicate.isNullish(value)) return palette.muted;
128
+ return palette.text;
129
+ }
130
+ function copyText(value) {
131
+ try {
132
+ return JSON.stringify(value, null, 2) ?? String(value);
133
+ } catch {
134
+ return preview(value);
135
+ }
136
+ }
137
+ function Explorer(props) {
138
+ const [expanded, setExpanded] = useState(props.depth < 1);
139
+ const children = childrenOf(props.value);
140
+ const palette = props.palette;
141
+ const label = props.label === void 0 ? null : /* @__PURE__ */ jsx("span", {
142
+ style: { color: palette.accent },
143
+ children: `${props.label}: `
144
+ });
145
+ if (children === void 0) return /* @__PURE__ */ jsxs("div", {
146
+ style: { paddingLeft: 14 },
147
+ children: [label, /* @__PURE__ */ jsx("span", {
148
+ style: { color: colorOf(props.value, palette) },
149
+ children: preview(props.value)
150
+ })]
151
+ });
152
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("button", {
153
+ "aria-expanded": expanded,
154
+ onClick: () => setExpanded(!expanded),
155
+ style: {
156
+ background: "none",
157
+ border: "none",
158
+ color: palette.text,
159
+ cursor: "pointer",
160
+ font: "inherit",
161
+ padding: 0,
162
+ textAlign: "left"
163
+ },
164
+ type: "button",
165
+ children: [
166
+ /* @__PURE__ */ jsx("span", {
167
+ "aria-hidden": true,
168
+ style: {
169
+ display: "inline-block",
170
+ width: 14
171
+ },
172
+ children: expanded ? "▾" : "▸"
173
+ }),
174
+ label,
175
+ /* @__PURE__ */ jsx("span", {
176
+ style: { color: palette.muted },
177
+ children: preview(props.value)
178
+ })
179
+ ]
180
+ }), expanded ? /* @__PURE__ */ jsxs("div", {
181
+ style: { paddingLeft: 14 },
182
+ children: [children.slice(0, maxChildren).map(([key, item]) => /* @__PURE__ */ jsx(Explorer, {
183
+ depth: props.depth + 1,
184
+ label: key,
185
+ palette,
186
+ value: item
187
+ }, key)), children.length > maxChildren ? /* @__PURE__ */ jsx("div", {
188
+ style: {
189
+ color: palette.muted,
190
+ paddingLeft: 14
191
+ },
192
+ children: `… ${children.length - maxChildren} more`
193
+ }) : null]
194
+ }) : null] });
195
+ }
196
+ function StatusChip(props) {
197
+ return /* @__PURE__ */ jsxs("span", {
198
+ "data-status": props.status,
199
+ style: {
200
+ alignItems: "center",
201
+ background: props.palette.raised,
202
+ border: `1px solid ${props.palette.border}`,
203
+ borderRadius: 999,
204
+ display: "inline-flex",
205
+ gap: 6,
206
+ padding: "2px 8px"
207
+ },
208
+ children: [/* @__PURE__ */ jsx("span", {
209
+ "aria-hidden": true,
210
+ style: {
211
+ background: statusColors[props.status],
212
+ borderRadius: 999,
213
+ height: 8,
214
+ width: 8
215
+ }
216
+ }), `${props.status} ${props.count}`]
217
+ });
218
+ }
219
+ function ActionButton(props) {
220
+ return /* @__PURE__ */ jsx("button", {
221
+ disabled: props.disabled,
222
+ onClick: props.onClick,
223
+ style: {
224
+ background: props.color,
225
+ border: "none",
226
+ borderRadius: 4,
227
+ color: "#ffffff",
228
+ cursor: props.disabled ? "not-allowed" : "pointer",
229
+ font: "inherit",
230
+ opacity: props.disabled ? .5 : 1,
231
+ padding: "4px 10px"
232
+ },
233
+ type: "button",
234
+ children: props.label
235
+ });
236
+ }
237
+ function Heading(props) {
238
+ return /* @__PURE__ */ jsx("h3", {
239
+ style: {
240
+ background: props.palette.raised,
241
+ borderBottom: `1px solid ${props.palette.border}`,
242
+ borderTop: `1px solid ${props.palette.border}`,
243
+ fontSize: 12,
244
+ fontWeight: 600,
245
+ margin: 0,
246
+ padding: "4px 8px"
247
+ },
248
+ children: props.title
249
+ });
250
+ }
251
+ function Field(props) {
252
+ return /* @__PURE__ */ jsxs("div", {
253
+ style: {
254
+ display: "flex",
255
+ gap: 8,
256
+ justifyContent: "space-between"
257
+ },
258
+ children: [/* @__PURE__ */ jsx("dt", {
259
+ style: { color: props.palette.muted },
260
+ children: props.label
261
+ }), /* @__PURE__ */ jsx("dd", {
262
+ style: {
263
+ margin: 0,
264
+ textAlign: "right"
265
+ },
266
+ children: props.children
267
+ })]
268
+ });
269
+ }
270
+ function QueryDetails(props) {
271
+ const palette = props.palette;
272
+ const query = props.row.query;
273
+ const state = props.row.state;
274
+ const options = query.options;
275
+ const section = { padding: 8 };
276
+ return /* @__PURE__ */ jsxs("section", {
277
+ "aria-label": "Query details",
278
+ style: {
279
+ flex: 1,
280
+ minWidth: 0,
281
+ overflow: "auto"
282
+ },
283
+ children: [
284
+ /* @__PURE__ */ jsx(Heading, {
285
+ palette,
286
+ title: "Query details"
287
+ }),
288
+ /* @__PURE__ */ jsxs("div", {
289
+ style: section,
290
+ children: [/* @__PURE__ */ jsx("pre", {
291
+ style: {
292
+ fontFamily: monoFamily,
293
+ margin: "0 0 8px",
294
+ whiteSpace: "pre-wrap",
295
+ wordBreak: "break-all"
296
+ },
297
+ children: JSON.stringify(query.key, null, 2)
298
+ }), /* @__PURE__ */ jsxs("dl", {
299
+ style: {
300
+ display: "grid",
301
+ gap: 4,
302
+ margin: 0
303
+ },
304
+ children: [
305
+ /* @__PURE__ */ jsx(Field, {
306
+ label: "Status",
307
+ palette,
308
+ children: props.row.status
309
+ }),
310
+ /* @__PURE__ */ jsx(Field, {
311
+ label: "Observers",
312
+ palette,
313
+ children: String(props.row.observers)
314
+ }),
315
+ /* @__PURE__ */ jsx(Field, {
316
+ label: "Last updated",
317
+ palette,
318
+ children: formatTime(state.updatedAt)
319
+ }),
320
+ /* @__PURE__ */ jsx(Field, {
321
+ label: "Kind",
322
+ palette,
323
+ children: options === void 0 ? "hydrated" : options.kind
324
+ }),
325
+ /* @__PURE__ */ jsx(Field, {
326
+ label: "Failures",
327
+ palette,
328
+ children: String(state.failureCount)
329
+ })
330
+ ]
331
+ })]
332
+ }),
333
+ /* @__PURE__ */ jsx(Heading, {
334
+ palette,
335
+ title: "Actions"
336
+ }),
337
+ /* @__PURE__ */ jsxs("div", {
338
+ style: {
339
+ ...section,
340
+ display: "flex",
341
+ flexWrap: "wrap",
342
+ gap: 6
343
+ },
344
+ children: [
345
+ /* @__PURE__ */ jsx(ActionButton, {
346
+ color: statusColors.fetching,
347
+ disabled: options === void 0 || state.isFetching,
348
+ label: "Refetch",
349
+ onClick: () => void query.refetch()
350
+ }),
351
+ /* @__PURE__ */ jsx(ActionButton, {
352
+ color: statusColors.stale,
353
+ label: "Invalidate",
354
+ onClick: () => void query.invalidate()
355
+ }),
356
+ /* @__PURE__ */ jsx(ActionButton, {
357
+ color: statusColors.inactive,
358
+ label: "Reset",
359
+ onClick: () => void query.reset()
360
+ }),
361
+ /* @__PURE__ */ jsx(ActionButton, {
362
+ color: statusColors.error,
363
+ label: "Remove",
364
+ onClick: () => props.client.remove({
365
+ exact: true,
366
+ queryKey: query.key
367
+ })
368
+ })
369
+ ]
370
+ }),
371
+ state.error == null ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
372
+ palette,
373
+ title: "Error"
374
+ }), /* @__PURE__ */ jsxs("div", {
375
+ style: {
376
+ ...section,
377
+ color: statusColors.error
378
+ },
379
+ children: [/* @__PURE__ */ jsx("div", { children: preview(state.error) }), Predicate.isError(state.error) && state.error.stack ? /* @__PURE__ */ jsx("pre", {
380
+ style: {
381
+ fontFamily: monoFamily,
382
+ fontSize: 11,
383
+ margin: "6px 0 0",
384
+ whiteSpace: "pre-wrap"
385
+ },
386
+ children: state.error.stack
387
+ }) : null]
388
+ })] }),
389
+ /* @__PURE__ */ jsx(Heading, {
390
+ palette,
391
+ title: "Data"
392
+ }),
393
+ /* @__PURE__ */ jsxs("div", {
394
+ style: {
395
+ ...section,
396
+ fontFamily: monoFamily
397
+ },
398
+ children: [/* @__PURE__ */ jsx("button", {
399
+ onClick: () => void navigator.clipboard?.writeText(copyText(state.data)),
400
+ style: {
401
+ background: "none",
402
+ border: `1px solid ${palette.border}`,
403
+ borderRadius: 4,
404
+ color: palette.text,
405
+ cursor: "pointer",
406
+ float: "right",
407
+ font: "inherit",
408
+ fontFamily,
409
+ padding: "2px 8px"
410
+ },
411
+ type: "button",
412
+ children: "Copy"
413
+ }), /* @__PURE__ */ jsx(Explorer, {
414
+ depth: 0,
415
+ palette,
416
+ value: state.data
417
+ })]
418
+ }),
419
+ /* @__PURE__ */ jsx(Heading, {
420
+ palette,
421
+ title: "Input"
422
+ }),
423
+ /* @__PURE__ */ jsx("div", {
424
+ style: {
425
+ ...section,
426
+ fontFamily: monoFamily
427
+ },
428
+ children: /* @__PURE__ */ jsx(Explorer, {
429
+ depth: 0,
430
+ palette,
431
+ value: query.handle.input
432
+ })
433
+ })
434
+ ]
435
+ });
436
+ }
437
+ function QueryList(props) {
438
+ const palette = props.palette;
439
+ if (props.rows.length === 0) return /* @__PURE__ */ jsx("p", {
440
+ style: {
441
+ color: palette.muted,
442
+ margin: 0,
443
+ padding: 12
444
+ },
445
+ children: "No queries"
446
+ });
447
+ return /* @__PURE__ */ jsx("ul", {
448
+ style: {
449
+ listStyle: "none",
450
+ margin: 0,
451
+ padding: 0
452
+ },
453
+ children: props.rows.map((row) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
454
+ "aria-pressed": row.hash === props.selectedHash,
455
+ "data-status": row.status,
456
+ onClick: () => props.onSelect(row.hash),
457
+ style: {
458
+ alignItems: "center",
459
+ background: row.hash === props.selectedHash ? palette.selected : "transparent",
460
+ border: "none",
461
+ borderBottom: `1px solid ${palette.border}`,
462
+ color: palette.text,
463
+ cursor: "pointer",
464
+ display: "flex",
465
+ font: "inherit",
466
+ gap: 8,
467
+ padding: "4px 8px",
468
+ textAlign: "left",
469
+ width: "100%"
470
+ },
471
+ title: row.status,
472
+ type: "button",
473
+ children: [/* @__PURE__ */ jsx("span", {
474
+ "aria-label": `${row.observers} observers`,
475
+ style: {
476
+ background: statusColors[row.status],
477
+ borderRadius: 4,
478
+ color: "#ffffff",
479
+ flexShrink: 0,
480
+ fontWeight: 600,
481
+ minWidth: 22,
482
+ padding: "1px 4px",
483
+ textAlign: "center"
484
+ },
485
+ children: row.observers
486
+ }), /* @__PURE__ */ jsx("code", {
487
+ style: {
488
+ fontFamily: monoFamily,
489
+ overflow: "hidden",
490
+ textOverflow: "ellipsis",
491
+ whiteSpace: "nowrap"
492
+ },
493
+ children: row.hash
494
+ })]
495
+ }) }, row.hash))
496
+ });
497
+ }
498
+ function isSortBy(value) {
499
+ return sortOrders.some((order) => order === value);
500
+ }
501
+ function Panel(props) {
502
+ const client = useDevtoolsClient(props.client);
503
+ const palette = usePalette(props.theme);
504
+ const rows = useQueryRows(client);
505
+ const [filter, setFilter] = useState("");
506
+ const [sortBy, setSortBy] = useState("status");
507
+ const [selectedHash, setSelectedHash] = useState(void 0);
508
+ const counts = countByStatus(rows);
509
+ const shown = visibleRows(rows, filter, sortBy);
510
+ const selected = rows.find((row) => row.hash === selectedHash);
511
+ const control = {
512
+ background: palette.raised,
513
+ border: `1px solid ${palette.border}`,
514
+ borderRadius: 4,
515
+ color: palette.text,
516
+ font: "inherit",
517
+ padding: "3px 6px"
518
+ };
519
+ const onClose = props.onClose;
520
+ return /* @__PURE__ */ jsxs("div", {
521
+ "data-raf-query-devtools": "",
522
+ style: {
523
+ background: palette.background,
524
+ boxSizing: "border-box",
525
+ color: palette.text,
526
+ colorScheme: palette === palettes.dark ? "dark" : "light",
527
+ display: "flex",
528
+ flexDirection: "column",
529
+ fontFamily,
530
+ fontSize: 12,
531
+ lineHeight: 1.4,
532
+ minHeight: 0,
533
+ overflow: "hidden",
534
+ ...props.style
535
+ },
536
+ children: [/* @__PURE__ */ jsxs("header", {
537
+ style: {
538
+ alignItems: "center",
539
+ borderBottom: `1px solid ${palette.border}`,
540
+ display: "flex",
541
+ flexWrap: "wrap",
542
+ gap: 8,
543
+ padding: 8
544
+ },
545
+ children: [
546
+ /* @__PURE__ */ jsxs("strong", {
547
+ style: { fontSize: 13 },
548
+ children: ["raf-query ", /* @__PURE__ */ jsx("span", {
549
+ style: { color: palette.accent },
550
+ children: "devtools"
551
+ })]
552
+ }),
553
+ /* @__PURE__ */ jsx("div", {
554
+ style: {
555
+ display: "flex",
556
+ flexWrap: "wrap",
557
+ gap: 4
558
+ },
559
+ children: statuses.map((status) => /* @__PURE__ */ jsx(StatusChip, {
560
+ count: counts[status],
561
+ palette,
562
+ status
563
+ }, status))
564
+ }),
565
+ /* @__PURE__ */ jsxs("div", {
566
+ style: {
567
+ display: "flex",
568
+ gap: 6,
569
+ marginLeft: "auto"
570
+ },
571
+ children: [
572
+ /* @__PURE__ */ jsx("input", {
573
+ "aria-label": "Filter queries",
574
+ onChange: (event) => setFilter(event.target.value),
575
+ placeholder: "Filter",
576
+ style: control,
577
+ type: "search",
578
+ value: filter
579
+ }),
580
+ /* @__PURE__ */ jsx("select", {
581
+ "aria-label": "Sort queries",
582
+ onChange: (event) => {
583
+ const value = event.target.value;
584
+ if (isSortBy(value)) setSortBy(value);
585
+ },
586
+ style: control,
587
+ value: sortBy,
588
+ children: sortOrders.map((order) => /* @__PURE__ */ jsx("option", {
589
+ value: order,
590
+ children: sortLabels[order]
591
+ }, order))
592
+ }),
593
+ /* @__PURE__ */ jsx("button", {
594
+ onClick: () => void client.invalidate({ queryKey: [] }),
595
+ style: {
596
+ ...control,
597
+ cursor: "pointer"
598
+ },
599
+ type: "button",
600
+ children: "Invalidate all"
601
+ }),
602
+ /* @__PURE__ */ jsx("button", {
603
+ onClick: () => client.clear(),
604
+ style: {
605
+ ...control,
606
+ cursor: "pointer"
607
+ },
608
+ type: "button",
609
+ children: "Clear cache"
610
+ }),
611
+ onClose ? /* @__PURE__ */ jsx("button", {
612
+ "aria-label": "Close raf-query devtools",
613
+ onClick: () => onClose(),
614
+ style: {
615
+ ...control,
616
+ cursor: "pointer"
617
+ },
618
+ type: "button",
619
+ children: "✕"
620
+ }) : null
621
+ ]
622
+ })
623
+ ]
624
+ }), /* @__PURE__ */ jsxs("div", {
625
+ style: {
626
+ display: "flex",
627
+ flex: 1,
628
+ minHeight: 0
629
+ },
630
+ children: [/* @__PURE__ */ jsx("nav", {
631
+ "aria-label": "Queries",
632
+ style: {
633
+ borderRight: `1px solid ${palette.border}`,
634
+ flex: 1,
635
+ minWidth: 0,
636
+ overflow: "auto"
637
+ },
638
+ children: /* @__PURE__ */ jsx(QueryList, {
639
+ onSelect: setSelectedHash,
640
+ palette,
641
+ rows: shown,
642
+ selectedHash
643
+ })
644
+ }), selected ? /* @__PURE__ */ jsx(QueryDetails, {
645
+ client,
646
+ palette,
647
+ row: selected
648
+ }) : /* @__PURE__ */ jsx("p", {
649
+ style: {
650
+ color: palette.muted,
651
+ flex: 1,
652
+ margin: 0,
653
+ padding: 12
654
+ },
655
+ children: "Select a query to inspect it"
656
+ })]
657
+ })]
658
+ });
659
+ }
660
+ /**
661
+ * The devtools panel on its own, to place anywhere in the page. Renders
662
+ * nothing on the server, since the server's cache isn't the browser's.
663
+ */
664
+ function QueryDevtoolsPanel(props) {
665
+ return useIsClient() ? /* @__PURE__ */ jsx(Panel, {
666
+ client: props.client,
667
+ onClose: props.onClose,
668
+ style: props.style,
669
+ theme: props.theme
670
+ }) : null;
671
+ }
672
+ const corners = {
673
+ "bottom-left": {
674
+ bottom: 12,
675
+ left: 12
676
+ },
677
+ "bottom-right": {
678
+ bottom: 12,
679
+ right: 12
680
+ },
681
+ "top-left": {
682
+ left: 12,
683
+ top: 12
684
+ },
685
+ "top-right": {
686
+ right: 12,
687
+ top: 12
688
+ }
689
+ };
690
+ function clampSize(size, max) {
691
+ return Math.min(Math.max(size, minSize), Math.max(max - 40, minSize));
692
+ }
693
+ function DockedDevtools(props) {
694
+ const position = props.position ?? "bottom";
695
+ const buttonPosition = props.buttonPosition ?? "bottom-right";
696
+ const horizontal = position === "left" || position === "right";
697
+ const [isOpen, setIsOpen] = useState(() => {
698
+ const stored = readStored(storageKeys.open);
699
+ return stored === null ? props.initialIsOpen ?? false : stored === "true";
700
+ });
701
+ const [size, setSize] = useState(() => Number(readStored(storageKeys.size)) || defaultSize);
702
+ const palette = usePalette(props.theme);
703
+ const setOpen = (open) => {
704
+ setIsOpen(open);
705
+ writeStored(storageKeys.open, String(open));
706
+ };
707
+ const startResize = (event) => {
708
+ event.preventDefault();
709
+ let latest = size;
710
+ const onMove = (move) => {
711
+ const next = {
712
+ bottom: window.innerHeight - move.clientY,
713
+ left: move.clientX,
714
+ right: window.innerWidth - move.clientX,
715
+ top: move.clientY
716
+ }[position];
717
+ latest = clampSize(next, horizontal ? window.innerWidth : window.innerHeight);
718
+ setSize(latest);
719
+ };
720
+ const onUp = () => {
721
+ window.removeEventListener("pointermove", onMove);
722
+ window.removeEventListener("pointerup", onUp);
723
+ writeStored(storageKeys.size, String(latest));
724
+ };
725
+ window.addEventListener("pointermove", onMove);
726
+ window.addEventListener("pointerup", onUp);
727
+ };
728
+ if (!isOpen) return /* @__PURE__ */ jsxs("button", {
729
+ "aria-label": "Open raf-query devtools",
730
+ onClick: () => setOpen(true),
731
+ style: {
732
+ ...corners[buttonPosition],
733
+ alignItems: "center",
734
+ background: palette.background,
735
+ border: `1px solid ${palette.border}`,
736
+ borderRadius: 999,
737
+ boxShadow: "0 2px 8px rgb(0 0 0 / 0.25)",
738
+ color: palette.text,
739
+ cursor: "pointer",
740
+ display: "flex",
741
+ fontFamily,
742
+ fontSize: 12,
743
+ fontWeight: 600,
744
+ gap: 6,
745
+ padding: "6px 12px",
746
+ position: "fixed",
747
+ zIndex: 99999
748
+ },
749
+ type: "button",
750
+ children: [/* @__PURE__ */ jsx("span", {
751
+ "aria-hidden": true,
752
+ style: {
753
+ background: palette.accent,
754
+ borderRadius: 999,
755
+ height: 8,
756
+ width: 8
757
+ }
758
+ }), "raf-query"]
759
+ });
760
+ const edge = {
761
+ bottom: {
762
+ bottom: 0,
763
+ height: size,
764
+ left: 0,
765
+ right: 0
766
+ },
767
+ left: {
768
+ bottom: 0,
769
+ left: 0,
770
+ top: 0,
771
+ width: size
772
+ },
773
+ right: {
774
+ bottom: 0,
775
+ right: 0,
776
+ top: 0,
777
+ width: size
778
+ },
779
+ top: {
780
+ height: size,
781
+ left: 0,
782
+ right: 0,
783
+ top: 0
784
+ }
785
+ }[position];
786
+ const handleEdge = {
787
+ bottom: {
788
+ height: 10,
789
+ left: 0,
790
+ right: 0,
791
+ top: -5
792
+ },
793
+ left: {
794
+ bottom: 0,
795
+ right: -5,
796
+ top: 0,
797
+ width: 10
798
+ },
799
+ right: {
800
+ bottom: 0,
801
+ left: -5,
802
+ top: 0,
803
+ width: 10
804
+ },
805
+ top: {
806
+ bottom: -5,
807
+ height: 10,
808
+ left: 0,
809
+ right: 0
810
+ }
811
+ }[position];
812
+ return /* @__PURE__ */ jsxs("aside", {
813
+ "aria-label": "raf-query devtools",
814
+ style: {
815
+ ...edge,
816
+ borderColor: palette.border,
817
+ borderStyle: "solid",
818
+ borderWidth: {
819
+ bottom: "1px 0 0",
820
+ left: "0 1px 0 0",
821
+ right: "0 0 0 1px",
822
+ top: "0 0 1px"
823
+ }[position],
824
+ boxShadow: "0 0 16px rgb(0 0 0 / 0.3)",
825
+ position: "fixed",
826
+ zIndex: 99999
827
+ },
828
+ children: [/* @__PURE__ */ jsx("div", {
829
+ "aria-label": "Resize raf-query devtools",
830
+ "aria-orientation": horizontal ? "vertical" : "horizontal",
831
+ onPointerDown: startResize,
832
+ role: "separator",
833
+ style: {
834
+ ...handleEdge,
835
+ alignItems: "center",
836
+ cursor: horizontal ? "ew-resize" : "ns-resize",
837
+ display: "flex",
838
+ justifyContent: "center",
839
+ position: "absolute",
840
+ touchAction: "none",
841
+ zIndex: 1
842
+ },
843
+ children: /* @__PURE__ */ jsx("span", {
844
+ "aria-hidden": true,
845
+ style: {
846
+ background: palette.border,
847
+ borderRadius: 999,
848
+ height: horizontal ? 32 : 4,
849
+ width: horizontal ? 4 : 32
850
+ }
851
+ })
852
+ }), /* @__PURE__ */ jsx(QueryDevtoolsPanel, {
853
+ client: props.client,
854
+ onClose: () => setOpen(false),
855
+ style: {
856
+ height: "100%",
857
+ width: "100%"
858
+ },
859
+ theme: props.theme
860
+ })]
861
+ });
862
+ }
863
+ /**
864
+ * A toggle button that opens the devtools panel docked to an edge of the
865
+ * viewport. Renders nothing on the server. Open state and size are kept in
866
+ * `localStorage`.
867
+ */
868
+ function QueryDevtools(props) {
869
+ return useIsClient() ? /* @__PURE__ */ jsx(DockedDevtools, {
870
+ buttonPosition: props.buttonPosition,
871
+ client: props.client,
872
+ initialIsOpen: props.initialIsOpen,
873
+ position: props.position,
874
+ theme: props.theme
875
+ }) : null;
876
+ }
877
+ //#endregion
878
+ export { QueryDevtools, QueryDevtoolsPanel, preview };
@@ -0,0 +1,2 @@
1
+ import { DevtoolsTheme, QueryDevtools, QueryDevtoolsPanel, QueryDevtoolsPanelProps, QueryDevtoolsProps } from "./devtools-panel.mjs";
2
+ export { type DevtoolsTheme, QueryDevtools, QueryDevtoolsPanel, type QueryDevtoolsPanelProps, type QueryDevtoolsProps };
@@ -0,0 +1,3 @@
1
+ "use client";
2
+ import { QueryDevtools, QueryDevtoolsPanel } from "./devtools-panel.mjs";
3
+ export { QueryDevtools, QueryDevtoolsPanel };
@@ -0,0 +1,103 @@
1
+ import { useSyncExternalStore } from "react";
2
+ //#region src/devtools-store.ts
3
+ const statuses = [
4
+ "fetching",
5
+ "error",
6
+ "stale",
7
+ "fresh",
8
+ "inactive"
9
+ ];
10
+ const sortOrders = [
11
+ "status",
12
+ "hash",
13
+ "updated"
14
+ ];
15
+ const stores = /* @__PURE__ */ new WeakMap();
16
+ function statusOf(query) {
17
+ const state = query.getState();
18
+ if (state.isFetching) return "fetching";
19
+ if (state.status === "error") return "error";
20
+ if (query.observerCount === 0) return "inactive";
21
+ return query.isStaleNow() ? "stale" : "fresh";
22
+ }
23
+ function rowOf(query) {
24
+ return {
25
+ hash: query.hash,
26
+ observers: query.observerCount,
27
+ query,
28
+ state: query.getState(),
29
+ status: statusOf(query)
30
+ };
31
+ }
32
+ function createStore(client) {
33
+ const listeners = /* @__PURE__ */ new Set();
34
+ let version = 0;
35
+ let snapshotVersion = -1;
36
+ let snapshot = [];
37
+ let scheduled = false;
38
+ client.subscribe(() => {
39
+ version++;
40
+ if (scheduled || listeners.size === 0) return;
41
+ scheduled = true;
42
+ queueMicrotask(() => {
43
+ scheduled = false;
44
+ for (const listener of listeners) listener();
45
+ });
46
+ });
47
+ return {
48
+ getSnapshot: () => {
49
+ if (snapshotVersion !== version) {
50
+ snapshotVersion = version;
51
+ snapshot = client.getAll().map(rowOf);
52
+ }
53
+ return snapshot;
54
+ },
55
+ subscribe: (onChange) => {
56
+ listeners.add(onChange);
57
+ return () => {
58
+ listeners.delete(onChange);
59
+ };
60
+ }
61
+ };
62
+ }
63
+ function storeFor(client) {
64
+ let store = stores.get(client);
65
+ if (!store) {
66
+ store = createStore(client);
67
+ stores.set(client, store);
68
+ }
69
+ return store;
70
+ }
71
+ /** Every cache entry, re-rendering on each batch of cache events. */
72
+ function useQueryRows(client) {
73
+ const store = storeFor(client);
74
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
75
+ }
76
+ function countByStatus(rows) {
77
+ const counts = {
78
+ error: 0,
79
+ fetching: 0,
80
+ fresh: 0,
81
+ inactive: 0,
82
+ stale: 0
83
+ };
84
+ for (const row of rows) counts[row.status]++;
85
+ return counts;
86
+ }
87
+ function compareRows(sortBy) {
88
+ return (a, b) => {
89
+ const primary = {
90
+ hash: 0,
91
+ status: statuses.indexOf(a.status) - statuses.indexOf(b.status),
92
+ updated: b.state.updatedAt - a.state.updatedAt
93
+ }[sortBy];
94
+ return primary === 0 ? a.hash.localeCompare(b.hash) : primary;
95
+ };
96
+ }
97
+ /** Rows whose hash contains `filter` (case-insensitive), sorted. */
98
+ function visibleRows(rows, filter, sortBy) {
99
+ const needle = filter.trim().toLowerCase();
100
+ return rows.filter((row) => row.hash.toLowerCase().includes(needle)).toSorted(compareRows(sortBy));
101
+ }
102
+ //#endregion
103
+ export { countByStatus, sortOrders, statusOf, statuses, useQueryRows, visibleRows };
@@ -0,0 +1,8 @@
1
+ import { DevtoolsTheme, QueryDevtools as QueryDevtools$1, QueryDevtoolsPanel as QueryDevtoolsPanel$1, QueryDevtoolsPanelProps, QueryDevtoolsProps } from "./devtools-panel.mjs";
2
+ //#region src/devtools.d.ts
3
+ /** Toggle button plus docked panel. Renders nothing in production builds. */
4
+ export declare const QueryDevtools: typeof QueryDevtools$1;
5
+ /** The panel alone, to embed anywhere. Renders nothing in production builds. */
6
+ export declare const QueryDevtoolsPanel: typeof QueryDevtoolsPanel$1;
7
+ //#endregion
8
+ export type { DevtoolsTheme, QueryDevtoolsPanelProps, QueryDevtoolsProps };
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { QueryDevtools as QueryDevtools$1, QueryDevtoolsPanel as QueryDevtoolsPanel$1 } from "./devtools-panel.mjs";
3
+ //#region src/devtools.tsx
4
+ function Nothing() {
5
+ return null;
6
+ }
7
+ /** Toggle button plus docked panel. Renders nothing in production builds. */
8
+ const QueryDevtools = process.env.NODE_ENV === "production" ? Nothing : QueryDevtools$1;
9
+ /** The panel alone, to embed anywhere. Renders nothing in production builds. */
10
+ const QueryDevtoolsPanel = process.env.NODE_ENV === "production" ? Nothing : QueryDevtoolsPanel$1;
11
+ //#endregion
12
+ export { QueryDevtools, QueryDevtoolsPanel };
package/dist/provider.mjs CHANGED
@@ -15,6 +15,10 @@ function QueryClientProvider(props) {
15
15
  children: props.children
16
16
  });
17
17
  }
18
+ /** The nearest provider's client, or `null` outside one. */
19
+ function useOptionalQueryClient() {
20
+ return use(QueryClientContext);
21
+ }
18
22
  /** The client from the nearest {@link QueryClientProvider}. */
19
23
  function useQueryClient() {
20
24
  const client = use(QueryClientContext);
@@ -22,4 +26,4 @@ function useQueryClient() {
22
26
  return client;
23
27
  }
24
28
  //#endregion
25
- export { QueryClientProvider, useQueryClient };
29
+ export { QueryClientProvider, useOptionalQueryClient, useQueryClient };
@@ -1,6 +1,6 @@
1
1
  import { canDehydrate, dehydrateQuery, hydrate } from "@raf-query/core";
2
- import { isRedirect } from "@tanstack/react-router";
3
2
  import { Predicate } from "effect";
3
+ import { isRedirect } from "@tanstack/react-router";
4
4
  //#region src/tanstack-router.ts
5
5
  const noop = () => {};
6
6
  function setupServer(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/react",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "React bindings for raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",
@@ -33,12 +33,14 @@
33
33
  "sideEffects": false,
34
34
  "exports": {
35
35
  ".": "./dist/index.mjs",
36
+ "./devtools": "./dist/devtools.mjs",
37
+ "./devtools/production": "./dist/devtools-production.mjs",
36
38
  "./next": "./dist/next.mjs",
37
39
  "./rsc": "./dist/rsc.mjs",
38
40
  "./tanstack-router": "./dist/tanstack-router.mjs"
39
41
  },
40
42
  "dependencies": {
41
- "@raf-query/core": "0.0.6",
43
+ "@raf-query/core": "0.0.8",
42
44
  "effect": "^4.0.1"
43
45
  },
44
46
  "peerDependencies": {