@classytic/arc-next 0.11.0 → 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/README.md +50 -0
- package/dist/api.d.ts +40 -23
- package/dist/api.js +14 -1
- package/dist/cache.d.ts +20 -2
- package/dist/cache.js +52 -1
- package/dist/client.d.ts +82 -12
- package/dist/client.js +110 -22
- package/dist/encryption.d.ts +1 -1
- package/dist/hooks.d.ts +28 -16
- package/dist/hooks.js +99 -27
- package/dist/mutation.d.ts +42 -7
- package/dist/mutation.js +28 -11
- package/dist/presets/history.d.ts +1 -1
- package/dist/query.d.ts +1 -1
- package/dist/sse.js +1 -1
- package/dist/upload.d.ts +9 -0
- package/dist/upload.js +6 -3
- package/dist/ws.d.ts +1 -1
- package/dist/ws.js +1 -1
- package/package.json +189 -178
package/dist/mutation.d.ts
CHANGED
|
@@ -3,9 +3,16 @@ import * as _$_tanstack_react_query0 from "@tanstack/react-query";
|
|
|
3
3
|
import { QueryClient, QueryKey, UseMutateAsyncFunction, UseMutateFunction } from "@tanstack/react-query";
|
|
4
4
|
|
|
5
5
|
//#region src/mutation.d.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Toast copy for a mutation. Generic over the mutation's result/variables so
|
|
8
|
+
* `success: (result) => ...` sees the TYPED FLAT action result (arc 2.13+
|
|
9
|
+
* returns results with no `{ data }` envelope) — the stale-envelope
|
|
10
|
+
* `(data as { data: T }).data` cast class is a compile error now. Defaults
|
|
11
|
+
* keep pre-0.11.1 unparameterized usage compiling unchanged.
|
|
12
|
+
*/
|
|
13
|
+
interface MutationMessages<TData = unknown, TVariables = unknown> {
|
|
14
|
+
success?: string | ((data: TData, variables: TVariables) => string);
|
|
15
|
+
error?: string | ((error: Error, variables: TVariables) => string);
|
|
9
16
|
}
|
|
10
17
|
interface MutationCallbacks<TData, TVariables, TContext = unknown> {
|
|
11
18
|
onMutate?: (variables: TVariables) => TContext | Promise<TContext>;
|
|
@@ -53,10 +60,17 @@ declare function getToastHandler(): ToastHandler;
|
|
|
53
60
|
interface TransitionMutationConfig<TData, TVariables> {
|
|
54
61
|
mutationFn: (variables: TVariables) => Promise<TData>;
|
|
55
62
|
invalidateQueries?: QueryKey[];
|
|
63
|
+
/**
|
|
64
|
+
* Result-aware invalidation gate. When provided, `invalidateQueries` only
|
|
65
|
+
* fire if this returns true for the mutation result — lets bulk operations
|
|
66
|
+
* skip refetching everything when the server reports nothing changed
|
|
67
|
+
* (`modifiedCount: 0`, `deletedCount: 0`).
|
|
68
|
+
*/
|
|
69
|
+
shouldInvalidate?: (data: TData) => boolean;
|
|
56
70
|
onSuccess?: (data: TData, variables: TVariables) => void;
|
|
57
71
|
onError?: (error: Error, variables: TVariables) => void;
|
|
58
72
|
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
|
|
59
|
-
messages?: MutationMessages
|
|
73
|
+
messages?: MutationMessages<TData, TVariables>;
|
|
60
74
|
useTransition?: boolean;
|
|
61
75
|
showToast?: boolean;
|
|
62
76
|
/** Per-call toast guard — return false to suppress toast for this invocation */
|
|
@@ -80,7 +94,7 @@ interface OptimisticMutationConfig<TData, TVariables> {
|
|
|
80
94
|
onSuccess?: (data: TData, variables: TVariables) => void;
|
|
81
95
|
onError?: (error: Error, variables: TVariables) => void;
|
|
82
96
|
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
|
|
83
|
-
messages?: MutationMessages
|
|
97
|
+
messages?: MutationMessages<TData, TVariables>;
|
|
84
98
|
showToast?: boolean;
|
|
85
99
|
toastHandler?: ToastHandler;
|
|
86
100
|
}
|
|
@@ -108,11 +122,32 @@ interface CreateOptimisticMutationConfig<TData, TVariables> {
|
|
|
108
122
|
mutationFn: (variables: TVariables) => Promise<TData>;
|
|
109
123
|
queryClient: QueryClient;
|
|
110
124
|
queryKeys: QueryKey[];
|
|
111
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Per-cache-entry optimistic updater. Receives the concrete query key of
|
|
127
|
+
* the entry being updated so a single updater can treat list, detail, and
|
|
128
|
+
* aggregation caches differently (merge the doc into `[e,'detail',id]`,
|
|
129
|
+
* map items inside `[e,'list',…]`, leave `[e,'aggregation',…]` untouched).
|
|
130
|
+
* Return the input unchanged to skip the write for that entry.
|
|
131
|
+
*/
|
|
132
|
+
optimisticUpdate?: (oldData: unknown, variables: TVariables, queryKey: QueryKey) => unknown;
|
|
133
|
+
/**
|
|
134
|
+
* Shared identity for this resource's write mutations. When set, settled
|
|
135
|
+
* invalidation only fires from the LAST pending mutation carrying the same
|
|
136
|
+
* key (`isMutating === 1`) — rapid sequential writes produce one refetch
|
|
137
|
+
* at the end instead of N racing refetches, and a refetch triggered by
|
|
138
|
+
* write #1 can never clobber write #2's optimistic state.
|
|
139
|
+
*/
|
|
140
|
+
mutationKey?: readonly unknown[];
|
|
141
|
+
/**
|
|
142
|
+
* Post-success cache reconciliation, run BEFORE any invalidation while the
|
|
143
|
+
* optimistic state is still in place. Use to swap temp IDs for server
|
|
144
|
+
* documents or seed detail caches from the response.
|
|
145
|
+
*/
|
|
146
|
+
reconcile?: (data: TData, variables: TVariables) => void;
|
|
112
147
|
onSuccess?: (data: TData, variables: TVariables) => void;
|
|
113
148
|
onError?: (error: Error, variables: TVariables) => void;
|
|
114
149
|
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
|
|
115
|
-
messages?: MutationMessages
|
|
150
|
+
messages?: MutationMessages<TData, TVariables>;
|
|
116
151
|
/** Per-call toast guard — return false to suppress toast for this invocation */
|
|
117
152
|
shouldToast?: () => boolean;
|
|
118
153
|
toastHandler?: ToastHandler;
|
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 {
|
|
5
|
+
import { useRef, useTransition } from "react";
|
|
6
6
|
|
|
7
7
|
//#region src/mutation.ts
|
|
8
8
|
let toastHandler = {
|
|
9
|
-
success: (
|
|
10
|
-
error: (
|
|
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) =>
|
|
81
|
+
invalidateQueries.forEach((key) => {
|
|
82
|
+
queryClient.invalidateQueries({ queryKey: key });
|
|
83
|
+
});
|
|
82
84
|
};
|
|
83
|
-
if (
|
|
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) =>
|
|
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 }) =>
|
|
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
|
-
|
|
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
|
-
|
|
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]) =>
|
|
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:
|
|
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?: (
|
|
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 *
|
|
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
|
|
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:
|
|
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 *
|
|
153
|
+
const delay = Math.min(reconnectDelay * 1.5 ** (reconnectAttempts - 1), 3e4);
|
|
154
154
|
reconnectTimer = setTimeout(connect, delay);
|
|
155
155
|
}
|
|
156
156
|
};
|