@plasius/graph-client-react 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -1,22 +1,128 @@
1
1
  # @plasius/graph-client-react
2
2
 
3
- React bindings for @plasius/graph-client-core.
3
+ [![npm version](https://img.shields.io/npm/v/@plasius/graph-client-react.svg)](https://www.npmjs.com/package/@plasius/graph-client-react)
4
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/Plasius-LTD/graph-client-react/ci.yml?branch=main&label=build&style=flat)](https://github.com/Plasius-LTD/graph-client-react/actions/workflows/ci.yml)
5
+ [![coverage](https://img.shields.io/codecov/c/github/Plasius-LTD/graph-client-react)](https://codecov.io/gh/Plasius-LTD/graph-client-react)
6
+ [![License](https://img.shields.io/github/license/Plasius-LTD/graph-client-react)](./LICENSE)
7
+ [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-yes-blue.svg)](./CODE_OF_CONDUCT.md)
8
+ [![Security Policy](https://img.shields.io/badge/security%20policy-yes-orange.svg)](./SECURITY.md)
9
+ [![Changelog](https://img.shields.io/badge/changelog-md-blue.svg)](./CHANGELOG.md)
10
+
11
+ [![CI](https://github.com/Plasius-LTD/graph-client-react/actions/workflows/ci.yml/badge.svg)](https://github.com/Plasius-LTD/graph-client-react/actions/workflows/ci.yml)
12
+ [![CD](https://github.com/Plasius-LTD/graph-client-react/actions/workflows/cd.yml/badge.svg)](https://github.com/Plasius-LTD/graph-client-react/actions/workflows/cd.yml)
13
+
14
+ React bindings for `@plasius/graph-client-core` query and mutation workflows.
15
+
16
+ Apache-2.0. ESM + CJS builds. TypeScript types included.
17
+
18
+ ---
19
+
20
+ ## Requirements
21
+
22
+ - Node.js 24+ (matches `.nvmrc` and CI/CD)
23
+ - React 19 (`peerDependencies`)
24
+ - `@plasius/graph-client-core`
25
+
26
+ ---
4
27
 
5
28
  ## Installation
6
29
 
7
- npm install @plasius/graph-client-react
30
+ ```bash
31
+ npm install @plasius/graph-client-react @plasius/graph-client-core
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Exports
37
+
38
+ ```ts
39
+ import {
40
+ GraphClientProvider,
41
+ useGraphClient,
42
+ useGraphQuery,
43
+ useGraphMutation,
44
+ type UseGraphQueryOptions,
45
+ type UseGraphQueryState,
46
+ type UseGraphMutationOptions,
47
+ type GraphMutationClient,
48
+ } from "@plasius/graph-client-react";
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Quick Start
54
+
55
+ ```tsx
56
+ import { GraphClient } from "@plasius/graph-client-core";
57
+ import { GraphClientProvider, useGraphQuery } from "@plasius/graph-client-react";
58
+
59
+ const client = new GraphClient({ transport: { async fetch() { return { data: { ok: true }, version: 1 }; } } });
60
+
61
+ function Profile() {
62
+ const { data, loading, revalidating, error, refresh } = useGraphQuery(
63
+ {
64
+ requests: [{ resolver: "user.profile", key: "user:1" }],
65
+ },
66
+ { retryAttempts: 1, retryDelayMs: 25 },
67
+ );
68
+
69
+ if (loading) return <p>Loading</p>;
70
+ if (error) return <p>{error.message}</p>;
71
+
72
+ return (
73
+ <button onClick={() => void refresh()}>
74
+ {revalidating ? "Refreshing..." : JSON.stringify(data?.results)}
75
+ </button>
76
+ );
77
+ }
78
+
79
+ export function App() {
80
+ return (
81
+ <GraphClientProvider client={client}>
82
+ <Profile />
83
+ </GraphClientProvider>
84
+ );
85
+ }
86
+ ```
87
+
88
+ ---
8
89
 
9
90
  ## Development
10
91
 
92
+ ```bash
93
+ npm run clean
11
94
  npm install
95
+ npm run lint
96
+ npm run typecheck
97
+ npm run test:coverage
12
98
  npm run build
13
- npm test
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Query State Mapping
104
+
105
+ - `loading`: first request with no cached hook data.
106
+ - `revalidating`: refresh/rerender request when prior hook data exists.
107
+ - `status`: `"loading" | "success" | "error"` terminal state for last request.
108
+ - `error`: normalized to `Error`.
109
+
110
+ Suspense is intentionally disabled in current package scope (see ADR-0003). Passing `suspense: true` to `useGraphQuery` throws.
111
+
112
+ Both hooks support optional `telemetry` (from `TelemetrySink`) to emit analytics signals:
113
+
114
+ - Query: `graph.react.query.run`, `graph.react.query.success`, `graph.react.query.retry`, `graph.react.query.error`
115
+ - Mutation: `graph.react.mutation.success`, `graph.react.mutation.error`
116
+
117
+ ---
14
118
 
15
119
  ## Architecture
16
120
 
17
- - ADR documents are in docs/adrs/.
18
- - Cross-package architecture is tracked in plasius-ltd-site/docs/adrs/adr-0020 through adr-0024.
121
+ - Package ADRs: [`docs/adrs`](./docs/adrs)
122
+ - Cross-package ADRs: `plasius-ltd-site/docs/adrs/adr-0020` to `adr-0024`
123
+
124
+ ---
19
125
 
20
126
  ## License
21
127
 
22
- Apache-2.0
128
+ Licensed under the [Apache-2.0 License](./LICENSE).
package/dist/index.cjs CHANGED
@@ -48,24 +48,98 @@ function GraphClientProvider(props) {
48
48
 
49
49
  // src/hooks.ts
50
50
  var import_react3 = require("react");
51
- var useGraphQuery = (query) => {
51
+ var wait = async (delayMs) => {
52
+ if (delayMs <= 0) {
53
+ return;
54
+ }
55
+ await new Promise((resolve) => {
56
+ setTimeout(resolve, delayMs);
57
+ });
58
+ };
59
+ var useGraphQuery = (query, options = {}) => {
52
60
  const client = useGraphClient();
61
+ const retryAttempts = Math.max(0, options.retryAttempts ?? 0);
62
+ const retryDelayMs = Math.max(0, options.retryDelayMs ?? 0);
63
+ const telemetry = options.telemetry;
64
+ if (options.suspense) {
65
+ throw new Error("Suspense mode is not supported by useGraphQuery. Use loading/error/revalidating state mapping.");
66
+ }
53
67
  const [data, setData] = (0, import_react3.useState)(null);
68
+ const dataRef = (0, import_react3.useRef)(null);
54
69
  const [error, setError] = (0, import_react3.useState)(null);
55
70
  const [loading, setLoading] = (0, import_react3.useState)(true);
71
+ const [revalidating, setRevalidating] = (0, import_react3.useState)(false);
72
+ const [status, setStatus] = (0, import_react3.useState)("loading");
73
+ (0, import_react3.useEffect)(() => {
74
+ dataRef.current = data;
75
+ }, [data]);
56
76
  const run = (0, import_react3.useCallback)(async (forceRefresh = false) => {
57
- setLoading(true);
77
+ const startedAt = Date.now();
78
+ const hasExistingData = dataRef.current !== null;
79
+ setLoading(!hasExistingData);
80
+ setRevalidating(hasExistingData);
81
+ if (!hasExistingData) {
82
+ setStatus("loading");
83
+ }
84
+ telemetry?.metric({
85
+ name: "graph.react.query.run",
86
+ value: 1,
87
+ unit: "count",
88
+ tags: {
89
+ forceRefresh: forceRefresh ? "true" : "false",
90
+ hasExistingData: hasExistingData ? "true" : "false"
91
+ }
92
+ });
58
93
  setError(null);
59
- try {
60
- const result = await client.query(query, { allowStale: true, forceRefresh });
61
- setData(result);
62
- } catch (queryError) {
63
- const resolvedError = queryError instanceof Error ? queryError : new Error("Graph query failed");
64
- setError(resolvedError);
65
- } finally {
66
- setLoading(false);
94
+ for (let attempt = 0; attempt <= retryAttempts; attempt += 1) {
95
+ try {
96
+ const result = await client.query(query, { allowStale: true, forceRefresh });
97
+ dataRef.current = result;
98
+ setData(result);
99
+ setStatus("success");
100
+ setLoading(false);
101
+ setRevalidating(false);
102
+ telemetry?.metric({
103
+ name: "graph.react.query.success",
104
+ value: Date.now() - startedAt,
105
+ unit: "ms",
106
+ tags: {
107
+ attempt: String(attempt + 1)
108
+ }
109
+ });
110
+ return;
111
+ } catch (queryError) {
112
+ const resolvedError = queryError instanceof Error ? queryError : new Error("Graph query failed");
113
+ const hasRetriesRemaining = attempt < retryAttempts;
114
+ if (!hasRetriesRemaining) {
115
+ setError(resolvedError);
116
+ setStatus("error");
117
+ setLoading(false);
118
+ setRevalidating(false);
119
+ telemetry?.metric({
120
+ name: "graph.react.query.error",
121
+ value: 1,
122
+ unit: "count"
123
+ });
124
+ telemetry?.error({
125
+ message: resolvedError.message,
126
+ source: "graph-client-react.useGraphQuery",
127
+ code: "GRAPH_REACT_QUERY_FAILED"
128
+ });
129
+ return;
130
+ }
131
+ telemetry?.metric({
132
+ name: "graph.react.query.retry",
133
+ value: 1,
134
+ unit: "count",
135
+ tags: {
136
+ attempt: String(attempt + 1)
137
+ }
138
+ });
139
+ await wait(retryDelayMs);
140
+ }
67
141
  }
68
- }, [client, query]);
142
+ }, [client, query, retryAttempts, retryDelayMs, telemetry]);
69
143
  (0, import_react3.useEffect)(() => {
70
144
  void run(false);
71
145
  }, [run]);
@@ -73,26 +147,47 @@ var useGraphQuery = (query) => {
73
147
  await run(true);
74
148
  }, [run]);
75
149
  return (0, import_react3.useMemo)(
76
- () => ({ data, error, loading, refresh }),
77
- [data, error, loading, refresh]
150
+ () => ({ data, error, loading, revalidating, status, refresh }),
151
+ [data, error, loading, revalidating, status, refresh]
78
152
  );
79
153
  };
80
- var useGraphMutation = (mutationClient) => {
154
+ var useGraphMutation = (mutationClient, options = {}) => {
155
+ const telemetry = options.telemetry;
81
156
  const [loading, setLoading] = (0, import_react3.useState)(false);
82
157
  const [error, setError] = (0, import_react3.useState)(null);
83
158
  const mutate = (0, import_react3.useCallback)(async (command) => {
159
+ const startedAt = Date.now();
84
160
  setLoading(true);
85
161
  setError(null);
86
162
  try {
87
- return await mutationClient.write(command);
163
+ const operation = await mutationClient.write(command);
164
+ telemetry?.metric({
165
+ name: "graph.react.mutation.success",
166
+ value: Date.now() - startedAt,
167
+ unit: "ms",
168
+ tags: {
169
+ state: operation.state
170
+ }
171
+ });
172
+ return operation;
88
173
  } catch (mutationError) {
89
174
  const resolvedError = mutationError instanceof Error ? mutationError : new Error("Graph mutation failed");
90
175
  setError(resolvedError);
176
+ telemetry?.metric({
177
+ name: "graph.react.mutation.error",
178
+ value: 1,
179
+ unit: "count"
180
+ });
181
+ telemetry?.error({
182
+ message: resolvedError.message,
183
+ source: "graph-client-react.useGraphMutation",
184
+ code: "GRAPH_REACT_MUTATION_FAILED"
185
+ });
91
186
  throw resolvedError;
92
187
  } finally {
93
188
  setLoading(false);
94
189
  }
95
- }, [mutationClient]);
190
+ }, [mutationClient, telemetry]);
96
191
  return {
97
192
  mutate,
98
193
  loading,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/context.ts","../src/provider.ts","../src/hooks.ts"],"sourcesContent":["export * from \"./context.js\";\nexport * from \"./provider.js\";\nexport * from \"./hooks.js\";\n","import { createContext, useContext } from \"react\";\n\nimport type { GraphClient } from \"@plasius/graph-client-core\";\n\nexport const GraphClientContext = createContext<GraphClient | null>(null);\n\nexport const useGraphClient = (): GraphClient => {\n const client = useContext(GraphClientContext);\n if (!client) {\n throw new Error(\"GraphClientContext is missing. Wrap your tree in GraphClientProvider.\");\n }\n\n return client;\n};\n","import { createElement, type ReactNode } from \"react\";\n\nimport type { GraphClient } from \"@plasius/graph-client-core\";\nimport { GraphClientContext } from \"./context.js\";\n\nexport interface GraphClientProviderProps {\n client: GraphClient;\n children: ReactNode;\n}\n\nexport function GraphClientProvider(props: GraphClientProviderProps) {\n const { client, children } = props;\n return createElement(GraphClientContext.Provider, { value: client }, children);\n}\n","import { useCallback, useEffect, useMemo, useState } from \"react\";\n\nimport type { GraphQuery, GraphQueryResult, WriteCommand, WriteOperation } from \"@plasius/graph-contracts\";\nimport { useGraphClient } from \"./context.js\";\n\nexport interface UseGraphQueryState {\n data: GraphQueryResult | null;\n error: Error | null;\n loading: boolean;\n refresh: () => Promise<void>;\n}\n\nexport const useGraphQuery = (query: GraphQuery): UseGraphQueryState => {\n const client = useGraphClient();\n const [data, setData] = useState<GraphQueryResult | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState<boolean>(true);\n\n const run = useCallback(async (forceRefresh = false) => {\n setLoading(true);\n setError(null);\n try {\n const result = await client.query(query, { allowStale: true, forceRefresh });\n setData(result);\n } catch (queryError) {\n const resolvedError = queryError instanceof Error ? queryError : new Error(\"Graph query failed\");\n setError(resolvedError);\n } finally {\n setLoading(false);\n }\n }, [client, query]);\n\n useEffect(() => {\n void run(false);\n }, [run]);\n\n const refresh = useCallback(async () => {\n await run(true);\n }, [run]);\n\n return useMemo(\n () => ({ data, error, loading, refresh }),\n [data, error, loading, refresh],\n );\n};\n\nexport interface GraphMutationClient {\n write(command: WriteCommand): Promise<WriteOperation>;\n}\n\nexport const useGraphMutation = (mutationClient: GraphMutationClient) => {\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const mutate = useCallback(async (command: WriteCommand) => {\n setLoading(true);\n setError(null);\n try {\n return await mutationClient.write(command);\n } catch (mutationError) {\n const resolvedError = mutationError instanceof Error ? mutationError : new Error(\"Graph mutation failed\");\n setError(resolvedError);\n throw resolvedError;\n } finally {\n setLoading(false);\n }\n }, [mutationClient]);\n\n return {\n mutate,\n loading,\n error,\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA0C;AAInC,IAAM,yBAAqB,4BAAkC,IAAI;AAEjE,IAAM,iBAAiB,MAAmB;AAC/C,QAAM,aAAS,yBAAW,kBAAkB;AAC5C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,uEAAuE;AAAA,EACzF;AAEA,SAAO;AACT;;;ACbA,IAAAA,gBAA8C;AAUvC,SAAS,oBAAoB,OAAiC;AACnE,QAAM,EAAE,QAAQ,SAAS,IAAI;AAC7B,aAAO,6BAAc,mBAAmB,UAAU,EAAE,OAAO,OAAO,GAAG,QAAQ;AAC/E;;;ACbA,IAAAC,gBAA0D;AAYnD,IAAM,gBAAgB,CAAC,UAA0C;AACtE,QAAM,SAAS,eAAe;AAC9B,QAAM,CAAC,MAAM,OAAO,QAAI,wBAAkC,IAAI;AAC9D,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAuB,IAAI;AACrD,QAAM,CAAC,SAAS,UAAU,QAAI,wBAAkB,IAAI;AAEpD,QAAM,UAAM,2BAAY,OAAO,eAAe,UAAU;AACtD,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,SAAS,MAAM,OAAO,MAAM,OAAO,EAAE,YAAY,MAAM,aAAa,CAAC;AAC3E,cAAQ,MAAM;AAAA,IAChB,SAAS,YAAY;AACnB,YAAM,gBAAgB,sBAAsB,QAAQ,aAAa,IAAI,MAAM,oBAAoB;AAC/F,eAAS,aAAa;AAAA,IACxB,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,+BAAU,MAAM;AACd,SAAK,IAAI,KAAK;AAAA,EAChB,GAAG,CAAC,GAAG,CAAC;AAER,QAAM,cAAU,2BAAY,YAAY;AACtC,UAAM,IAAI,IAAI;AAAA,EAChB,GAAG,CAAC,GAAG,CAAC;AAER,aAAO;AAAA,IACL,OAAO,EAAE,MAAM,OAAO,SAAS,QAAQ;AAAA,IACvC,CAAC,MAAM,OAAO,SAAS,OAAO;AAAA,EAChC;AACF;AAMO,IAAM,mBAAmB,CAAC,mBAAwC;AACvE,QAAM,CAAC,SAAS,UAAU,QAAI,wBAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAuB,IAAI;AAErD,QAAM,aAAS,2BAAY,OAAO,YAA0B;AAC1D,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,aAAO,MAAM,eAAe,MAAM,OAAO;AAAA,IAC3C,SAAS,eAAe;AACtB,YAAM,gBAAgB,yBAAyB,QAAQ,gBAAgB,IAAI,MAAM,uBAAuB;AACxG,eAAS,aAAa;AACtB,YAAM;AAAA,IACR,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,cAAc,CAAC;AAEnB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":["import_react","import_react"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/context.ts","../src/provider.ts","../src/hooks.ts"],"sourcesContent":["export * from \"./context.js\";\nexport * from \"./provider.js\";\nexport * from \"./hooks.js\";\n","import { createContext, useContext } from \"react\";\n\nimport type { GraphClient } from \"@plasius/graph-client-core\";\n\nexport const GraphClientContext = createContext<GraphClient | null>(null);\n\nexport const useGraphClient = (): GraphClient => {\n const client = useContext(GraphClientContext);\n if (!client) {\n throw new Error(\"GraphClientContext is missing. Wrap your tree in GraphClientProvider.\");\n }\n\n return client;\n};\n","import { createElement, type ReactNode } from \"react\";\n\nimport type { GraphClient } from \"@plasius/graph-client-core\";\nimport { GraphClientContext } from \"./context.js\";\n\nexport interface GraphClientProviderProps {\n client: GraphClient;\n children: ReactNode;\n}\n\nexport function GraphClientProvider(props: GraphClientProviderProps) {\n const { client, children } = props;\n return createElement(GraphClientContext.Provider, { value: client }, children);\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport type { GraphQuery, GraphQueryResult, TelemetrySink, WriteCommand, WriteOperation } from \"@plasius/graph-contracts\";\nimport { useGraphClient } from \"./context.js\";\n\nexport interface UseGraphQueryState {\n data: GraphQueryResult | null;\n error: Error | null;\n loading: boolean;\n revalidating: boolean;\n status: \"loading\" | \"success\" | \"error\";\n refresh: () => Promise<void>;\n}\n\nexport interface UseGraphQueryOptions {\n retryAttempts?: number;\n retryDelayMs?: number;\n suspense?: boolean;\n telemetry?: TelemetrySink;\n}\n\nconst wait = async (delayMs: number): Promise<void> => {\n if (delayMs <= 0) {\n return;\n }\n\n await new Promise((resolve) => {\n setTimeout(resolve, delayMs);\n });\n};\n\nexport const useGraphQuery = (query: GraphQuery, options: UseGraphQueryOptions = {}): UseGraphQueryState => {\n const client = useGraphClient();\n const retryAttempts = Math.max(0, options.retryAttempts ?? 0);\n const retryDelayMs = Math.max(0, options.retryDelayMs ?? 0);\n const telemetry = options.telemetry;\n if (options.suspense) {\n throw new Error(\"Suspense mode is not supported by useGraphQuery. Use loading/error/revalidating state mapping.\");\n }\n\n const [data, setData] = useState<GraphQueryResult | null>(null);\n const dataRef = useRef<GraphQueryResult | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState<boolean>(true);\n const [revalidating, setRevalidating] = useState<boolean>(false);\n const [status, setStatus] = useState<\"loading\" | \"success\" | \"error\">(\"loading\");\n\n useEffect(() => {\n dataRef.current = data;\n }, [data]);\n\n const run = useCallback(async (forceRefresh = false) => {\n const startedAt = Date.now();\n const hasExistingData = dataRef.current !== null;\n setLoading(!hasExistingData);\n setRevalidating(hasExistingData);\n if (!hasExistingData) {\n setStatus(\"loading\");\n }\n telemetry?.metric({\n name: \"graph.react.query.run\",\n value: 1,\n unit: \"count\",\n tags: {\n forceRefresh: forceRefresh ? \"true\" : \"false\",\n hasExistingData: hasExistingData ? \"true\" : \"false\",\n },\n });\n setError(null);\n for (let attempt = 0; attempt <= retryAttempts; attempt += 1) {\n try {\n const result = await client.query(query, { allowStale: true, forceRefresh });\n dataRef.current = result;\n setData(result);\n setStatus(\"success\");\n setLoading(false);\n setRevalidating(false);\n telemetry?.metric({\n name: \"graph.react.query.success\",\n value: Date.now() - startedAt,\n unit: \"ms\",\n tags: {\n attempt: String(attempt + 1),\n },\n });\n return;\n } catch (queryError) {\n const resolvedError = queryError instanceof Error ? queryError : new Error(\"Graph query failed\");\n const hasRetriesRemaining = attempt < retryAttempts;\n if (!hasRetriesRemaining) {\n setError(resolvedError);\n setStatus(\"error\");\n setLoading(false);\n setRevalidating(false);\n telemetry?.metric({\n name: \"graph.react.query.error\",\n value: 1,\n unit: \"count\",\n });\n telemetry?.error({\n message: resolvedError.message,\n source: \"graph-client-react.useGraphQuery\",\n code: \"GRAPH_REACT_QUERY_FAILED\",\n });\n return;\n }\n\n telemetry?.metric({\n name: \"graph.react.query.retry\",\n value: 1,\n unit: \"count\",\n tags: {\n attempt: String(attempt + 1),\n },\n });\n await wait(retryDelayMs);\n }\n }\n }, [client, query, retryAttempts, retryDelayMs, telemetry]);\n\n useEffect(() => {\n void run(false);\n }, [run]);\n\n const refresh = useCallback(async () => {\n await run(true);\n }, [run]);\n\n return useMemo(\n () => ({ data, error, loading, revalidating, status, refresh }),\n [data, error, loading, revalidating, status, refresh],\n );\n};\n\nexport interface GraphMutationClient {\n write(command: WriteCommand): Promise<WriteOperation>;\n}\n\nexport interface UseGraphMutationOptions {\n telemetry?: TelemetrySink;\n}\n\nexport const useGraphMutation = (mutationClient: GraphMutationClient, options: UseGraphMutationOptions = {}) => {\n const telemetry = options.telemetry;\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const mutate = useCallback(async (command: WriteCommand) => {\n const startedAt = Date.now();\n setLoading(true);\n setError(null);\n try {\n const operation = await mutationClient.write(command);\n telemetry?.metric({\n name: \"graph.react.mutation.success\",\n value: Date.now() - startedAt,\n unit: \"ms\",\n tags: {\n state: operation.state,\n },\n });\n return operation;\n } catch (mutationError) {\n const resolvedError = mutationError instanceof Error ? mutationError : new Error(\"Graph mutation failed\");\n setError(resolvedError);\n telemetry?.metric({\n name: \"graph.react.mutation.error\",\n value: 1,\n unit: \"count\",\n });\n telemetry?.error({\n message: resolvedError.message,\n source: \"graph-client-react.useGraphMutation\",\n code: \"GRAPH_REACT_MUTATION_FAILED\",\n });\n throw resolvedError;\n } finally {\n setLoading(false);\n }\n }, [mutationClient, telemetry]);\n\n return {\n mutate,\n loading,\n error,\n };\n};\n"],"mappings":"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","names":["import_react","import_react"]}
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { GraphClient } from '@plasius/graph-client-core';
4
- import { WriteCommand, WriteOperation, GraphQueryResult, GraphQuery } from '@plasius/graph-contracts';
4
+ import { WriteCommand, WriteOperation, TelemetrySink, GraphQueryResult, GraphQuery } from '@plasius/graph-contracts';
5
5
 
6
6
  declare const GraphClientContext: react.Context<GraphClient | null>;
7
7
  declare const useGraphClient: () => GraphClient;
@@ -16,16 +16,27 @@ interface UseGraphQueryState {
16
16
  data: GraphQueryResult | null;
17
17
  error: Error | null;
18
18
  loading: boolean;
19
+ revalidating: boolean;
20
+ status: "loading" | "success" | "error";
19
21
  refresh: () => Promise<void>;
20
22
  }
21
- declare const useGraphQuery: (query: GraphQuery) => UseGraphQueryState;
23
+ interface UseGraphQueryOptions {
24
+ retryAttempts?: number;
25
+ retryDelayMs?: number;
26
+ suspense?: boolean;
27
+ telemetry?: TelemetrySink;
28
+ }
29
+ declare const useGraphQuery: (query: GraphQuery, options?: UseGraphQueryOptions) => UseGraphQueryState;
22
30
  interface GraphMutationClient {
23
31
  write(command: WriteCommand): Promise<WriteOperation>;
24
32
  }
25
- declare const useGraphMutation: (mutationClient: GraphMutationClient) => {
33
+ interface UseGraphMutationOptions {
34
+ telemetry?: TelemetrySink;
35
+ }
36
+ declare const useGraphMutation: (mutationClient: GraphMutationClient, options?: UseGraphMutationOptions) => {
26
37
  mutate: (command: WriteCommand) => Promise<WriteOperation>;
27
38
  loading: boolean;
28
39
  error: Error | null;
29
40
  };
30
41
 
31
- export { GraphClientContext, GraphClientProvider, type GraphClientProviderProps, type GraphMutationClient, type UseGraphQueryState, useGraphClient, useGraphMutation, useGraphQuery };
42
+ export { GraphClientContext, GraphClientProvider, type GraphClientProviderProps, type GraphMutationClient, type UseGraphMutationOptions, type UseGraphQueryOptions, type UseGraphQueryState, useGraphClient, useGraphMutation, useGraphQuery };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { GraphClient } from '@plasius/graph-client-core';
4
- import { WriteCommand, WriteOperation, GraphQueryResult, GraphQuery } from '@plasius/graph-contracts';
4
+ import { WriteCommand, WriteOperation, TelemetrySink, GraphQueryResult, GraphQuery } from '@plasius/graph-contracts';
5
5
 
6
6
  declare const GraphClientContext: react.Context<GraphClient | null>;
7
7
  declare const useGraphClient: () => GraphClient;
@@ -16,16 +16,27 @@ interface UseGraphQueryState {
16
16
  data: GraphQueryResult | null;
17
17
  error: Error | null;
18
18
  loading: boolean;
19
+ revalidating: boolean;
20
+ status: "loading" | "success" | "error";
19
21
  refresh: () => Promise<void>;
20
22
  }
21
- declare const useGraphQuery: (query: GraphQuery) => UseGraphQueryState;
23
+ interface UseGraphQueryOptions {
24
+ retryAttempts?: number;
25
+ retryDelayMs?: number;
26
+ suspense?: boolean;
27
+ telemetry?: TelemetrySink;
28
+ }
29
+ declare const useGraphQuery: (query: GraphQuery, options?: UseGraphQueryOptions) => UseGraphQueryState;
22
30
  interface GraphMutationClient {
23
31
  write(command: WriteCommand): Promise<WriteOperation>;
24
32
  }
25
- declare const useGraphMutation: (mutationClient: GraphMutationClient) => {
33
+ interface UseGraphMutationOptions {
34
+ telemetry?: TelemetrySink;
35
+ }
36
+ declare const useGraphMutation: (mutationClient: GraphMutationClient, options?: UseGraphMutationOptions) => {
26
37
  mutate: (command: WriteCommand) => Promise<WriteOperation>;
27
38
  loading: boolean;
28
39
  error: Error | null;
29
40
  };
30
41
 
31
- export { GraphClientContext, GraphClientProvider, type GraphClientProviderProps, type GraphMutationClient, type UseGraphQueryState, useGraphClient, useGraphMutation, useGraphQuery };
42
+ export { GraphClientContext, GraphClientProvider, type GraphClientProviderProps, type GraphMutationClient, type UseGraphMutationOptions, type UseGraphQueryOptions, type UseGraphQueryState, useGraphClient, useGraphMutation, useGraphQuery };
package/dist/index.js CHANGED
@@ -17,25 +17,99 @@ function GraphClientProvider(props) {
17
17
  }
18
18
 
19
19
  // src/hooks.ts
20
- import { useCallback, useEffect, useMemo, useState } from "react";
21
- var useGraphQuery = (query) => {
20
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
21
+ var wait = async (delayMs) => {
22
+ if (delayMs <= 0) {
23
+ return;
24
+ }
25
+ await new Promise((resolve) => {
26
+ setTimeout(resolve, delayMs);
27
+ });
28
+ };
29
+ var useGraphQuery = (query, options = {}) => {
22
30
  const client = useGraphClient();
31
+ const retryAttempts = Math.max(0, options.retryAttempts ?? 0);
32
+ const retryDelayMs = Math.max(0, options.retryDelayMs ?? 0);
33
+ const telemetry = options.telemetry;
34
+ if (options.suspense) {
35
+ throw new Error("Suspense mode is not supported by useGraphQuery. Use loading/error/revalidating state mapping.");
36
+ }
23
37
  const [data, setData] = useState(null);
38
+ const dataRef = useRef(null);
24
39
  const [error, setError] = useState(null);
25
40
  const [loading, setLoading] = useState(true);
41
+ const [revalidating, setRevalidating] = useState(false);
42
+ const [status, setStatus] = useState("loading");
43
+ useEffect(() => {
44
+ dataRef.current = data;
45
+ }, [data]);
26
46
  const run = useCallback(async (forceRefresh = false) => {
27
- setLoading(true);
47
+ const startedAt = Date.now();
48
+ const hasExistingData = dataRef.current !== null;
49
+ setLoading(!hasExistingData);
50
+ setRevalidating(hasExistingData);
51
+ if (!hasExistingData) {
52
+ setStatus("loading");
53
+ }
54
+ telemetry?.metric({
55
+ name: "graph.react.query.run",
56
+ value: 1,
57
+ unit: "count",
58
+ tags: {
59
+ forceRefresh: forceRefresh ? "true" : "false",
60
+ hasExistingData: hasExistingData ? "true" : "false"
61
+ }
62
+ });
28
63
  setError(null);
29
- try {
30
- const result = await client.query(query, { allowStale: true, forceRefresh });
31
- setData(result);
32
- } catch (queryError) {
33
- const resolvedError = queryError instanceof Error ? queryError : new Error("Graph query failed");
34
- setError(resolvedError);
35
- } finally {
36
- setLoading(false);
64
+ for (let attempt = 0; attempt <= retryAttempts; attempt += 1) {
65
+ try {
66
+ const result = await client.query(query, { allowStale: true, forceRefresh });
67
+ dataRef.current = result;
68
+ setData(result);
69
+ setStatus("success");
70
+ setLoading(false);
71
+ setRevalidating(false);
72
+ telemetry?.metric({
73
+ name: "graph.react.query.success",
74
+ value: Date.now() - startedAt,
75
+ unit: "ms",
76
+ tags: {
77
+ attempt: String(attempt + 1)
78
+ }
79
+ });
80
+ return;
81
+ } catch (queryError) {
82
+ const resolvedError = queryError instanceof Error ? queryError : new Error("Graph query failed");
83
+ const hasRetriesRemaining = attempt < retryAttempts;
84
+ if (!hasRetriesRemaining) {
85
+ setError(resolvedError);
86
+ setStatus("error");
87
+ setLoading(false);
88
+ setRevalidating(false);
89
+ telemetry?.metric({
90
+ name: "graph.react.query.error",
91
+ value: 1,
92
+ unit: "count"
93
+ });
94
+ telemetry?.error({
95
+ message: resolvedError.message,
96
+ source: "graph-client-react.useGraphQuery",
97
+ code: "GRAPH_REACT_QUERY_FAILED"
98
+ });
99
+ return;
100
+ }
101
+ telemetry?.metric({
102
+ name: "graph.react.query.retry",
103
+ value: 1,
104
+ unit: "count",
105
+ tags: {
106
+ attempt: String(attempt + 1)
107
+ }
108
+ });
109
+ await wait(retryDelayMs);
110
+ }
37
111
  }
38
- }, [client, query]);
112
+ }, [client, query, retryAttempts, retryDelayMs, telemetry]);
39
113
  useEffect(() => {
40
114
  void run(false);
41
115
  }, [run]);
@@ -43,26 +117,47 @@ var useGraphQuery = (query) => {
43
117
  await run(true);
44
118
  }, [run]);
45
119
  return useMemo(
46
- () => ({ data, error, loading, refresh }),
47
- [data, error, loading, refresh]
120
+ () => ({ data, error, loading, revalidating, status, refresh }),
121
+ [data, error, loading, revalidating, status, refresh]
48
122
  );
49
123
  };
50
- var useGraphMutation = (mutationClient) => {
124
+ var useGraphMutation = (mutationClient, options = {}) => {
125
+ const telemetry = options.telemetry;
51
126
  const [loading, setLoading] = useState(false);
52
127
  const [error, setError] = useState(null);
53
128
  const mutate = useCallback(async (command) => {
129
+ const startedAt = Date.now();
54
130
  setLoading(true);
55
131
  setError(null);
56
132
  try {
57
- return await mutationClient.write(command);
133
+ const operation = await mutationClient.write(command);
134
+ telemetry?.metric({
135
+ name: "graph.react.mutation.success",
136
+ value: Date.now() - startedAt,
137
+ unit: "ms",
138
+ tags: {
139
+ state: operation.state
140
+ }
141
+ });
142
+ return operation;
58
143
  } catch (mutationError) {
59
144
  const resolvedError = mutationError instanceof Error ? mutationError : new Error("Graph mutation failed");
60
145
  setError(resolvedError);
146
+ telemetry?.metric({
147
+ name: "graph.react.mutation.error",
148
+ value: 1,
149
+ unit: "count"
150
+ });
151
+ telemetry?.error({
152
+ message: resolvedError.message,
153
+ source: "graph-client-react.useGraphMutation",
154
+ code: "GRAPH_REACT_MUTATION_FAILED"
155
+ });
61
156
  throw resolvedError;
62
157
  } finally {
63
158
  setLoading(false);
64
159
  }
65
- }, [mutationClient]);
160
+ }, [mutationClient, telemetry]);
66
161
  return {
67
162
  mutate,
68
163
  loading,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/context.ts","../src/provider.ts","../src/hooks.ts"],"sourcesContent":["import { createContext, useContext } from \"react\";\n\nimport type { GraphClient } from \"@plasius/graph-client-core\";\n\nexport const GraphClientContext = createContext<GraphClient | null>(null);\n\nexport const useGraphClient = (): GraphClient => {\n const client = useContext(GraphClientContext);\n if (!client) {\n throw new Error(\"GraphClientContext is missing. Wrap your tree in GraphClientProvider.\");\n }\n\n return client;\n};\n","import { createElement, type ReactNode } from \"react\";\n\nimport type { GraphClient } from \"@plasius/graph-client-core\";\nimport { GraphClientContext } from \"./context.js\";\n\nexport interface GraphClientProviderProps {\n client: GraphClient;\n children: ReactNode;\n}\n\nexport function GraphClientProvider(props: GraphClientProviderProps) {\n const { client, children } = props;\n return createElement(GraphClientContext.Provider, { value: client }, children);\n}\n","import { useCallback, useEffect, useMemo, useState } from \"react\";\n\nimport type { GraphQuery, GraphQueryResult, WriteCommand, WriteOperation } from \"@plasius/graph-contracts\";\nimport { useGraphClient } from \"./context.js\";\n\nexport interface UseGraphQueryState {\n data: GraphQueryResult | null;\n error: Error | null;\n loading: boolean;\n refresh: () => Promise<void>;\n}\n\nexport const useGraphQuery = (query: GraphQuery): UseGraphQueryState => {\n const client = useGraphClient();\n const [data, setData] = useState<GraphQueryResult | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState<boolean>(true);\n\n const run = useCallback(async (forceRefresh = false) => {\n setLoading(true);\n setError(null);\n try {\n const result = await client.query(query, { allowStale: true, forceRefresh });\n setData(result);\n } catch (queryError) {\n const resolvedError = queryError instanceof Error ? queryError : new Error(\"Graph query failed\");\n setError(resolvedError);\n } finally {\n setLoading(false);\n }\n }, [client, query]);\n\n useEffect(() => {\n void run(false);\n }, [run]);\n\n const refresh = useCallback(async () => {\n await run(true);\n }, [run]);\n\n return useMemo(\n () => ({ data, error, loading, refresh }),\n [data, error, loading, refresh],\n );\n};\n\nexport interface GraphMutationClient {\n write(command: WriteCommand): Promise<WriteOperation>;\n}\n\nexport const useGraphMutation = (mutationClient: GraphMutationClient) => {\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const mutate = useCallback(async (command: WriteCommand) => {\n setLoading(true);\n setError(null);\n try {\n return await mutationClient.write(command);\n } catch (mutationError) {\n const resolvedError = mutationError instanceof Error ? mutationError : new Error(\"Graph mutation failed\");\n setError(resolvedError);\n throw resolvedError;\n } finally {\n setLoading(false);\n }\n }, [mutationClient]);\n\n return {\n mutate,\n loading,\n error,\n };\n};\n"],"mappings":";AAAA,SAAS,eAAe,kBAAkB;AAInC,IAAM,qBAAqB,cAAkC,IAAI;AAEjE,IAAM,iBAAiB,MAAmB;AAC/C,QAAM,SAAS,WAAW,kBAAkB;AAC5C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,uEAAuE;AAAA,EACzF;AAEA,SAAO;AACT;;;ACbA,SAAS,qBAAqC;AAUvC,SAAS,oBAAoB,OAAiC;AACnE,QAAM,EAAE,QAAQ,SAAS,IAAI;AAC7B,SAAO,cAAc,mBAAmB,UAAU,EAAE,OAAO,OAAO,GAAG,QAAQ;AAC/E;;;ACbA,SAAS,aAAa,WAAW,SAAS,gBAAgB;AAYnD,IAAM,gBAAgB,CAAC,UAA0C;AACtE,QAAM,SAAS,eAAe;AAC9B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAkC,IAAI;AAC9D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAkB,IAAI;AAEpD,QAAM,MAAM,YAAY,OAAO,eAAe,UAAU;AACtD,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,SAAS,MAAM,OAAO,MAAM,OAAO,EAAE,YAAY,MAAM,aAAa,CAAC;AAC3E,cAAQ,MAAM;AAAA,IAChB,SAAS,YAAY;AACnB,YAAM,gBAAgB,sBAAsB,QAAQ,aAAa,IAAI,MAAM,oBAAoB;AAC/F,eAAS,aAAa;AAAA,IACxB,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,YAAU,MAAM;AACd,SAAK,IAAI,KAAK;AAAA,EAChB,GAAG,CAAC,GAAG,CAAC;AAER,QAAM,UAAU,YAAY,YAAY;AACtC,UAAM,IAAI,IAAI;AAAA,EAChB,GAAG,CAAC,GAAG,CAAC;AAER,SAAO;AAAA,IACL,OAAO,EAAE,MAAM,OAAO,SAAS,QAAQ;AAAA,IACvC,CAAC,MAAM,OAAO,SAAS,OAAO;AAAA,EAChC;AACF;AAMO,IAAM,mBAAmB,CAAC,mBAAwC;AACvE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AAErD,QAAM,SAAS,YAAY,OAAO,YAA0B;AAC1D,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,aAAO,MAAM,eAAe,MAAM,OAAO;AAAA,IAC3C,SAAS,eAAe;AACtB,YAAM,gBAAgB,yBAAyB,QAAQ,gBAAgB,IAAI,MAAM,uBAAuB;AACxG,eAAS,aAAa;AACtB,YAAM;AAAA,IACR,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,cAAc,CAAC;AAEnB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/context.ts","../src/provider.ts","../src/hooks.ts"],"sourcesContent":["import { createContext, useContext } from \"react\";\n\nimport type { GraphClient } from \"@plasius/graph-client-core\";\n\nexport const GraphClientContext = createContext<GraphClient | null>(null);\n\nexport const useGraphClient = (): GraphClient => {\n const client = useContext(GraphClientContext);\n if (!client) {\n throw new Error(\"GraphClientContext is missing. Wrap your tree in GraphClientProvider.\");\n }\n\n return client;\n};\n","import { createElement, type ReactNode } from \"react\";\n\nimport type { GraphClient } from \"@plasius/graph-client-core\";\nimport { GraphClientContext } from \"./context.js\";\n\nexport interface GraphClientProviderProps {\n client: GraphClient;\n children: ReactNode;\n}\n\nexport function GraphClientProvider(props: GraphClientProviderProps) {\n const { client, children } = props;\n return createElement(GraphClientContext.Provider, { value: client }, children);\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport type { GraphQuery, GraphQueryResult, TelemetrySink, WriteCommand, WriteOperation } from \"@plasius/graph-contracts\";\nimport { useGraphClient } from \"./context.js\";\n\nexport interface UseGraphQueryState {\n data: GraphQueryResult | null;\n error: Error | null;\n loading: boolean;\n revalidating: boolean;\n status: \"loading\" | \"success\" | \"error\";\n refresh: () => Promise<void>;\n}\n\nexport interface UseGraphQueryOptions {\n retryAttempts?: number;\n retryDelayMs?: number;\n suspense?: boolean;\n telemetry?: TelemetrySink;\n}\n\nconst wait = async (delayMs: number): Promise<void> => {\n if (delayMs <= 0) {\n return;\n }\n\n await new Promise((resolve) => {\n setTimeout(resolve, delayMs);\n });\n};\n\nexport const useGraphQuery = (query: GraphQuery, options: UseGraphQueryOptions = {}): UseGraphQueryState => {\n const client = useGraphClient();\n const retryAttempts = Math.max(0, options.retryAttempts ?? 0);\n const retryDelayMs = Math.max(0, options.retryDelayMs ?? 0);\n const telemetry = options.telemetry;\n if (options.suspense) {\n throw new Error(\"Suspense mode is not supported by useGraphQuery. Use loading/error/revalidating state mapping.\");\n }\n\n const [data, setData] = useState<GraphQueryResult | null>(null);\n const dataRef = useRef<GraphQueryResult | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState<boolean>(true);\n const [revalidating, setRevalidating] = useState<boolean>(false);\n const [status, setStatus] = useState<\"loading\" | \"success\" | \"error\">(\"loading\");\n\n useEffect(() => {\n dataRef.current = data;\n }, [data]);\n\n const run = useCallback(async (forceRefresh = false) => {\n const startedAt = Date.now();\n const hasExistingData = dataRef.current !== null;\n setLoading(!hasExistingData);\n setRevalidating(hasExistingData);\n if (!hasExistingData) {\n setStatus(\"loading\");\n }\n telemetry?.metric({\n name: \"graph.react.query.run\",\n value: 1,\n unit: \"count\",\n tags: {\n forceRefresh: forceRefresh ? \"true\" : \"false\",\n hasExistingData: hasExistingData ? \"true\" : \"false\",\n },\n });\n setError(null);\n for (let attempt = 0; attempt <= retryAttempts; attempt += 1) {\n try {\n const result = await client.query(query, { allowStale: true, forceRefresh });\n dataRef.current = result;\n setData(result);\n setStatus(\"success\");\n setLoading(false);\n setRevalidating(false);\n telemetry?.metric({\n name: \"graph.react.query.success\",\n value: Date.now() - startedAt,\n unit: \"ms\",\n tags: {\n attempt: String(attempt + 1),\n },\n });\n return;\n } catch (queryError) {\n const resolvedError = queryError instanceof Error ? queryError : new Error(\"Graph query failed\");\n const hasRetriesRemaining = attempt < retryAttempts;\n if (!hasRetriesRemaining) {\n setError(resolvedError);\n setStatus(\"error\");\n setLoading(false);\n setRevalidating(false);\n telemetry?.metric({\n name: \"graph.react.query.error\",\n value: 1,\n unit: \"count\",\n });\n telemetry?.error({\n message: resolvedError.message,\n source: \"graph-client-react.useGraphQuery\",\n code: \"GRAPH_REACT_QUERY_FAILED\",\n });\n return;\n }\n\n telemetry?.metric({\n name: \"graph.react.query.retry\",\n value: 1,\n unit: \"count\",\n tags: {\n attempt: String(attempt + 1),\n },\n });\n await wait(retryDelayMs);\n }\n }\n }, [client, query, retryAttempts, retryDelayMs, telemetry]);\n\n useEffect(() => {\n void run(false);\n }, [run]);\n\n const refresh = useCallback(async () => {\n await run(true);\n }, [run]);\n\n return useMemo(\n () => ({ data, error, loading, revalidating, status, refresh }),\n [data, error, loading, revalidating, status, refresh],\n );\n};\n\nexport interface GraphMutationClient {\n write(command: WriteCommand): Promise<WriteOperation>;\n}\n\nexport interface UseGraphMutationOptions {\n telemetry?: TelemetrySink;\n}\n\nexport const useGraphMutation = (mutationClient: GraphMutationClient, options: UseGraphMutationOptions = {}) => {\n const telemetry = options.telemetry;\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const mutate = useCallback(async (command: WriteCommand) => {\n const startedAt = Date.now();\n setLoading(true);\n setError(null);\n try {\n const operation = await mutationClient.write(command);\n telemetry?.metric({\n name: \"graph.react.mutation.success\",\n value: Date.now() - startedAt,\n unit: \"ms\",\n tags: {\n state: operation.state,\n },\n });\n return operation;\n } catch (mutationError) {\n const resolvedError = mutationError instanceof Error ? mutationError : new Error(\"Graph mutation failed\");\n setError(resolvedError);\n telemetry?.metric({\n name: \"graph.react.mutation.error\",\n value: 1,\n unit: \"count\",\n });\n telemetry?.error({\n message: resolvedError.message,\n source: \"graph-client-react.useGraphMutation\",\n code: \"GRAPH_REACT_MUTATION_FAILED\",\n });\n throw resolvedError;\n } finally {\n setLoading(false);\n }\n }, [mutationClient, telemetry]);\n\n return {\n mutate,\n loading,\n error,\n };\n};\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/graph-client-react",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "React bindings for @plasius/graph-client-core",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -49,33 +49,33 @@
49
49
  },
50
50
  "homepage": "https://github.com/Plasius-LTD/graph-client-react#readme",
51
51
  "dependencies": {
52
- "@plasius/graph-client-core": "^0.1.0",
53
- "@plasius/graph-contracts": "^0.1.0"
52
+ "@plasius/graph-client-core": "^0.1.1",
53
+ "@plasius/graph-contracts": "^0.1.1"
54
54
  },
55
55
  "peerDependencies": {
56
- "react": "^19.1.0",
57
- "react-dom": "^19.1.0"
56
+ "react": "^19",
57
+ "react-dom": "^19"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@eslint/js": "^10.0.1",
61
61
  "@testing-library/dom": "^10.4.1",
62
62
  "@testing-library/react": "^16.3.0",
63
- "@types/node": "^24.3.1",
63
+ "@types/node": "^25.5.0",
64
64
  "@types/react": "^19.1.8",
65
65
  "@types/react-dom": "^19.1.2",
66
- "@typescript-eslint/eslint-plugin": "^8.56.0",
67
- "@typescript-eslint/parser": "^8.56.0",
68
- "@vitest/coverage-v8": "^4.0.18",
69
- "eslint": "^10.0.1",
66
+ "@typescript-eslint/eslint-plugin": "^8.58.0",
67
+ "@typescript-eslint/parser": "^8.58.0",
68
+ "@vitest/coverage-v8": "^4.1.2",
69
+ "eslint": "^10.1.0",
70
70
  "globals": "^17.3.0",
71
- "jsdom": "^26.1.0",
72
- "react": "^19.1.0",
73
- "react-dom": "^19.1.0",
71
+ "jsdom": "^29.0.1",
72
+ "react": "^19.2.4",
73
+ "react-dom": "^19.2.4",
74
74
  "rimraf": "^6.0.1",
75
75
  "tsup": "^8.5.0",
76
76
  "tsx": "^4.20.5",
77
- "typescript": "^5.9.2",
78
- "vitest": "^4.0.18"
77
+ "typescript": "^6.0.2",
78
+ "vitest": "^4.1.2"
79
79
  },
80
80
  "publishConfig": {
81
81
  "access": "public"
@@ -92,5 +92,8 @@
92
92
  ],
93
93
  "overrides": {
94
94
  "minimatch": "^10.2.1"
95
+ },
96
+ "engines": {
97
+ "node": ">=24"
95
98
  }
96
99
  }