@marktiderman/genesis-core 1.3.0 → 1.5.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 +223 -0
- package/dist/chunk-5NHR6ZXS.js +31 -0
- package/dist/chunk-5NHR6ZXS.js.map +1 -0
- package/dist/chunk-7NUL32W7.js +14 -0
- package/dist/chunk-7NUL32W7.js.map +1 -0
- package/dist/chunk-BQOJX67X.js +31 -0
- package/dist/chunk-BQOJX67X.js.map +1 -0
- package/dist/{chunk-QFTMHOEH.js → chunk-DVW5GXWZ.js} +84 -31
- package/dist/chunk-DVW5GXWZ.js.map +1 -0
- package/dist/{chunk-6V2TIN3G.js → chunk-GGSXSI6Z.js} +6 -5
- package/dist/chunk-GGSXSI6Z.js.map +1 -0
- package/dist/chunk-HT43MPKW.js +3 -0
- package/dist/{chunk-EL7YGOTY.js.map → chunk-HT43MPKW.js.map} +1 -1
- package/dist/chunk-HUNGCTRA.js +33 -0
- package/dist/chunk-HUNGCTRA.js.map +1 -0
- package/dist/{chunk-5FTUB2M6.js → chunk-IZT2O57C.js} +160 -40
- package/dist/chunk-IZT2O57C.js.map +1 -0
- package/dist/chunk-QH4P5MI2.js +278 -0
- package/dist/chunk-QH4P5MI2.js.map +1 -0
- package/dist/chunk-RLY7TZUY.js +444 -0
- package/dist/chunk-RLY7TZUY.js.map +1 -0
- package/dist/chunk-XQO4OH2N.js +32 -0
- package/dist/chunk-XQO4OH2N.js.map +1 -0
- package/dist/{chunk-3P4PHPMJ.js → chunk-ZK5C3OOT.js} +3 -3
- package/dist/{chunk-3P4PHPMJ.js.map → chunk-ZK5C3OOT.js.map} +1 -1
- package/dist/form-types.d.ts +6 -0
- package/dist/hooks/index.d.ts +5 -1
- package/dist/hooks/index.js +11 -6
- package/dist/hooks/mutation-mode.d.ts +109 -0
- package/dist/hooks/mutation-mode.js +3 -0
- package/dist/hooks/mutation-mode.js.map +1 -0
- package/dist/hooks/related-keys.d.ts +12 -0
- package/dist/hooks/related-keys.js +3 -0
- package/dist/hooks/related-keys.js.map +1 -0
- package/dist/hooks/use-capabilities.d.ts +21 -0
- package/dist/hooks/use-capabilities.js +5 -0
- package/dist/hooks/use-capabilities.js.map +1 -0
- package/dist/hooks/use-one.d.ts +2 -1
- package/dist/hooks/use-one.js +2 -2
- package/dist/hooks/use-resource-form.d.ts +53 -2
- package/dist/hooks/use-resource-form.js +7 -3
- package/dist/hooks/use-resource.d.ts +42 -5
- package/dist/hooks/use-resource.js +4 -2
- package/dist/hooks/use-unsaved-changes-guard.d.ts +33 -0
- package/dist/hooks/use-unsaved-changes-guard.js +3 -0
- package/dist/hooks/use-unsaved-changes-guard.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +14 -9
- package/dist/provider/index.d.ts +212 -6
- package/dist/provider/index.js +5 -3
- package/dist/read-outcome-BQkYcV4t.d.ts +88 -0
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +2 -1
- package/dist/{types-DUbRb8qV.d.ts → types-C5bjsbxz.d.ts} +13 -255
- package/dist/types-Dsqqr9s7.d.ts +281 -0
- package/package.json +2 -2
- package/dist/chunk-4W4W7F4I.js +0 -20
- package/dist/chunk-4W4W7F4I.js.map +0 -1
- package/dist/chunk-5FTUB2M6.js.map +0 -1
- package/dist/chunk-6AA4ISUA.js +0 -125
- package/dist/chunk-6AA4ISUA.js.map +0 -1
- package/dist/chunk-6V2TIN3G.js.map +0 -1
- package/dist/chunk-EL7YGOTY.js +0 -3
- package/dist/chunk-QFTMHOEH.js.map +0 -1
- package/dist/write-outcome-aYToY3dK.d.ts +0 -38
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/hooks/mutation-mode.ts
|
|
4
|
+
var DEFAULT_UNDO_DELAY_MS = 5e3;
|
|
5
|
+
var pendingByClient = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
function pendingMap(queryClient) {
|
|
7
|
+
let map = pendingByClient.get(queryClient);
|
|
8
|
+
if (!map) {
|
|
9
|
+
map = /* @__PURE__ */ new Map();
|
|
10
|
+
pendingByClient.set(queryClient, map);
|
|
11
|
+
}
|
|
12
|
+
return map;
|
|
13
|
+
}
|
|
14
|
+
function pendingWriteCount(queryClient, resource) {
|
|
15
|
+
return pendingMap(queryClient).get(resource) ?? 0;
|
|
16
|
+
}
|
|
17
|
+
function beginPending(queryClient, resource) {
|
|
18
|
+
const map = pendingMap(queryClient);
|
|
19
|
+
map.set(resource, (map.get(resource) ?? 0) + 1);
|
|
20
|
+
}
|
|
21
|
+
function endPending(queryClient, resource) {
|
|
22
|
+
const map = pendingMap(queryClient);
|
|
23
|
+
const left = Math.max(0, (map.get(resource) ?? 0) - 1);
|
|
24
|
+
if (left === 0) map.delete(resource);
|
|
25
|
+
else map.set(resource, left);
|
|
26
|
+
return left;
|
|
27
|
+
}
|
|
28
|
+
function invalidateResource(queryClient, resource) {
|
|
29
|
+
if (pendingWriteCount(queryClient, resource) > 0) return Promise.resolve();
|
|
30
|
+
return queryClient.invalidateQueries({ queryKey: [resource] });
|
|
31
|
+
}
|
|
32
|
+
function sameId(a, b) {
|
|
33
|
+
return a != null && b != null && String(a) === String(b);
|
|
34
|
+
}
|
|
35
|
+
function isListResult(value) {
|
|
36
|
+
return typeof value === "object" && value !== null && Array.isArray(value.data);
|
|
37
|
+
}
|
|
38
|
+
function isDetailKeyFor(key, resource, id) {
|
|
39
|
+
return key[0] === resource && key[1] === "detail" && sameId(key[2], id);
|
|
40
|
+
}
|
|
41
|
+
function cachedRecord(queryClient, resource, id) {
|
|
42
|
+
for (const [key, value] of queryClient.getQueriesData({ queryKey: [resource, "detail"] })) {
|
|
43
|
+
if (isDetailKeyFor(key, resource, id) && value && typeof value === "object") return value;
|
|
44
|
+
}
|
|
45
|
+
for (const [, value] of queryClient.getQueriesData({ queryKey: [resource, "list"] })) {
|
|
46
|
+
if (!isListResult(value)) continue;
|
|
47
|
+
const found = value.data.find((row) => sameId(row.id, id));
|
|
48
|
+
if (found) return found;
|
|
49
|
+
}
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
function patchUpdate(queryClient, resource, id, data) {
|
|
53
|
+
const lists = [];
|
|
54
|
+
const details = [];
|
|
55
|
+
for (const [key, value] of queryClient.getQueriesData({ queryKey: [resource, "list"] })) {
|
|
56
|
+
if (!isListResult(value)) continue;
|
|
57
|
+
const index = value.data.findIndex((row) => sameId(row.id, id));
|
|
58
|
+
if (index < 0) continue;
|
|
59
|
+
const previous = value.data[index];
|
|
60
|
+
lists.push({ key, previous });
|
|
61
|
+
const rows = value.data.slice();
|
|
62
|
+
rows[index] = { ...previous, ...data, id: previous.id };
|
|
63
|
+
queryClient.setQueryData(key, { ...value, data: rows });
|
|
64
|
+
}
|
|
65
|
+
for (const [key, value] of queryClient.getQueriesData({ queryKey: [resource, "detail"] })) {
|
|
66
|
+
if (!isDetailKeyFor(key, resource, id) || !value || typeof value !== "object") continue;
|
|
67
|
+
details.push({ key, previous: value });
|
|
68
|
+
queryClient.setQueryData(key, { ...value, ...data, id: value.id });
|
|
69
|
+
}
|
|
70
|
+
return () => {
|
|
71
|
+
for (const { key, previous } of lists) {
|
|
72
|
+
const now = queryClient.getQueryData(key);
|
|
73
|
+
if (!isListResult(now)) continue;
|
|
74
|
+
const index = now.data.findIndex((row) => sameId(row.id, id));
|
|
75
|
+
if (index < 0) continue;
|
|
76
|
+
const rows = now.data.slice();
|
|
77
|
+
rows[index] = previous;
|
|
78
|
+
queryClient.setQueryData(key, { ...now, data: rows });
|
|
79
|
+
}
|
|
80
|
+
for (const { key, previous } of details) queryClient.setQueryData(key, previous);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function patchDelete(queryClient, resource, id) {
|
|
84
|
+
const lists = [];
|
|
85
|
+
for (const [key, value] of queryClient.getQueriesData({ queryKey: [resource, "list"] })) {
|
|
86
|
+
if (!isListResult(value)) continue;
|
|
87
|
+
const index = value.data.findIndex((row) => sameId(row.id, id));
|
|
88
|
+
if (index < 0) continue;
|
|
89
|
+
lists.push({ key, index, previous: value.data[index] });
|
|
90
|
+
const rows = value.data.filter((_, i) => i !== index);
|
|
91
|
+
queryClient.setQueryData(key, {
|
|
92
|
+
...value,
|
|
93
|
+
data: rows,
|
|
94
|
+
total: typeof value.total === "number" ? Math.max(0, value.total - 1) : value.total
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return () => {
|
|
98
|
+
for (const { key, index, previous } of lists) {
|
|
99
|
+
const now = queryClient.getQueryData(key);
|
|
100
|
+
if (!isListResult(now) || now.data.some((row) => sameId(row.id, id))) continue;
|
|
101
|
+
const rows = now.data.slice();
|
|
102
|
+
rows.splice(Math.min(index, rows.length), 0, previous);
|
|
103
|
+
queryClient.setQueryData(key, {
|
|
104
|
+
...now,
|
|
105
|
+
data: rows,
|
|
106
|
+
total: typeof now.total === "number" ? now.total + 1 : now.total
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
var allDelayed = /* @__PURE__ */ new Set();
|
|
112
|
+
function onBeforeUnload(event) {
|
|
113
|
+
if (allDelayed.size === 0) return;
|
|
114
|
+
for (const write of [...allDelayed]) write.flush();
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
event.returnValue = "";
|
|
117
|
+
}
|
|
118
|
+
function trackDelayed(write) {
|
|
119
|
+
allDelayed.add(write);
|
|
120
|
+
if (allDelayed.size === 1 && typeof window !== "undefined") {
|
|
121
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function untrackDelayed(write) {
|
|
125
|
+
if (!allDelayed.delete(write)) return;
|
|
126
|
+
if (allDelayed.size === 0 && typeof window !== "undefined") {
|
|
127
|
+
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function useDelayedWrites() {
|
|
131
|
+
const [writes] = useState(() => /* @__PURE__ */ new Set());
|
|
132
|
+
useEffect(
|
|
133
|
+
() => () => {
|
|
134
|
+
for (const write of [...writes]) write.flush();
|
|
135
|
+
},
|
|
136
|
+
[writes]
|
|
137
|
+
);
|
|
138
|
+
return writes;
|
|
139
|
+
}
|
|
140
|
+
async function providerServesUndo(queryClient, provider, resource) {
|
|
141
|
+
if (!provider.undo || !provider.capabilities) return false;
|
|
142
|
+
try {
|
|
143
|
+
const caps = await queryClient.fetchQuery({
|
|
144
|
+
// The same key as useCapabilities, so the answer is shared.
|
|
145
|
+
queryKey: ["__capabilities", resource],
|
|
146
|
+
queryFn: () => provider.capabilities(resource),
|
|
147
|
+
staleTime: Infinity,
|
|
148
|
+
// The write waits on this answer; an app's default retries would
|
|
149
|
+
// hold it for seconds.
|
|
150
|
+
retry: false
|
|
151
|
+
});
|
|
152
|
+
return caps?.undo === true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function asError(error) {
|
|
158
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
159
|
+
}
|
|
160
|
+
async function executeWrite(args) {
|
|
161
|
+
const { queryClient, provider, resource, kind, id, data, mode, send, committed, failedLater } = args;
|
|
162
|
+
if (mode === "pessimistic") {
|
|
163
|
+
const { record } = await send();
|
|
164
|
+
await committed(record);
|
|
165
|
+
return { status: "committed", record };
|
|
166
|
+
}
|
|
167
|
+
const viaProviderUndo = mode === "undoable" && await providerServesUndo(queryClient, provider, resource);
|
|
168
|
+
await queryClient.cancelQueries({ queryKey: [resource] });
|
|
169
|
+
beginPending(queryClient, resource);
|
|
170
|
+
let rollback = () => {
|
|
171
|
+
};
|
|
172
|
+
if (kind === "update" && id != null && data) rollback = patchUpdate(queryClient, resource, id, data);
|
|
173
|
+
else if (kind === "delete" && id != null) rollback = patchDelete(queryClient, resource, id);
|
|
174
|
+
let ended = false;
|
|
175
|
+
const end = () => {
|
|
176
|
+
if (ended) return;
|
|
177
|
+
ended = true;
|
|
178
|
+
if (endPending(queryClient, resource) === 0) {
|
|
179
|
+
void queryClient.invalidateQueries({ queryKey: [resource] });
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const commitNow = async () => {
|
|
183
|
+
let result;
|
|
184
|
+
try {
|
|
185
|
+
result = await send();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
rollback();
|
|
188
|
+
end();
|
|
189
|
+
failedLater(asError(error));
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
await committed(result.record);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(`[genesis] after-write callback failed for "${resource}":`, error);
|
|
196
|
+
}
|
|
197
|
+
end();
|
|
198
|
+
return result;
|
|
199
|
+
};
|
|
200
|
+
const delayMs = Math.max(0, args.undoDelayMs ?? DEFAULT_UNDO_DELAY_MS);
|
|
201
|
+
if (mode === "optimistic") {
|
|
202
|
+
void commitNow();
|
|
203
|
+
return { status: "applied" };
|
|
204
|
+
}
|
|
205
|
+
if (viaProviderUndo) {
|
|
206
|
+
void commitNow().then((result) => {
|
|
207
|
+
const auditId = result?.audit?.id;
|
|
208
|
+
if (!result || !args.onUndoable) return;
|
|
209
|
+
if (!auditId) {
|
|
210
|
+
console.warn(
|
|
211
|
+
`[genesis] "${resource}" serves undo, but its ${kind} returned no audit entry; no undo offered.`
|
|
212
|
+
);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
let undone = false;
|
|
216
|
+
args.onUndoable({
|
|
217
|
+
resource,
|
|
218
|
+
kind,
|
|
219
|
+
id: id ?? result.record?.id,
|
|
220
|
+
strategy: "provider",
|
|
221
|
+
delayMs,
|
|
222
|
+
commit: () => {
|
|
223
|
+
},
|
|
224
|
+
undo: async () => {
|
|
225
|
+
if (undone) return false;
|
|
226
|
+
undone = true;
|
|
227
|
+
let ok = true;
|
|
228
|
+
try {
|
|
229
|
+
await provider.undo(resource, { auditId });
|
|
230
|
+
} catch (error) {
|
|
231
|
+
ok = false;
|
|
232
|
+
failedLater(asError(error));
|
|
233
|
+
}
|
|
234
|
+
await invalidateResource(queryClient, resource);
|
|
235
|
+
return ok;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
return { status: "applied" };
|
|
240
|
+
}
|
|
241
|
+
let state = "waiting";
|
|
242
|
+
const write = {
|
|
243
|
+
flush: () => {
|
|
244
|
+
if (state !== "waiting") return;
|
|
245
|
+
state = "sent";
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
args.delayed.delete(write);
|
|
248
|
+
untrackDelayed(write);
|
|
249
|
+
void commitNow();
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const timer = setTimeout(write.flush, delayMs);
|
|
253
|
+
args.delayed.add(write);
|
|
254
|
+
trackDelayed(write);
|
|
255
|
+
args.onUndoable?.({
|
|
256
|
+
resource,
|
|
257
|
+
kind,
|
|
258
|
+
id,
|
|
259
|
+
strategy: "delay",
|
|
260
|
+
delayMs,
|
|
261
|
+
commit: write.flush,
|
|
262
|
+
undo: async () => {
|
|
263
|
+
if (state !== "waiting") return false;
|
|
264
|
+
state = "cancelled";
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
args.delayed.delete(write);
|
|
267
|
+
untrackDelayed(write);
|
|
268
|
+
rollback();
|
|
269
|
+
end();
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return { status: "applied" };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export { DEFAULT_UNDO_DELAY_MS, cachedRecord, executeWrite, invalidateResource, pendingWriteCount, useDelayedWrites };
|
|
277
|
+
//# sourceMappingURL=chunk-QH4P5MI2.js.map
|
|
278
|
+
//# sourceMappingURL=chunk-QH4P5MI2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/mutation-mode.ts"],"names":[],"mappings":"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","file":"chunk-QH4P5MI2.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Mutation modes — when a write reaches the cache, and when the provider.\n//\n// - `pessimistic` (default): wait for the provider, then refetch. Today's\n// behaviour.\n// - `optimistic`: patch the TanStack Query cache at once, send the write, and\n// put the cache back (and report the error) if it fails.\n// - `undoable`: patch the cache at once and offer an undo (`onUndoable`);\n// the write is sent after a delay unless undone. A provider that serves\n// `undo` (capabilities `undo: true`) gets the write at once instead, and\n// the undo asks the provider to reverse it.\n//\n// Core has no UI: the undo toast is the caller's (`onUndoable`). genesis-ui's\n// ResourceForm and ResourcePage show a sonner toast.\n// ---------------------------------------------------------------------------\n\nimport { useEffect, useState } from \"react\";\nimport type { QueryClient, QueryKey } from \"@tanstack/react-query\";\nimport type { BaseRecord, DataProvider, Identifier, ListResult } from \"../provider/types\";\nimport type { AuditEntry, ProviderCapabilities } from \"../schema/types\";\n\nexport type MutationMode = \"pessimistic\" | \"optimistic\" | \"undoable\";\n\n/** How long an `undoable` write waits for an undo before it is sent. */\nexport const DEFAULT_UNDO_DELAY_MS = 5000;\n\n/** An undoable write, as handed to `onUndoable` so a UI can offer the undo. */\nexport interface UndoableWrite {\n resource: string;\n kind: \"create\" | \"update\" | \"delete\";\n /** The record's id; absent for a create (it has none until it is sent). */\n id?: Identifier;\n /**\n * - `delay`: the write has not been sent. `undo()` discards it; `commit()`\n * sends it now instead of waiting.\n * - `provider`: the write was sent at once, because the provider serves\n * `undo`. `undo()` asks the provider to reverse it; `commit()` does\n * nothing.\n */\n strategy: \"delay\" | \"provider\";\n /** How long the undo is offered, in ms (the toast's duration). */\n delayMs: number;\n /**\n * Take the write back. Resolves true when it was undone, false when it\n * could not be: already sent under `delay` (the window closed, or the hook\n * unmounted), already undone, or the provider's undo failed (reported\n * through `onError`). Safe to call more than once.\n */\n undo: () => Promise<boolean>;\n /** Send a `delay` write now. */\n commit: () => void;\n}\n\nexport interface MutationModeOptions {\n /** Default `\"pessimistic\"`. */\n mutationMode?: MutationMode;\n /** How long an `undoable` write waits, in ms. Default 5000. */\n undoDelayMs?: number;\n /** Called with every `undoable` write, to show an undo control. */\n onUndoable?: (write: UndoableWrite) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Writes in flight per QueryClient and resource\n//\n// While an optimistic or undoable write on a resource is pending, refetching\n// that resource would bring back the server's rows and wipe the patch. So a\n// write's `[resource]` invalidation waits until no patched write on the\n// resource is left; the last one to settle refetches.\n// ---------------------------------------------------------------------------\n\nconst pendingByClient = new WeakMap<QueryClient, Map<string, number>>();\n\nfunction pendingMap(queryClient: QueryClient): Map<string, number> {\n let map = pendingByClient.get(queryClient);\n if (!map) {\n map = new Map();\n pendingByClient.set(queryClient, map);\n }\n return map;\n}\n\n/** Optimistic or undoable writes on `resource` not yet settled. */\nexport function pendingWriteCount(queryClient: QueryClient, resource: string): number {\n return pendingMap(queryClient).get(resource) ?? 0;\n}\n\nfunction beginPending(queryClient: QueryClient, resource: string): void {\n const map = pendingMap(queryClient);\n map.set(resource, (map.get(resource) ?? 0) + 1);\n}\n\n/** Returns how many are left. */\nfunction endPending(queryClient: QueryClient, resource: string): number {\n const map = pendingMap(queryClient);\n const left = Math.max(0, (map.get(resource) ?? 0) - 1);\n if (left === 0) map.delete(resource);\n else map.set(resource, left);\n return left;\n}\n\n/**\n * Invalidate `[resource]`, unless a patched write on it is still pending\n * (the last one to settle refetches).\n */\nexport function invalidateResource(queryClient: QueryClient, resource: string): Promise<void> {\n if (pendingWriteCount(queryClient, resource) > 0) return Promise.resolve();\n return queryClient.invalidateQueries({ queryKey: [resource] });\n}\n\n// ---------------------------------------------------------------------------\n// Cache patches\n//\n// Lists live at [resource, \"list\", params] as ListResult; one record at\n// [resource, \"detail\", id, select]. A patch touches only the record it is\n// about, and its rollback puts back only that record, so two writes on\n// different records never undo each other.\n// ---------------------------------------------------------------------------\n\nfunction sameId(a: unknown, b: unknown): boolean {\n return a != null && b != null && String(a) === String(b);\n}\n\nfunction isListResult(value: unknown): value is ListResult<BaseRecord> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n Array.isArray((value as { data?: unknown }).data)\n );\n}\n\nfunction isDetailKeyFor(key: QueryKey, resource: string, id: Identifier): boolean {\n return key[0] === resource && key[1] === \"detail\" && sameId(key[2], id);\n}\n\n/** The record as the cache has it now, from a detail entry or any list. */\nexport function cachedRecord(\n queryClient: QueryClient,\n resource: string,\n id: Identifier,\n): BaseRecord | undefined {\n for (const [key, value] of queryClient.getQueriesData<unknown>({ queryKey: [resource, \"detail\"] })) {\n if (isDetailKeyFor(key, resource, id) && value && typeof value === \"object\") return value as BaseRecord;\n }\n for (const [, value] of queryClient.getQueriesData<unknown>({ queryKey: [resource, \"list\"] })) {\n if (!isListResult(value)) continue;\n const found = value.data.find((row) => sameId(row.id, id));\n if (found) return found;\n }\n return undefined;\n}\n\ntype Rollback = () => void;\n\nfunction patchUpdate(\n queryClient: QueryClient,\n resource: string,\n id: Identifier,\n data: Record<string, unknown>,\n): Rollback {\n const lists: { key: QueryKey; previous: BaseRecord }[] = [];\n const details: { key: QueryKey; previous: unknown }[] = [];\n for (const [key, value] of queryClient.getQueriesData<unknown>({ queryKey: [resource, \"list\"] })) {\n if (!isListResult(value)) continue;\n const index = value.data.findIndex((row) => sameId(row.id, id));\n if (index < 0) continue;\n const previous = value.data[index]!;\n lists.push({ key, previous });\n const rows = value.data.slice();\n rows[index] = { ...previous, ...data, id: previous.id };\n queryClient.setQueryData(key, { ...value, data: rows });\n }\n for (const [key, value] of queryClient.getQueriesData<unknown>({ queryKey: [resource, \"detail\"] })) {\n if (!isDetailKeyFor(key, resource, id) || !value || typeof value !== \"object\") continue;\n details.push({ key, previous: value });\n queryClient.setQueryData(key, { ...(value as BaseRecord), ...data, id: (value as BaseRecord).id });\n }\n return () => {\n for (const { key, previous } of lists) {\n const now = queryClient.getQueryData<unknown>(key);\n if (!isListResult(now)) continue;\n const index = now.data.findIndex((row) => sameId(row.id, id));\n if (index < 0) continue;\n const rows = now.data.slice();\n rows[index] = previous;\n queryClient.setQueryData(key, { ...now, data: rows });\n }\n for (const { key, previous } of details) queryClient.setQueryData(key, previous);\n };\n}\n\nfunction patchDelete(queryClient: QueryClient, resource: string, id: Identifier): Rollback {\n const lists: { key: QueryKey; index: number; previous: BaseRecord }[] = [];\n for (const [key, value] of queryClient.getQueriesData<unknown>({ queryKey: [resource, \"list\"] })) {\n if (!isListResult(value)) continue;\n const index = value.data.findIndex((row) => sameId(row.id, id));\n if (index < 0) continue;\n lists.push({ key, index, previous: value.data[index]! });\n const rows = value.data.filter((_, i) => i !== index);\n queryClient.setQueryData(key, {\n ...value,\n data: rows,\n total: typeof value.total === \"number\" ? Math.max(0, value.total - 1) : value.total,\n });\n }\n return () => {\n for (const { key, index, previous } of lists) {\n const now = queryClient.getQueryData<unknown>(key);\n if (!isListResult(now) || now.data.some((row) => sameId(row.id, id))) continue;\n const rows = now.data.slice();\n rows.splice(Math.min(index, rows.length), 0, previous);\n queryClient.setQueryData(key, {\n ...now,\n data: rows,\n total: typeof now.total === \"number\" ? now.total + 1 : now.total,\n });\n }\n };\n}\n\n// ---------------------------------------------------------------------------\n// Pending (delayed) writes — flushed on unmount and before the page unloads\n// ---------------------------------------------------------------------------\n\ninterface DelayedWrite {\n /** Sends it now, if it is still waiting. */\n flush: () => void;\n}\n\nconst allDelayed = new Set<DelayedWrite>();\n\nfunction onBeforeUnload(event: BeforeUnloadEvent): void {\n if (allDelayed.size === 0) return;\n // Send what is waiting, and ask the browser to hold the page so the\n // requests can finish (the user may still leave).\n for (const write of [...allDelayed]) write.flush();\n event.preventDefault();\n event.returnValue = \"\";\n}\n\nfunction trackDelayed(write: DelayedWrite): void {\n allDelayed.add(write);\n if (allDelayed.size === 1 && typeof window !== \"undefined\") {\n window.addEventListener(\"beforeunload\", onBeforeUnload);\n }\n}\n\nfunction untrackDelayed(write: DelayedWrite): void {\n if (!allDelayed.delete(write)) return;\n if (allDelayed.size === 0 && typeof window !== \"undefined\") {\n window.removeEventListener(\"beforeunload\", onBeforeUnload);\n }\n}\n\n/** The delayed writes one hook instance started, flushed when it unmounts. */\nexport type DelayedWrites = Set<DelayedWrite>;\n\n/**\n * A hook's own set of delayed writes. When the hook unmounts (the form\n * closes, the route changes) every write still waiting is sent at once,\n * as React Admin does: leaving is not undoing.\n */\nexport function useDelayedWrites(): DelayedWrites {\n const [writes] = useState<DelayedWrites>(() => new Set());\n useEffect(\n () => () => {\n for (const write of [...writes]) write.flush();\n },\n [writes],\n );\n return writes;\n}\n\n// ---------------------------------------------------------------------------\n// The engine\n// ---------------------------------------------------------------------------\n\nexport interface ExecuteWriteArgs<R extends BaseRecord | void> {\n queryClient: QueryClient;\n provider: DataProvider;\n resource: string;\n kind: \"create\" | \"update\" | \"delete\";\n id?: Identifier;\n /**\n * The values to merge into the cached row (update). Leave out to patch\n * nothing, e.g. when the values are form-shaped and the row is not.\n */\n data?: Record<string, unknown>;\n mode: MutationMode;\n undoDelayMs?: number;\n onUndoable?: (write: UndoableWrite) => void;\n /** The delayed writes of the calling hook instance. */\n delayed: DelayedWrites;\n /** Sends the write. */\n send: () => Promise<{ record: R; audit?: AuditEntry }>;\n /**\n * After the write committed. Invalidates and calls success callbacks; its\n * `[resource]` invalidation should go through `invalidateResource`.\n */\n committed: (record: R) => Promise<void> | void;\n /** A write that failed after the caller was already told it went through. */\n failedLater: (error: Error) => void;\n}\n\nexport type ExecuteWriteResult<R> =\n | { status: \"committed\"; record: R }\n | { status: \"applied\" };\n\nasync function providerServesUndo(\n queryClient: QueryClient,\n provider: DataProvider,\n resource: string,\n): Promise<boolean> {\n if (!provider.undo || !provider.capabilities) return false;\n try {\n const caps = await queryClient.fetchQuery<ProviderCapabilities>({\n // The same key as useCapabilities, so the answer is shared.\n queryKey: [\"__capabilities\", resource],\n queryFn: () => provider.capabilities!(resource),\n staleTime: Infinity,\n // The write waits on this answer; an app's default retries would\n // hold it for seconds.\n retry: false,\n });\n return caps?.undo === true;\n } catch {\n return false;\n }\n}\n\nfunction asError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\n/**\n * Runs one write in the given mode.\n *\n * - `pessimistic`: sends it and waits; a failure rejects.\n * - `optimistic` / `undoable`: resolves `{ status: \"applied\" }` as soon as\n * the cache is patched. A later failure rolls the patch back and goes to\n * `failedLater`, never to the returned promise.\n */\nexport async function executeWrite<R extends BaseRecord | void>(\n args: ExecuteWriteArgs<R>,\n): Promise<ExecuteWriteResult<R>> {\n const { queryClient, provider, resource, kind, id, data, mode, send, committed, failedLater } = args;\n\n if (mode === \"pessimistic\") {\n const { record } = await send();\n await committed(record);\n return { status: \"committed\", record };\n }\n\n const viaProviderUndo = mode === \"undoable\" && (await providerServesUndo(queryClient, provider, resource));\n\n // A fetch already on its way would land after the patch and overwrite it.\n await queryClient.cancelQueries({ queryKey: [resource] });\n\n beginPending(queryClient, resource);\n let rollback: Rollback = () => {};\n if (kind === \"update\" && id != null && data) rollback = patchUpdate(queryClient, resource, id, data);\n else if (kind === \"delete\" && id != null) rollback = patchDelete(queryClient, resource, id);\n // A create has no cache entry yet: its id and its place in each sorted,\n // filtered list are the server's to say.\n\n let ended = false;\n const end = () => {\n if (ended) return;\n ended = true;\n if (endPending(queryClient, resource) === 0) {\n void queryClient.invalidateQueries({ queryKey: [resource] });\n }\n };\n\n const commitNow = async (): Promise<{ record: R; audit?: AuditEntry } | undefined> => {\n let result: { record: R; audit?: AuditEntry };\n try {\n result = await send();\n } catch (error) {\n rollback();\n end();\n failedLater(asError(error));\n return undefined;\n }\n try {\n await committed(result.record);\n } catch (error) {\n // The write did commit; only a callback failed.\n console.error(`[genesis] after-write callback failed for \"${resource}\":`, error);\n }\n end();\n return result;\n };\n\n const delayMs = Math.max(0, args.undoDelayMs ?? DEFAULT_UNDO_DELAY_MS);\n\n if (mode === \"optimistic\") {\n void commitNow();\n return { status: \"applied\" };\n }\n\n if (viaProviderUndo) {\n void commitNow().then((result) => {\n const auditId = result?.audit?.id;\n if (!result || !args.onUndoable) return;\n if (!auditId) {\n console.warn(\n `[genesis] \"${resource}\" serves undo, but its ${kind} returned no audit entry; no undo offered.`,\n );\n return;\n }\n let undone = false;\n args.onUndoable({\n resource,\n kind,\n id: id ?? (result.record as BaseRecord | undefined)?.id,\n strategy: \"provider\",\n delayMs,\n commit: () => {},\n undo: async () => {\n if (undone) return false;\n undone = true;\n let ok = true;\n try {\n await provider.undo!(resource, { auditId });\n } catch (error) {\n ok = false;\n failedLater(asError(error));\n }\n await invalidateResource(queryClient, resource);\n return ok;\n },\n });\n });\n return { status: \"applied\" };\n }\n\n // Delayed: wait, then send unless undone or flushed first.\n let state: \"waiting\" | \"sent\" | \"cancelled\" = \"waiting\";\n const write: DelayedWrite = {\n flush: () => {\n if (state !== \"waiting\") return;\n state = \"sent\";\n clearTimeout(timer);\n args.delayed.delete(write);\n untrackDelayed(write);\n void commitNow();\n },\n };\n const timer = setTimeout(write.flush, delayMs);\n args.delayed.add(write);\n trackDelayed(write);\n\n args.onUndoable?.({\n resource,\n kind,\n id,\n strategy: \"delay\",\n delayMs,\n commit: write.flush,\n undo: async () => {\n if (state !== \"waiting\") return false;\n state = \"cancelled\";\n clearTimeout(timer);\n args.delayed.delete(write);\n untrackDelayed(write);\n rollback();\n end();\n return true;\n },\n });\n\n return { status: \"applied\" };\n}\n"]}
|