@classytic/arc-next 0.11.1 → 0.12.0

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/dist/mutation.js CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  import { getQuotaDetails, isArcApiError, isAutoIdempotency } from "./client.js";
4
4
  import { useMutation, useQueryClient } from "@tanstack/react-query";
5
- import { useCallback, useRef, useTransition } from "react";
5
+ import { useRef, useTransition } from "react";
6
6
 
7
7
  //#region src/mutation.ts
8
8
  let toastHandler = {
9
- success: (msg) => console.log("[Success]", msg),
10
- error: (msg) => console.error("[Error]", msg)
9
+ success: () => {},
10
+ error: () => {}
11
11
  };
12
12
  /**
13
13
  * Configure toast handler. Call once at app init.
@@ -78,9 +78,11 @@ function useMutationWithTransition(config) {
78
78
  },
79
79
  onSuccess: (data, variables) => {
80
80
  const invalidate = () => {
81
- invalidateQueries.forEach((key) => queryClient.invalidateQueries({ queryKey: key }));
81
+ invalidateQueries.forEach((key) => {
82
+ queryClient.invalidateQueries({ queryKey: key });
83
+ });
82
84
  };
83
- if (withTransition && invalidateQueries.length > 0) startTransition(invalidate);
85
+ if (invalidateQueries.length > 0 && (config.shouldInvalidate?.(data) ?? true)) if (withTransition) startTransition(invalidate);
84
86
  else invalidate();
85
87
  if (toast && (config.shouldToast?.() ?? true)) showToast("success", messages, data, variables, void 0, instanceToast);
86
88
  onSuccess?.(data, variables);
@@ -121,12 +123,16 @@ function useMutationWithOptimistic(config) {
121
123
  return { previous };
122
124
  },
123
125
  onSuccess: (data, variables) => {
124
- queryKeys.forEach((key) => queryClient.invalidateQueries({ queryKey: key }));
126
+ queryKeys.forEach((key) => {
127
+ queryClient.invalidateQueries({ queryKey: key });
128
+ });
125
129
  if (toast) showToast("success", messages, data, variables, void 0, instanceToast);
126
130
  onSuccess?.(data, variables);
127
131
  },
128
132
  onError: (error, variables, context) => {
129
- context?.previous?.forEach(({ key, data }) => queryClient.setQueryData(key, data));
133
+ context?.previous?.forEach(({ key, data }) => {
134
+ queryClient.setQueryData(key, data);
135
+ });
130
136
  if (toast) showToast("error", messages, null, variables, error, instanceToast);
131
137
  onError?.(error, variables);
132
138
  },
@@ -146,9 +152,15 @@ function useMutationWithOptimistic(config) {
146
152
  };
147
153
  }
148
154
  function useOptimisticMutation(config) {
149
- const { mutationFn, queryClient, queryKeys, optimisticUpdate, onSuccess, onError, onSettled, messages, toastHandler: instanceToast } = config;
155
+ const { mutationFn, queryClient, queryKeys, optimisticUpdate, mutationKey, reconcile, onSuccess, onError, onSettled, messages, toastHandler: instanceToast } = config;
156
+ const invalidateAll = () => {
157
+ queryKeys.forEach((key) => {
158
+ queryClient.invalidateQueries({ queryKey: key });
159
+ });
160
+ };
150
161
  return useMutation({
151
162
  mutationFn,
163
+ ...mutationKey ? { mutationKey } : {},
152
164
  onMutate: async (variables) => {
153
165
  await Promise.all(queryKeys.map((key) => queryClient.cancelQueries({
154
166
  queryKey: key,
@@ -160,24 +172,29 @@ function useOptimisticMutation(config) {
160
172
  }));
161
173
  if (optimisticUpdate) queryKeys.forEach((key) => {
162
174
  queryClient.getQueriesData({ queryKey: key }).forEach(([qKey, qData]) => {
163
- queryClient.setQueryData(qKey, optimisticUpdate(qData, variables));
175
+ const next = optimisticUpdate(qData, variables, qKey);
176
+ if (next !== qData) queryClient.setQueryData(qKey, next);
164
177
  });
165
178
  });
166
179
  return { previous };
167
180
  },
168
181
  onSuccess: (data, variables) => {
169
182
  if ((config.shouldToast?.() ?? true) && messages?.success) showToast("success", messages, data, variables, void 0, instanceToast);
170
- queryKeys.forEach((key) => queryClient.invalidateQueries({ queryKey: key }));
183
+ reconcile?.(data, variables);
184
+ if (!mutationKey) invalidateAll();
171
185
  onSuccess?.(data, variables);
172
186
  },
173
187
  onError: (error, variables, context) => {
174
188
  context?.previous?.forEach(({ data }) => {
175
- data.forEach(([qKey, qData]) => queryClient.setQueryData(qKey, qData));
189
+ data.forEach(([qKey, qData]) => {
190
+ queryClient.setQueryData(qKey, qData);
191
+ });
176
192
  });
177
193
  if (config.shouldToast?.() ?? true) showToast("error", messages, null, variables, error, instanceToast);
178
194
  onError?.(error, variables);
179
195
  },
180
196
  onSettled: (data, error, variables) => {
197
+ if (mutationKey && queryClient.isMutating({ mutationKey }) === 1) invalidateAll();
181
198
  onSettled?.(data, error, variables);
182
199
  }
183
200
  });
@@ -6,7 +6,7 @@ interface HistoryEntry {
6
6
  id: string;
7
7
  resource: string;
8
8
  documentId: string;
9
- action: 'create' | 'update' | 'delete' | 'restore' | 'custom';
9
+ action: "create" | "update" | "delete" | "restore" | "custom";
10
10
  userId?: string;
11
11
  organizationId?: string;
12
12
  before?: Record<string, unknown>;
package/dist/query.d.ts CHANGED
@@ -286,7 +286,7 @@ interface UseApiQueryOptions {
286
286
  refetchIntervalInBackground?: boolean;
287
287
  retry?: boolean | number;
288
288
  /** Limit re-renders to changes in these specific fields (perf optimization). */
289
- notifyOnChangeProps?: ('data' | 'error' | 'isLoading' | 'isFetching' | 'isError' | 'isSuccess' | 'isStale')[];
289
+ notifyOnChangeProps?: ("data" | "error" | "isLoading" | "isFetching" | "isError" | "isSuccess" | "isStale")[];
290
290
  }
291
291
  interface UseApiQueryConfig<TResponse, TData> {
292
292
  queryKey: QueryKey;
package/dist/sse.js CHANGED
@@ -166,7 +166,7 @@ function subscribeToEvents(options) {
166
166
  const scheduleReconnect = () => {
167
167
  if (reconnectAttempts < maxReconnectAttempts) {
168
168
  reconnectAttempts += 1;
169
- const delay = Math.min(reconnectDelay * Math.pow(1.5, reconnectAttempts - 1), 3e4);
169
+ const delay = Math.min(reconnectDelay * 1.5 ** (reconnectAttempts - 1), 3e4);
170
170
  reconnectTimer = setTimeout(connect, delay);
171
171
  }
172
172
  };
package/dist/upload.d.ts CHANGED
@@ -71,6 +71,15 @@ interface UploadWithProgressOptions {
71
71
  * an image and returns the resized binary). Default: false (parse JSON).
72
72
  */
73
73
  responseType?: "json" | "text" | "blob";
74
+ /**
75
+ * Upload timeout in ms, assigned to `xhr.timeout` — the whole request
76
+ * (upload + server processing + response) must finish within this window
77
+ * or the promise rejects with a `TimeoutError`. Default: disabled (0) —
78
+ * large files on slow links legitimately take minutes; size it to your
79
+ * payload ceiling when you enable it. (`ClientConfig.timeoutMs` does NOT
80
+ * apply to uploads — the fetch pipeline and XHR transport stay isolated.)
81
+ */
82
+ timeoutMs?: number;
74
83
  }
75
84
  /**
76
85
  * Upload a `FormData` payload via XHR with native progress events.
package/dist/upload.js CHANGED
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { ArcApiError, _getAuthErrorHandler, _isAuthRecoverable, _resolveRefreshedToken, _runAuthRecovery, getAuthMode, getBaseUrl, getClientAuthContext } from "./client.js";
2
4
  import { getToastHandler } from "./mutation.js";
3
5
  import { useQueryClient } from "@tanstack/react-query";
@@ -110,7 +112,7 @@ async function uploadWithProgress(options) {
110
112
  * in the outer loop; this function just performs one upload.
111
113
  */
112
114
  function uploadAttempt(options) {
113
- const { url, formData, method = "POST", onProgress, signal, client, headers: extraHeaders, elevated, idempotencyKey, responseType = "json" } = options;
115
+ const { url, formData, method = "POST", onProgress, signal, client, headers: extraHeaders, elevated, idempotencyKey, responseType = "json", timeoutMs = 0 } = options;
114
116
  return new Promise((resolve, reject) => {
115
117
  if (signal?.aborted) {
116
118
  reject(abortReason(signal));
@@ -119,6 +121,7 @@ function uploadAttempt(options) {
119
121
  const xhr = new XMLHttpRequest();
120
122
  const fullUrl = resolveUrl(url, client);
121
123
  xhr.open(method, fullUrl, true);
124
+ if (timeoutMs > 0) xhr.timeout = timeoutMs;
122
125
  xhr.responseType = responseType === "blob" ? "blob" : "";
123
126
  const authMode = client?.config?.authMode ?? getAuthMode();
124
127
  if (authMode === "cookie") xhr.withCredentials = true;
@@ -130,7 +133,7 @@ function uploadAttempt(options) {
130
133
  if (authMode === "header") {
131
134
  const headerName = client?.auth?.headerName ?? "x-api-key";
132
135
  builtHeaders[headerName] = token;
133
- } else if (authMode !== "cookie") builtHeaders["Authorization"] = `Bearer ${token}`;
136
+ } else if (authMode !== "cookie") builtHeaders.Authorization = `Bearer ${token}`;
134
137
  }
135
138
  if (orgId) builtHeaders["x-organization-id"] = orgId;
136
139
  if (elevated ?? client?.config?.elevated) builtHeaders["x-arc-scope"] = "platform";
@@ -191,7 +194,7 @@ function uploadAttempt(options) {
191
194
  };
192
195
  xhr.ontimeout = () => {
193
196
  if (abortListener && signal) signal.removeEventListener("abort", abortListener);
194
- reject(/* @__PURE__ */ new Error(`Upload timed out: ${fullUrl}`));
197
+ reject(Object.assign(/* @__PURE__ */ new Error(`Upload timed out after ${timeoutMs}ms: ${fullUrl}`), { name: "TimeoutError" }));
195
198
  };
196
199
  xhr.send(formData);
197
200
  });
package/dist/ws.d.ts CHANGED
@@ -21,7 +21,7 @@ interface ArcWsMessage<TData = unknown> {
21
21
  * Backend also accepts `channel` as alias.
22
22
  */
23
23
  interface ArcSubscribeFrame {
24
- type: 'subscribe' | 'unsubscribe';
24
+ type: "subscribe" | "unsubscribe";
25
25
  resource?: string;
26
26
  channel?: string;
27
27
  /** Free-form additional fields the backend may use (token, filters, etc.). */
package/dist/ws.js CHANGED
@@ -150,7 +150,7 @@ function connectWs(options = {}) {
150
150
  }
151
151
  if (reconnectAttempts < maxReconnectAttempts) {
152
152
  reconnectAttempts += 1;
153
- const delay = Math.min(reconnectDelay * Math.pow(1.5, reconnectAttempts - 1), 3e4);
153
+ const delay = Math.min(reconnectDelay * 1.5 ** (reconnectAttempts - 1), 3e4);
154
154
  reconnectTimer = setTimeout(connect, delay);
155
155
  }
156
156
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc-next",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "React + TanStack Query SDK for Arc resources",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -8,7 +8,7 @@
8
8
  "author": "Classytic",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/classytic/arc-next"
11
+ "url": "git+https://github.com/classytic/arc-next.git"
12
12
  },
13
13
  "keywords": [
14
14
  "react",
@@ -136,11 +136,19 @@
136
136
  "push": "classytic-push",
137
137
  "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
138
138
  "release": "npm run push -- main && npm run release:tag && npm publish",
139
- "prepublishOnly": "npm run typecheck && npm test && npm run build",
140
- "typecheck:tests": "tsc --noEmit -p tsconfig.test.json"
139
+ "prepublishOnly": "npm run typecheck && npm run typecheck:tests && npm run lint && npm run check:dead-code && npm test && npm run build && npm run check:package && npm run check:api-surface && npm run check:server-import && npm run check:bundle && npm run check:fixtures",
140
+ "typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
141
+ "check:package": "publint --strict && attw --pack . --profile esm-only",
142
+ "check:api-surface": "node scripts/check-api-surface.mjs",
143
+ "api:surface": "node scripts/check-api-surface.mjs --update",
144
+ "check:server-import": "node scripts/check-server-import.mjs",
145
+ "check:bundle": "node scripts/check-bundle-size.mjs",
146
+ "check:fixtures": "tsc -p fixtures/tsconfig.json",
147
+ "lint": "biome check src/ tests/ scripts/ fixtures/",
148
+ "check:dead-code": "knip"
141
149
  },
142
150
  "peerDependencies": {
143
- "@classytic/repo-core": ">=0.4.0",
151
+ "@classytic/repo-core": ">=0.14.0",
144
152
  "@tanstack/react-query": ">=5.62.0",
145
153
  "jose": ">=5.0.0",
146
154
  "react": ">=19.0.0"
@@ -160,15 +168,18 @@
160
168
  }
161
169
  },
162
170
  "devDependencies": {
171
+ "@arethetypeswrong/cli": "^0.18.5",
172
+ "@biomejs/biome": "^2.5.5",
163
173
  "@classytic/dev-tools": "^0.2.0",
164
- "@classytic/repo-core": "^0.5.0",
174
+ "@classytic/repo-core": "^0.14.0",
165
175
  "@tanstack/react-query": "^5.97.0",
166
- "@testing-library/jest-dom": "^6.9.1",
167
176
  "@testing-library/react": "^16.3.2",
168
177
  "@types/react": "^19.2.14",
169
178
  "@types/react-dom": "^19.2.3",
170
179
  "jose": "^6.2.3",
171
180
  "jsdom": "^29.0.2",
181
+ "knip": "^6.27.0",
182
+ "publint": "^0.3.21",
172
183
  "react": "^19.2.5",
173
184
  "react-dom": "^19.2.5",
174
185
  "tsdown": "^0.21.7",