cursedbelt 5.89.0 → 5.90.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.
Files changed (38) hide show
  1. package/dist/react/LazyToaster.d.ts +3 -0
  2. package/dist/react/LazyToaster.d.ts.map +1 -0
  3. package/dist/react/LazyToaster.js +21 -0
  4. package/dist/react/LazyToaster.js.map +1 -0
  5. package/dist/react/createApiHooks.d.ts +30 -0
  6. package/dist/react/createApiHooks.d.ts.map +1 -1
  7. package/dist/react/createApiHooks.js +31 -2
  8. package/dist/react/createApiHooks.js.map +1 -1
  9. package/dist/react/index.d.ts +2 -0
  10. package/dist/react/index.d.ts.map +1 -1
  11. package/dist/react/index.js +2 -0
  12. package/dist/react/index.js.map +1 -1
  13. package/dist/react/optimistic/index.d.ts +10 -0
  14. package/dist/react/optimistic/index.d.ts.map +1 -0
  15. package/dist/react/optimistic/index.js +22 -0
  16. package/dist/react/optimistic/index.js.map +1 -0
  17. package/dist/react/optimistic/optimistic.d.ts +159 -0
  18. package/dist/react/optimistic/optimistic.d.ts.map +1 -0
  19. package/dist/react/optimistic/optimistic.js +216 -0
  20. package/dist/react/optimistic/optimistic.js.map +1 -0
  21. package/dist/react/optimistic/revert.d.ts +47 -0
  22. package/dist/react/optimistic/revert.d.ts.map +1 -0
  23. package/dist/react/optimistic/revert.js +235 -0
  24. package/dist/react/optimistic/revert.js.map +1 -0
  25. package/package.json +1 -1
  26. package/scripts/styleAreas.ts +3 -0
  27. package/src/react/LazyToaster.spec.tsx +18 -0
  28. package/src/react/LazyToaster.tsx +26 -0
  29. package/src/react/createApiHooks.spec.tsx +77 -0
  30. package/src/react/createApiHooks.ts +58 -1
  31. package/src/react/index.ts +2 -0
  32. package/src/react/optimistic/index.ts +36 -0
  33. package/src/react/optimistic/optimistic.spec.tsx +225 -0
  34. package/src/react/optimistic/optimistic.ts +326 -0
  35. package/src/react/optimistic/queryTarget.spec.ts +64 -0
  36. package/src/react/optimistic/revert.spec.ts +114 -0
  37. package/src/react/optimistic/revert.ts +238 -0
  38. package/src/react/optimistic/staysOffTheEntry.spec.ts +38 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * `optimistic()` — do what the user asked NOW, tell the server after, and take it back only if
3
+ * the server says no.
4
+ *
5
+ * The owner, 2026-10-22 (forge task 3583): *"I want the cursedforge apps to use optimistic
6
+ * updates, where the UI doesn't wait for the server before doing what the user wants … if the
7
+ * delete fails it can popup a toast error but it would already be on the next track either way."*
8
+ *
9
+ * optimistic({
10
+ * label: `delete “${track.title}”`,
11
+ * targets: [
12
+ * { set: setTracks, apply: (rows) => rows.filter((r) => r.id !== track.id) },
13
+ * { set: setTotal, apply: (n) => n - 1, revert: counterRevert },
14
+ * ],
15
+ * request: () => api.deleteTracks({ ids: [track.id] }),
16
+ * });
17
+ *
18
+ * What it does, in order:
19
+ *
20
+ * 1. **Applies every target's forward edit at once**, through a setState-shaped `set` — React's
21
+ * own setter, a store's, or {@link queryTarget} for a TanStack cache. It records the state
22
+ * each edit started from and produced, inside the updater, so the rollback is derived
23
+ * (`revert.ts`) and no caller writes an undo by hand.
24
+ * 2. **Sends the request** — after any earlier request with the same `key` has settled, so two
25
+ * quick edits to one record reach the server in the order they were made. The UI never waits
26
+ * for that queue; only the network does.
27
+ * 3. **On failure**, reverts exactly what this change touched (concurrent changes survive) and
28
+ * shows an error toast naming what did not happen, with **Retry** (OVL-9) — Retry applies the
29
+ * change again and resends it.
30
+ * 4. **While anything is in flight**, the page asks before it is closed or reloaded, because the
31
+ * user has already seen the change happen and would otherwise lose it without knowing.
32
+ *
33
+ * Side effects that must NOT be undone stay outside it — the user's own words: a deleted song
34
+ * that comes back after a failed delete comes back to the list, but the player stays on the next
35
+ * track. Call the player before `optimistic()`, not as a target.
36
+ *
37
+ * It never rejects: the resolved {@link OptimisticOutcome} says what happened, so a fire-and-forget
38
+ * call (`void optimistic(...)`) cannot leak an unhandled rejection.
39
+ */
40
+ import { defaultIdOf, inverseOf } from './revert.js';
41
+ /** Pair a setter with its edit so the two are type-checked against each other. */
42
+ export const target = (t) => t;
43
+ let pending = [];
44
+ let nextId = 1;
45
+ const listeners = new Set();
46
+ const queues = new Map();
47
+ const emit = () => {
48
+ for (const listener of listeners)
49
+ listener();
50
+ };
51
+ /** Every optimistic change whose request has not settled, oldest first. */
52
+ export const pendingChanges = () => pending;
53
+ /** Subscribe to {@link pendingChanges} — the `useSyncExternalStore` shape. */
54
+ export const subscribePendingChanges = (listener) => {
55
+ listeners.add(listener);
56
+ return () => {
57
+ listeners.delete(listener);
58
+ };
59
+ };
60
+ /**
61
+ * Resolves once nothing is pending — the moment to re-read a list that is not a TanStack query,
62
+ * so a read that started before the last change landed cannot draw it as undone.
63
+ */
64
+ export const whenSettled = () => pending.length === 0
65
+ ? Promise.resolve()
66
+ : new Promise((resolve) => {
67
+ const off = subscribePendingChanges(() => {
68
+ if (pending.length > 0)
69
+ return;
70
+ off();
71
+ resolve();
72
+ });
73
+ });
74
+ /**
75
+ * Closing the tab while a change is unsent loses it, and the user has already seen it happen.
76
+ * The listener exists only while something is pending, so an idle page leaves bfcache alone.
77
+ */
78
+ const onBeforeUnload = (event) => {
79
+ event.preventDefault();
80
+ // Chrome still wants returnValue set; the text itself is never shown.
81
+ event.returnValue = '';
82
+ };
83
+ const track = (change) => {
84
+ const wasIdle = pending.length === 0;
85
+ pending = [...pending, change];
86
+ if (wasIdle && typeof window !== 'undefined')
87
+ window.addEventListener('beforeunload', onBeforeUnload);
88
+ emit();
89
+ return () => {
90
+ pending = pending.filter((p) => p.id !== change.id);
91
+ if (pending.length === 0 && typeof window !== 'undefined') {
92
+ window.removeEventListener('beforeunload', onBeforeUnload);
93
+ }
94
+ emit();
95
+ };
96
+ };
97
+ /** Run `task` after every earlier task under `key` has settled. */
98
+ const enqueue = (key, task) => {
99
+ if (key === undefined)
100
+ return task();
101
+ const run = (queues.get(key) ?? Promise.resolve()).then(task, task);
102
+ const tail = run.catch(() => undefined);
103
+ queues.set(key, tail);
104
+ void tail.then(() => {
105
+ if (queues.get(key) === tail)
106
+ queues.delete(key);
107
+ });
108
+ return run;
109
+ };
110
+ const isStep = (t) => typeof t.now === 'function';
111
+ function applyStep(step) {
112
+ step.now();
113
+ return { rollback: step.undo, settled: () => undefined };
114
+ }
115
+ function applyTarget(t) {
116
+ // Captured INSIDE the updater: React runs updaters during render, not when `set` is called.
117
+ // The rollback updater is queued behind this one, so by the time it runs the capture is there.
118
+ let seen;
119
+ const scope = t.scope?.();
120
+ t.begin?.();
121
+ t.set((current) => {
122
+ const after = t.apply(current);
123
+ seen = { before: current, after };
124
+ return after;
125
+ });
126
+ return {
127
+ rollback: () => {
128
+ if (t.scope && !Object.is(t.scope(), scope))
129
+ return; // another view is on screen now
130
+ t.set((current) => {
131
+ if (!seen)
132
+ return current; // the edit never ran (its owner unmounted) — nothing to undo
133
+ const { before, after } = seen;
134
+ return t.revert ? t.revert(current, before, after) : inverseOf(before, after, t.idOf ?? defaultIdOf)(current);
135
+ });
136
+ },
137
+ settled: () => t.settled?.(),
138
+ };
139
+ }
140
+ const messageOf = (error) => {
141
+ if (error instanceof Error)
142
+ return error.message || undefined;
143
+ if (typeof error === 'string')
144
+ return error || undefined;
145
+ return undefined;
146
+ };
147
+ /** Apply now, send, revert on failure. See the file header. */
148
+ export function optimistic(options) {
149
+ const { label, request, key, onSuccess, onError } = options;
150
+ const targets = options.targets === undefined ? [] : Array.isArray(options.targets) ? options.targets : [options.targets];
151
+ const applied = targets.map((t) => isStep(t) ? applyStep(t) : applyTarget(t));
152
+ const done = track({ id: nextId++, label, key, startedAt: Date.now() });
153
+ return enqueue(key, request).then((value) => {
154
+ done();
155
+ for (const a of applied)
156
+ a.settled();
157
+ onSuccess?.(value);
158
+ return { ok: true, value };
159
+ }, async (error) => {
160
+ done();
161
+ // Newest-first, so a target listed twice unwinds in the order it was wound.
162
+ for (const a of [...applied].reverse())
163
+ a.rollback();
164
+ for (const a of applied)
165
+ a.settled();
166
+ onError?.(error);
167
+ if (options.toast !== false) {
168
+ // Loaded at the first refusal, never before: a static import would put sonner (~10 kB
169
+ // gzip) back on every app's entry chunk, which cursedbelt 5.64 took it off (forge task
170
+ // 2900) — flix's and roms' entries grew by it the first time they imported this.
171
+ const { toast } = await import('../Toast.js');
172
+ toast.error(`Couldn’t ${label}`, {
173
+ id: `optimistic:${key ?? label}`,
174
+ description: options.describeError ? options.describeError(error) : messageOf(error),
175
+ action: { label: 'Retry', onClick: () => void optimistic(options) },
176
+ });
177
+ }
178
+ return { ok: false, error };
179
+ });
180
+ }
181
+ /** Changes in flight per cache entry, so the entry is refetched once, after the LAST of them. */
182
+ const inFlight = new Map();
183
+ /**
184
+ * A TanStack cache entry as a target's `set`: cancels the key's in-flight fetch first (so a
185
+ * response already on the wire cannot land on top of the change), writes through `setQueryData`,
186
+ * and refetches the key once the request settles, so the cache ends on the server's truth.
187
+ * An entry with no data yet is left alone.
188
+ *
189
+ * 🔴 The refetch waits for the LAST change in flight on the entry. Refetching after each one
190
+ * (the first version) meant two quick deletes drew the second row back for a moment: the first
191
+ * answer's refetch read the server before the second delete had landed (collections and desk
192
+ * both met it, forge task 3583; TanStack's own docs gate on `isMutating`).
193
+ *
194
+ * targets: [{ ...queryTarget(queryClient, ['lists']), apply: (lists) => lists.filter(...) }]
195
+ */
196
+ export function queryTarget(client, queryKey, { refetch = true } = {}) {
197
+ const entry = JSON.stringify(queryKey);
198
+ return {
199
+ set: (updater) => {
200
+ void client.cancelQueries({ queryKey, exact: true });
201
+ client.setQueryData(queryKey, (old) => (old === undefined ? old : updater(old)));
202
+ },
203
+ begin: () => inFlight.set(entry, (inFlight.get(entry) ?? 0) + 1),
204
+ settled: () => {
205
+ const left = (inFlight.get(entry) ?? 1) - 1;
206
+ if (left > 0) {
207
+ inFlight.set(entry, left);
208
+ return;
209
+ }
210
+ inFlight.delete(entry);
211
+ if (refetch)
212
+ void client.invalidateQueries({ queryKey });
213
+ },
214
+ };
215
+ }
216
+ //# sourceMappingURL=optimistic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimistic.js","sourceRoot":"","sources":["../../../src/react/optimistic/optimistic.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The rollback half of an optimistic change, DERIVED from the change itself.
3
+ *
4
+ * A caller writes only the forward edit (`rows => rows.filter(r => r.id !== id)`); this works out
5
+ * what that edit touched by comparing the state before and after it, and returns an inverse that
6
+ * undoes exactly that — applied to whatever the state is when the request fails, not to a
7
+ * snapshot. That difference is the whole reason this file exists:
8
+ *
9
+ * delete A, delete B, A fails → restoring the snapshot taken before A would bring back B too
10
+ * (or, taken before B, drop a rename made since). The inverse instead puts A back where it
11
+ * was and leaves B gone and the rename standing.
12
+ *
13
+ * The rules, by shape (recursing through plain objects and arrays, so a patch three levels down
14
+ * reverts three levels down):
15
+ *
16
+ * | shape | the change | its inverse, against the current state |
17
+ * |---|---|---|
18
+ * | array of records (`id`) or primitives | item removed | put back after its nearest surviving neighbor, unless it is back already |
19
+ * | | item added | taken out by id |
20
+ * | | item changed | recurse into it by id (only fields still holding the optimistic value revert) |
21
+ * | | reordered | the old order back, only if the current order is still the optimistic one |
22
+ * | plain object | key changed / added / removed | per key, recursing |
23
+ * | `Set` / `Map` | member added / removed | the member out / back |
24
+ * | anything else | replaced | the old value back only if the current one is still the optimistic one |
25
+ *
26
+ * "Only if it still holds the optimistic value" is what lets two edits race: rename to "B", then
27
+ * to "C" — if the first request fails, the title is "C" by now, so it stays "C".
28
+ *
29
+ * Returns the current state UNCHANGED (same reference) when nothing needs reverting, so a React
30
+ * setter given the inverse does not re-render.
31
+ */
32
+ /** How an array item is identified. Records by `id` (or `key`), primitives by value. */
33
+ export type IdOf = (item: unknown) => unknown;
34
+ export declare const defaultIdOf: IdOf;
35
+ /**
36
+ * The inverse of the change `before → after`, as a function of the state at revert time.
37
+ * `idOf` names array items; the default reads `id`, then `key`, and uses a primitive's value.
38
+ */
39
+ export declare function inverseOf<S>(before: S, after: S, idOf?: IdOf): (current: S) => S;
40
+ /**
41
+ * A counter's inverse: give back the difference the change made, whatever has happened since.
42
+ * The whole-value rule is wrong for a count — two deletes take a total 10 → 9 → 8, and when the
43
+ * first fails the total is 8, not 9, so "revert only if still optimistic" would never fire. Pass
44
+ * it as a target's `revert`: `{ set: setTotal, apply: (n) => n - 1, revert: counterRevert }`.
45
+ */
46
+ export declare const counterRevert: (current: number, before: number, after: number) => number;
47
+ //# sourceMappingURL=revert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revert.d.ts","sourceRoot":"","sources":["../../../src/react/optimistic/revert.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,wFAAwF;AACxF,MAAM,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;AAI9C,eAAO,MAAM,WAAW,EAAE,IAMzB,CAAC;AAQF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,GAAE,IAAkB,GAAG,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAE7F;AA6KD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MACnD,CAAC"}
@@ -0,0 +1,235 @@
1
+ /**
2
+ * The rollback half of an optimistic change, DERIVED from the change itself.
3
+ *
4
+ * A caller writes only the forward edit (`rows => rows.filter(r => r.id !== id)`); this works out
5
+ * what that edit touched by comparing the state before and after it, and returns an inverse that
6
+ * undoes exactly that — applied to whatever the state is when the request fails, not to a
7
+ * snapshot. That difference is the whole reason this file exists:
8
+ *
9
+ * delete A, delete B, A fails → restoring the snapshot taken before A would bring back B too
10
+ * (or, taken before B, drop a rename made since). The inverse instead puts A back where it
11
+ * was and leaves B gone and the rename standing.
12
+ *
13
+ * The rules, by shape (recursing through plain objects and arrays, so a patch three levels down
14
+ * reverts three levels down):
15
+ *
16
+ * | shape | the change | its inverse, against the current state |
17
+ * |---|---|---|
18
+ * | array of records (`id`) or primitives | item removed | put back after its nearest surviving neighbor, unless it is back already |
19
+ * | | item added | taken out by id |
20
+ * | | item changed | recurse into it by id (only fields still holding the optimistic value revert) |
21
+ * | | reordered | the old order back, only if the current order is still the optimistic one |
22
+ * | plain object | key changed / added / removed | per key, recursing |
23
+ * | `Set` / `Map` | member added / removed | the member out / back |
24
+ * | anything else | replaced | the old value back only if the current one is still the optimistic one |
25
+ *
26
+ * "Only if it still holds the optimistic value" is what lets two edits race: rename to "B", then
27
+ * to "C" — if the first request fails, the title is "C" by now, so it stays "C".
28
+ *
29
+ * Returns the current state UNCHANGED (same reference) when nothing needs reverting, so a React
30
+ * setter given the inverse does not re-render.
31
+ */
32
+ const NO_ID = Symbol('no-id');
33
+ export const defaultIdOf = (item) => {
34
+ if (item === null || typeof item !== 'object')
35
+ return item;
36
+ const record = item;
37
+ if (record.id !== undefined)
38
+ return record.id;
39
+ if (record.key !== undefined)
40
+ return record.key;
41
+ return NO_ID;
42
+ };
43
+ const isPlainObject = (value) => {
44
+ if (value === null || typeof value !== 'object')
45
+ return false;
46
+ const proto = Object.getPrototypeOf(value);
47
+ return proto === Object.prototype || proto === null;
48
+ };
49
+ /**
50
+ * The inverse of the change `before → after`, as a function of the state at revert time.
51
+ * `idOf` names array items; the default reads `id`, then `key`, and uses a primitive's value.
52
+ */
53
+ export function inverseOf(before, after, idOf = defaultIdOf) {
54
+ return (current) => revertValue(before, after, current, idOf);
55
+ }
56
+ function revertValue(before, after, current, idOf) {
57
+ if (Object.is(before, after))
58
+ return current;
59
+ if (Array.isArray(before) && Array.isArray(after) && Array.isArray(current)) {
60
+ const reverted = revertArray(before, after, current, idOf);
61
+ if (reverted !== undefined)
62
+ return reverted;
63
+ }
64
+ if (before instanceof Set && after instanceof Set && current instanceof Set) {
65
+ return revertSet(before, after, current);
66
+ }
67
+ if (before instanceof Map && after instanceof Map && current instanceof Map) {
68
+ return revertMap(before, after, current, idOf);
69
+ }
70
+ if (isPlainObject(before) && isPlainObject(after) && isPlainObject(current)) {
71
+ return revertObject(before, after, current, idOf);
72
+ }
73
+ return Object.is(current, after) ? before : current;
74
+ }
75
+ function revertObject(before, after, current, idOf) {
76
+ let out;
77
+ const write = (key, value, remove = false) => {
78
+ out ??= { ...current };
79
+ if (remove)
80
+ delete out[key];
81
+ else
82
+ out[key] = value;
83
+ };
84
+ for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
85
+ const had = Object.hasOwn(before, key);
86
+ const has = Object.hasOwn(after, key);
87
+ if (had && has) {
88
+ if (Object.is(before[key], after[key]))
89
+ continue;
90
+ if (!Object.hasOwn(current, key))
91
+ continue; // removed since — not ours to bring back
92
+ const next = revertValue(before[key], after[key], current[key], idOf);
93
+ if (!Object.is(next, current[key]))
94
+ write(key, next);
95
+ }
96
+ else if (has) {
97
+ // Added by the change: take it out if it is still what the change put there.
98
+ if (Object.hasOwn(current, key) && Object.is(current[key], after[key]))
99
+ write(key, undefined, true);
100
+ }
101
+ else if (!Object.hasOwn(current, key)) {
102
+ write(key, before[key]); // removed by the change, and still gone
103
+ }
104
+ }
105
+ return out ?? current;
106
+ }
107
+ /** `undefined` when the items cannot be identified — the caller falls back to whole-value. */
108
+ function revertArray(before, after, current, idOf) {
109
+ const ids = (list) => list.map(idOf);
110
+ const beforeIds = ids(before);
111
+ const afterIds = ids(after);
112
+ const unique = (list) => !list.includes(NO_ID) && new Set(list).size === list.length;
113
+ if (!unique(beforeIds) || !unique(afterIds))
114
+ return undefined;
115
+ const beforeById = new Map(beforeIds.map((id, i) => [id, before[i]]));
116
+ const afterById = new Map(afterIds.map((id, i) => [id, after[i]]));
117
+ let out = current;
118
+ let touched = false;
119
+ const edit = () => {
120
+ if (!touched) {
121
+ out = [...current];
122
+ touched = true;
123
+ }
124
+ };
125
+ // 1 · Items the change ADDED go, if they are still there.
126
+ const added = afterIds.filter((id) => !beforeById.has(id));
127
+ if (added.length > 0) {
128
+ const drop = new Set(added);
129
+ const kept = out.filter((item) => !drop.has(idOf(item)));
130
+ if (kept.length !== out.length) {
131
+ out = kept;
132
+ touched = true;
133
+ }
134
+ }
135
+ // 2 · Items the change CHANGED revert field by field.
136
+ for (const [id, was] of beforeById) {
137
+ const now = afterById.get(id);
138
+ if (now === undefined || Object.is(was, now))
139
+ continue;
140
+ const at = out.findIndex((item) => Object.is(idOf(item), id));
141
+ if (at < 0)
142
+ continue;
143
+ const next = revertValue(was, now, out[at], idOf);
144
+ if (!Object.is(next, out[at])) {
145
+ edit();
146
+ out[at] = next;
147
+ }
148
+ }
149
+ // 3 · Items the change REMOVED come back beside the neighbor they had.
150
+ const removed = beforeIds.flatMap((id, index) => (afterById.has(id) ? [] : [{ id, index }]));
151
+ for (const { id, index } of removed) {
152
+ if (out.some((item) => Object.is(idOf(item), id)))
153
+ continue;
154
+ edit();
155
+ let at = 0;
156
+ for (let i = index - 1; i >= 0; i--) {
157
+ const neighbor = out.findIndex((item) => Object.is(idOf(item), beforeIds[i]));
158
+ if (neighbor >= 0) {
159
+ at = neighbor + 1;
160
+ break;
161
+ }
162
+ }
163
+ out.splice(at, 0, beforeById.get(id));
164
+ }
165
+ // 4 · A REORDER: the old order back, only where the optimistic order still stands.
166
+ const kept = beforeIds.filter((id) => afterById.has(id));
167
+ const keptAfter = afterIds.filter((id) => beforeById.has(id));
168
+ if (kept.some((id, i) => !Object.is(id, keptAfter[i]))) {
169
+ const keep = new Set(kept);
170
+ const positions = [];
171
+ out.forEach((item, i) => {
172
+ if (keep.has(idOf(item)))
173
+ positions.push(i);
174
+ });
175
+ const nowOrder = positions.map((i) => idOf(out[i]));
176
+ if (nowOrder.length === keptAfter.length && nowOrder.every((id, i) => Object.is(id, keptAfter[i]))) {
177
+ const byId = new Map(positions.map((i) => [idOf(out[i]), out[i]]));
178
+ edit();
179
+ positions.forEach((slot, i) => {
180
+ out[slot] = byId.get(kept[i]);
181
+ });
182
+ }
183
+ }
184
+ return touched ? out : current;
185
+ }
186
+ function revertSet(before, after, current) {
187
+ let out;
188
+ const copy = () => {
189
+ out ??= new Set(current);
190
+ return out;
191
+ };
192
+ for (const member of after) {
193
+ if (!before.has(member) && current.has(member))
194
+ copy().delete(member);
195
+ }
196
+ for (const member of before) {
197
+ if (!after.has(member) && !current.has(member))
198
+ copy().add(member);
199
+ }
200
+ return out ?? current;
201
+ }
202
+ function revertMap(before, after, current, idOf) {
203
+ let out;
204
+ const copy = () => {
205
+ out ??= new Map(current);
206
+ return out;
207
+ };
208
+ for (const key of new Set([...before.keys(), ...after.keys()])) {
209
+ const had = before.has(key);
210
+ const has = after.has(key);
211
+ if (had && has) {
212
+ if (Object.is(before.get(key), after.get(key)) || !current.has(key))
213
+ continue;
214
+ const next = revertValue(before.get(key), after.get(key), current.get(key), idOf);
215
+ if (!Object.is(next, current.get(key)))
216
+ copy().set(key, next);
217
+ }
218
+ else if (has) {
219
+ if (current.has(key) && Object.is(current.get(key), after.get(key)))
220
+ copy().delete(key);
221
+ }
222
+ else if (!current.has(key)) {
223
+ copy().set(key, before.get(key));
224
+ }
225
+ }
226
+ return out ?? current;
227
+ }
228
+ /**
229
+ * A counter's inverse: give back the difference the change made, whatever has happened since.
230
+ * The whole-value rule is wrong for a count — two deletes take a total 10 → 9 → 8, and when the
231
+ * first fails the total is 8, not 9, so "revert only if still optimistic" would never fire. Pass
232
+ * it as a target's `revert`: `{ set: setTotal, apply: (n) => n - 1, revert: counterRevert }`.
233
+ */
234
+ export const counterRevert = (current, before, after) => current + (before - after);
235
+ //# sourceMappingURL=revert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revert.js","sourceRoot":"","sources":["../../../src/react/optimistic/revert.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.89.0",
3
+ "version": "5.90.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
@@ -122,11 +122,14 @@ export const AREAS: readonly Area[] = [
122
122
  'authSignal.ts',
123
123
  'sessionAuth.ts',
124
124
  'createApiHooks.ts',
125
+ // Draws nothing; `createApiHooks` imports it, and every app's changes go through it (STA-10).
126
+ 'optimistic',
125
127
  'usePagedList.ts',
126
128
  'lazy',
127
129
  'confirm.tsx',
128
130
  'prompt.tsx',
129
131
  'Toast.tsx',
132
+ 'LazyToaster.tsx',
130
133
  'colorScheme',
131
134
  // The look store and its picker: the sibling of colorScheme, installed at boot beside it.
132
135
  'look',
@@ -0,0 +1,18 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { act, render } from '@testing-library/react';
3
+ import { LazyToaster } from './LazyToaster.js';
4
+ import { toast } from './Toast.js';
5
+
6
+ describe('LazyToaster', () => {
7
+ test('mounts the stack once its chunk lands, and a toast shows in it', async () => {
8
+ const { container } = render(<LazyToaster />);
9
+ await act(async () => {
10
+ await new Promise((resolve) => setTimeout(resolve, 20));
11
+ });
12
+ await act(async () => {
13
+ toast('Saved');
14
+ await new Promise((resolve) => setTimeout(resolve, 20));
15
+ });
16
+ expect(container.ownerDocument.body.textContent).toContain('Saved');
17
+ });
18
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `<LazyToaster />` — the toast stack, mounted without putting sonner on the entry chunk.
3
+ *
4
+ * Rollup colors a module into the chunk of its static importers, so an app that mounts `Toaster`
5
+ * from its root pays for the whole stack (~15 kB gzip, measured in family, forge task 2900) on
6
+ * first paint, however lazily its toasts are CALLED (`Button`, `optimistic()`). family solved it
7
+ * by hand with a lazy wrapper of its own; flix, roms and station mounted `Toaster` statically the
8
+ * day they first needed a toast (forge task 3583) and their entries grew by it. This is that
9
+ * wrapper, once: the stack's chunk loads after first paint, and a toast raised before it lands
10
+ * waits for it (sonner queues calls made before its Toaster mounts).
11
+ *
12
+ * Takes `Toaster`'s props. Mount it once, at the app's root, in place of `Toaster`.
13
+ */
14
+ import { Suspense } from 'react';
15
+ import { lazyWithReload } from './lazy/staleChunk.js';
16
+ import type { ToasterProps } from './Toast.js';
17
+
18
+ const Stack = lazyWithReload(() => import('./Toast.js').then((m) => ({ default: m.Toaster })));
19
+
20
+ export function LazyToaster(props: ToasterProps) {
21
+ return (
22
+ <Suspense fallback={null}>
23
+ <Stack {...props} />
24
+ </Suspense>
25
+ );
26
+ }