@buoy-gg/jotai 7.0.40 → 7.0.42
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 +11 -76
- package/lib/commonjs/index.js +1 -112
- package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +1 -199
- package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +1 -85
- package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +1 -715
- package/lib/commonjs/jotai/components/JotaiAtomValueDetailContent.js +1 -444
- package/lib/commonjs/jotai/components/JotaiEventFilterView.js +1 -43
- package/lib/commonjs/jotai/components/JotaiIcon.js +1 -18
- package/lib/commonjs/jotai/components/JotaiModal.js +1 -743
- package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +1 -69
- package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +1 -306
- package/lib/commonjs/jotai/types/index.js +1 -1
- package/lib/commonjs/jotai/utils/jotaiStateStore.js +1 -321
- package/lib/commonjs/jotai/utils/snapshotProvider.js +1 -215
- package/lib/commonjs/jotai/utils/testIDs.js +1 -90
- package/lib/commonjs/jotai/utils/watchAtoms.js +1 -162
- package/lib/commonjs/jotai/utils/writeSemantics.js +1 -47
- package/lib/commonjs/preset.js +1 -90
- package/lib/module/index.js +1 -85
- package/lib/module/jotai/components/JotaiAtomBrowser.js +1 -195
- package/lib/module/jotai/components/JotaiAtomChangeItem.js +1 -81
- package/lib/module/jotai/components/JotaiAtomDetailContent.js +1 -710
- package/lib/module/jotai/components/JotaiAtomValueDetailContent.js +1 -442
- package/lib/module/jotai/components/JotaiEventFilterView.js +1 -39
- package/lib/module/jotai/components/JotaiIcon.js +1 -11
- package/lib/module/jotai/components/JotaiModal.js +1 -738
- package/lib/module/jotai/hooks/useJotaiAtomChanges.js +1 -65
- package/lib/module/jotai/sync/jotaiSyncAdapter.js +1 -303
- package/lib/module/jotai/types/index.js +1 -1
- package/lib/module/jotai/utils/jotaiStateStore.js +1 -317
- package/lib/module/jotai/utils/snapshotProvider.js +1 -214
- package/lib/module/jotai/utils/testIDs.js +1 -82
- package/lib/module/jotai/utils/watchAtoms.js +1 -157
- package/lib/module/jotai/utils/writeSemantics.js +1 -43
- package/lib/module/preset.js +1 -87
- package/lib/web/index.mjs +2 -0
- package/package.json +35 -12
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiIcon.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiModal.d.ts.map +0 -1
- package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +0 -1
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +0 -1
- package/lib/typescript/jotai/types/index.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/snapshotProvider.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/testIDs.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/writeSemantics.d.ts.map +0 -1
- package/lib/typescript/preset.d.ts.map +0 -1
|
@@ -1,321 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.jotaiStateStore = void 0;
|
|
7
|
-
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
8
|
-
/**
|
|
9
|
-
* Jotai state store — captures and stores Jotai atom changes
|
|
10
|
-
*
|
|
11
|
-
* Mirrors the architecture of zustandStateStore.ts from @buoy-gg/zustand
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
// ============================================
|
|
15
|
-
// Atom Color Palette
|
|
16
|
-
// ============================================
|
|
17
|
-
|
|
18
|
-
const ATOM_COLORS = {
|
|
19
|
-
count: "#10B981",
|
|
20
|
-
// emerald
|
|
21
|
-
auth: "#8B5CF6",
|
|
22
|
-
// purple
|
|
23
|
-
user: "#3B82F6",
|
|
24
|
-
// blue
|
|
25
|
-
cart: "#EC4899",
|
|
26
|
-
// pink
|
|
27
|
-
app: "#6366F1",
|
|
28
|
-
// indigo
|
|
29
|
-
ui: "#F59E0B",
|
|
30
|
-
// amber
|
|
31
|
-
settings: "#14B8A6",
|
|
32
|
-
// teal
|
|
33
|
-
theme: "#06B6D4",
|
|
34
|
-
// cyan
|
|
35
|
-
nav: "#F97316",
|
|
36
|
-
// orange
|
|
37
|
-
form: "#EF4444",
|
|
38
|
-
// red
|
|
39
|
-
modal: "#A855F7",
|
|
40
|
-
// violet
|
|
41
|
-
filter: "#84CC16" // lime
|
|
42
|
-
};
|
|
43
|
-
function getAtomColor(label) {
|
|
44
|
-
return (0, _sharedUi.entityColor)(label, ATOM_COLORS);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ============================================
|
|
48
|
-
// Helper Functions
|
|
49
|
-
// ============================================
|
|
50
|
-
|
|
51
|
-
function formatValuePreview(value, maxLength = 40) {
|
|
52
|
-
return (0, _sharedUi.formatValuePreview)(value, {
|
|
53
|
-
maxLength,
|
|
54
|
-
functionLabel: "(fn)"
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
const getValueDiffSummary = _sharedUi.shallowDiffSummary;
|
|
58
|
-
|
|
59
|
-
// ============================================
|
|
60
|
-
// Jotai State Store
|
|
61
|
-
// ============================================
|
|
62
|
-
|
|
63
|
-
class JotaiStateStore extends _sharedUi.ChangeLog {
|
|
64
|
-
atoms = new Map();
|
|
65
|
-
atomListeners = new Set();
|
|
66
|
-
|
|
67
|
-
// ---- Change Tracking ----
|
|
68
|
-
|
|
69
|
-
addAtomChange(params) {
|
|
70
|
-
if (!this.canRecord()) return;
|
|
71
|
-
const {
|
|
72
|
-
atomLabel,
|
|
73
|
-
prevValue,
|
|
74
|
-
nextValue,
|
|
75
|
-
category = "write"
|
|
76
|
-
} = params;
|
|
77
|
-
const hasValueChange = prevValue !== nextValue;
|
|
78
|
-
const {
|
|
79
|
-
summary,
|
|
80
|
-
changedKeys,
|
|
81
|
-
changedCount
|
|
82
|
-
} = getValueDiffSummary(prevValue, nextValue);
|
|
83
|
-
const valuePreview = formatValuePreview(nextValue);
|
|
84
|
-
const change = {
|
|
85
|
-
id: this.nextId(),
|
|
86
|
-
atomLabel,
|
|
87
|
-
timestamp: Date.now(),
|
|
88
|
-
prevValue,
|
|
89
|
-
nextValue,
|
|
90
|
-
hasValueChange,
|
|
91
|
-
category,
|
|
92
|
-
changedKeys,
|
|
93
|
-
changedKeysCount: changedCount,
|
|
94
|
-
diffSummary: summary,
|
|
95
|
-
valuePreview,
|
|
96
|
-
isSlowUpdate: false
|
|
97
|
-
};
|
|
98
|
-
this.push(change);
|
|
99
|
-
const atomInfo = this.atoms.get(atomLabel);
|
|
100
|
-
if (atomInfo) {
|
|
101
|
-
atomInfo.changeCount++;
|
|
102
|
-
}
|
|
103
|
-
this.notifyListeners();
|
|
104
|
-
this.notifyAtomListeners();
|
|
105
|
-
}
|
|
106
|
-
getAtomChanges() {
|
|
107
|
-
return this.getAll();
|
|
108
|
-
}
|
|
109
|
-
getAtomChangeById(id) {
|
|
110
|
-
return this.getById(id);
|
|
111
|
-
}
|
|
112
|
-
clearAtomChanges() {
|
|
113
|
-
for (const atom of this.atoms.values()) {
|
|
114
|
-
atom.changeCount = 0;
|
|
115
|
-
}
|
|
116
|
-
this.clear();
|
|
117
|
-
this.notifyAtomListeners();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ---- Remote Mirror Mode ----
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Serializable snapshot of the tracked atoms (registry metadata plus each
|
|
124
|
-
* atom's current value). Used by the sync adapter on the device side —
|
|
125
|
-
* the live `getValue` handles can't go over the wire.
|
|
126
|
-
*/
|
|
127
|
-
getAtomSnapshots() {
|
|
128
|
-
return Array.from(this.atoms.values()).map(atom => {
|
|
129
|
-
let currentValue;
|
|
130
|
-
try {
|
|
131
|
-
currentValue = atom.getValue();
|
|
132
|
-
} catch {
|
|
133
|
-
currentValue = undefined;
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
label: atom.label,
|
|
137
|
-
changeCount: atom.changeCount,
|
|
138
|
-
color: atom.color,
|
|
139
|
-
currentValue,
|
|
140
|
-
writeKind: atom.writeKind
|
|
141
|
-
};
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Replace the entire mirror contents from a synced device snapshot. The
|
|
147
|
-
* rebuilt registry entries get stub `getValue` handles serving the
|
|
148
|
-
* snapshotted value. Respects the enabled flag so the UI's pause button
|
|
149
|
-
* freezes the mirror.
|
|
150
|
-
*/
|
|
151
|
-
replaceFromSnapshot(changes, atomSnapshots) {
|
|
152
|
-
if (!this.getEnabled()) return;
|
|
153
|
-
|
|
154
|
-
// Preserve values a remote mirror already hydrated — wire-form snapshots
|
|
155
|
-
// always carry markers, and a later snapshot must not blank the detail pane.
|
|
156
|
-
const hydrated = new Map();
|
|
157
|
-
for (const existing of this.items) {
|
|
158
|
-
const prev = existing.prevValue;
|
|
159
|
-
if (!prev || prev.__buoyValueOnDevice !== true) {
|
|
160
|
-
hydrated.set(existing.id, existing);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const merged = changes.slice(0, this.max).map(change => {
|
|
164
|
-
const kept = hydrated.get(change.id);
|
|
165
|
-
const incoming = change.prevValue;
|
|
166
|
-
return kept && incoming?.__buoyValueOnDevice === true ? {
|
|
167
|
-
...change,
|
|
168
|
-
prevValue: kept.prevValue,
|
|
169
|
-
nextValue: kept.nextValue
|
|
170
|
-
} : change;
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Preserve currentValue a remote mirror already hydrated — oversized
|
|
174
|
-
// atoms arrive as VALUE_ON_DEVICE markers on every snapshot.
|
|
175
|
-
const hydratedAtoms = new Map();
|
|
176
|
-
for (const [label, atom] of this.atoms) {
|
|
177
|
-
try {
|
|
178
|
-
const value = atom.getValue();
|
|
179
|
-
const marker = value;
|
|
180
|
-
if (!marker || marker.__buoyValueOnDevice !== true) {
|
|
181
|
-
hydratedAtoms.set(label, value);
|
|
182
|
-
}
|
|
183
|
-
} catch {
|
|
184
|
-
// ignore
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
this.atoms = new Map(atomSnapshots.map(snapshot => {
|
|
188
|
-
const incoming = snapshot.currentValue;
|
|
189
|
-
const kept = hydratedAtoms.get(snapshot.label);
|
|
190
|
-
const currentValue = kept !== undefined && incoming?.__buoyValueOnDevice === true ? kept : snapshot.currentValue;
|
|
191
|
-
return [snapshot.label, {
|
|
192
|
-
label: snapshot.label,
|
|
193
|
-
changeCount: snapshot.changeCount,
|
|
194
|
-
color: snapshot.color,
|
|
195
|
-
getValue: () => currentValue,
|
|
196
|
-
// No setValue: the mirror has no wire back to the device from
|
|
197
|
-
// this entry (yet) — the detail screen renders read-only there.
|
|
198
|
-
writeKind: snapshot.writeKind
|
|
199
|
-
}];
|
|
200
|
-
}));
|
|
201
|
-
this.setItems(merged);
|
|
202
|
-
this.notifyAtomListeners();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Hydrate one mirrored change with real values fetched on demand from the
|
|
207
|
-
* device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
|
|
208
|
-
*/
|
|
209
|
-
hydrateChange(id, patch) {
|
|
210
|
-
this.hydrateById(id, patch);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Hydrate one mirrored atom's currentValue fetched on demand from the
|
|
215
|
-
* device via the adapter's `getAtomValue`. Remote-mirror (desktop) only.
|
|
216
|
-
*/
|
|
217
|
-
hydrateAtomValue(label, currentValue) {
|
|
218
|
-
const atom = this.atoms.get(label);
|
|
219
|
-
if (!atom) return;
|
|
220
|
-
this.atoms.set(label, {
|
|
221
|
-
...atom,
|
|
222
|
-
getValue: () => currentValue
|
|
223
|
-
});
|
|
224
|
-
this.notifyAtomListeners();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ---- Atom Registry ----
|
|
228
|
-
|
|
229
|
-
registerAtom(label, getValue, setValue, writeKind) {
|
|
230
|
-
if (this.atoms.has(label)) return;
|
|
231
|
-
const atomInfo = {
|
|
232
|
-
label,
|
|
233
|
-
changeCount: 0,
|
|
234
|
-
color: getAtomColor(label),
|
|
235
|
-
getValue,
|
|
236
|
-
setValue,
|
|
237
|
-
writeKind
|
|
238
|
-
};
|
|
239
|
-
this.atoms.set(label, atomInfo);
|
|
240
|
-
this.notifyAtomListeners();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Write a value to a registered atom (remote write from the dashboard/MCP).
|
|
245
|
-
* Throws if the atom is unknown or read-only (derived atoms have no setter).
|
|
246
|
-
*/
|
|
247
|
-
setAtomValue(label, value) {
|
|
248
|
-
const atom = this.atoms.get(label);
|
|
249
|
-
if (!atom) {
|
|
250
|
-
throw new Error(`Atom "${label}" is not registered`);
|
|
251
|
-
}
|
|
252
|
-
if (!atom.setValue) {
|
|
253
|
-
throw new Error(`Atom "${label}" is read-only (derived) — cannot set`);
|
|
254
|
-
}
|
|
255
|
-
atom.setValue(value);
|
|
256
|
-
}
|
|
257
|
-
unregisterAtom(label) {
|
|
258
|
-
this.atoms.delete(label);
|
|
259
|
-
this.notifyAtomListeners();
|
|
260
|
-
}
|
|
261
|
-
getAtoms() {
|
|
262
|
-
return Array.from(this.atoms.values());
|
|
263
|
-
}
|
|
264
|
-
getAtom(label) {
|
|
265
|
-
return this.atoms.get(label);
|
|
266
|
-
}
|
|
267
|
-
getAtomColor(label) {
|
|
268
|
-
return this.atoms.get(label)?.color ?? getAtomColor(label);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ---- Filtering ----
|
|
272
|
-
|
|
273
|
-
filterAtomChanges(filter) {
|
|
274
|
-
let filtered = this.getAll();
|
|
275
|
-
if (filter.searchText) {
|
|
276
|
-
const search = filter.searchText.toLowerCase();
|
|
277
|
-
filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
|
|
278
|
-
}
|
|
279
|
-
if (filter.atomLabels && filter.atomLabels.length > 0) {
|
|
280
|
-
filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
|
|
281
|
-
}
|
|
282
|
-
if (filter.onlyWithChanges) {
|
|
283
|
-
filtered = filtered.filter(c => c.hasValueChange);
|
|
284
|
-
}
|
|
285
|
-
return filtered;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ---- Stats ----
|
|
289
|
-
|
|
290
|
-
getStats() {
|
|
291
|
-
const total = this.items.length;
|
|
292
|
-
const withChanges = this.items.filter(c => c.hasValueChange).length;
|
|
293
|
-
return {
|
|
294
|
-
totalChanges: total,
|
|
295
|
-
changesWithValueChange: withChanges,
|
|
296
|
-
changesWithoutValueChange: total - withChanges,
|
|
297
|
-
atomCount: this.atoms.size
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
getUniqueAtomLabels() {
|
|
301
|
-
return Array.from(this.atoms.keys()).sort();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ---- Enable / Disable ----
|
|
305
|
-
|
|
306
|
-
setMaxChanges(max) {
|
|
307
|
-
this.setMax(max);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ---- Subscriptions ----
|
|
311
|
-
|
|
312
|
-
subscribeToAtoms(listener) {
|
|
313
|
-
this.atomListeners.add(listener);
|
|
314
|
-
return () => this.atomListeners.delete(listener);
|
|
315
|
-
}
|
|
316
|
-
notifyAtomListeners() {
|
|
317
|
-
const atoms = this.getAtoms();
|
|
318
|
-
this.atomListeners.forEach(listener => listener(atoms));
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
const jotaiStateStore = exports.jotaiStateStore = new JotaiStateStore();
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.jotaiStateStore=void 0;var _sharedUi=require("@buoy-gg/shared-ui");const ATOM_COLORS={count:"#10B981",auth:"#8B5CF6",user:"#3B82F6",cart:"#EC4899",app:"#6366F1",ui:"#F59E0B",settings:"#14B8A6",theme:"#06B6D4",nav:"#F97316",form:"#EF4444",modal:"#A855F7",filter:"#84CC16"};function getAtomColor(h){return(0,_sharedUi.entityColor)(h,ATOM_COLORS)}function formatValuePreview(h,t=40){return(0,_sharedUi.formatValuePreview)(h,{maxLength:t,functionLabel:"(fn)"})}const getValueDiffSummary=_sharedUi.shallowDiffSummary;class JotaiStateStore extends _sharedUi.ChangeLog{atoms=new Map;atomListeners=new Set;addAtomChange(t){if(!this.canRecord())return;const{atomLabel:e,prevValue:o,nextValue:a,category:i="write"}=t;const s=o!==a;const{summary:r,changedKeys:n,changedCount:u}=getValueDiffSummary(o,a);const l=formatValuePreview(a);const c={id:this.nextId(),atomLabel:e,timestamp:Date.now(),prevValue:o,nextValue:a,hasValueChange:s,category:i,changedKeys:n,changedKeysCount:u,diffSummary:r,valuePreview:l,isSlowUpdate:false};this.push(c);const m=this.atoms.get(e);if(m){m.changeCount++}this.notifyListeners();this.notifyAtomListeners()}getAtomChanges(){return this.getAll()}getAtomChangeById(t){return this.getById(t)}clearAtomChanges(){for(const t of this.atoms.values()){t.changeCount=0}this.clear();this.notifyAtomListeners()}getAtomSnapshots(){return Array.from(this.atoms.values()).map(t=>{let e;try{e=t.getValue()}catch{e=void 0}return{label:t.label,changeCount:t.changeCount,color:t.color,currentValue:e,writeKind:t.writeKind}})}replaceFromSnapshot(t,e){if(!this.getEnabled())return;const o=new Map;for(const s of this.items){const r=s.prevValue;if(!r||r.__buoyValueOnDevice!==true){o.set(s.id,s)}}const a=t.slice(0,this.max).map(s=>{const r=o.get(s.id);const n=s.prevValue;return r&&n?.__buoyValueOnDevice===true?{...s,prevValue:r.prevValue,nextValue:r.nextValue}:s});const i=new Map;for(const[s,r]of this.atoms){try{const n=r.getValue();const u=n;if(!u||u.__buoyValueOnDevice!==true){i.set(s,n)}}catch{}}this.atoms=new Map(e.map(s=>{const r=s.currentValue;const n=i.get(s.label);const u=n!==void 0&&r?.__buoyValueOnDevice===true?n:s.currentValue;return[s.label,{label:s.label,changeCount:s.changeCount,color:s.color,getValue:()=>u,writeKind:s.writeKind}]}));this.setItems(a);this.notifyAtomListeners()}hydrateChange(t,e){this.hydrateById(t,e)}hydrateAtomValue(t,e){const o=this.atoms.get(t);if(!o)return;this.atoms.set(t,{...o,getValue:()=>e});this.notifyAtomListeners()}registerAtom(t,e,o,a){if(this.atoms.has(t))return;const i={label:t,changeCount:0,color:getAtomColor(t),getValue:e,setValue:o,writeKind:a};this.atoms.set(t,i);this.notifyAtomListeners()}setAtomValue(t,e){const o=this.atoms.get(t);if(!o){throw new Error(`Atom "${t}" is not registered`)}if(!o.setValue){throw new Error(`Atom "${t}" is read-only (derived) \u2014 cannot set`)}o.setValue(e)}unregisterAtom(t){this.atoms.delete(t);this.notifyAtomListeners()}getAtoms(){return Array.from(this.atoms.values())}getAtom(t){return this.atoms.get(t)}getAtomColor(t){return this.atoms.get(t)?.color??getAtomColor(t)}filterAtomChanges(t){let e=this.getAll();if(t.searchText){const o=t.searchText.toLowerCase();e=e.filter(a=>a.atomLabel.toLowerCase().includes(o)||a.valuePreview.toLowerCase().includes(o)||a.changedKeys.some(i=>i.toLowerCase().includes(o)))}if(t.atomLabels&&t.atomLabels.length>0){e=e.filter(o=>t.atomLabels.includes(o.atomLabel))}if(t.onlyWithChanges){e=e.filter(o=>o.hasValueChange)}return e}getStats(){const t=this.items.length;const e=this.items.filter(o=>o.hasValueChange).length;return{totalChanges:t,changesWithValueChange:e,changesWithoutValueChange:t-e,atomCount:this.atoms.size}}getUniqueAtomLabels(){return Array.from(this.atoms.keys()).sort()}setMaxChanges(t){this.setMax(t)}subscribeToAtoms(t){this.atomListeners.add(t);return()=>this.atomListeners.delete(t)}notifyAtomListeners(){const t=this.getAtoms();this.atomListeners.forEach(e=>e(t))}}const jotaiStateStore=exports.jotaiStateStore=new JotaiStateStore;
|
|
@@ -1,215 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
4
|
-
var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
|
|
5
|
-
var _jotaiStateStore = require("./jotaiStateStore");
|
|
6
|
-
/**
|
|
7
|
-
* Time Machine snapshot provider for Jotai.
|
|
8
|
-
*
|
|
9
|
-
* Registered into @buoy-gg/shared-ui's stateSnapshotRegistry at module init;
|
|
10
|
-
* consumed by @buoy-gg/time-machine.
|
|
11
|
-
*
|
|
12
|
-
* Coverage is opt-in by nature: only atoms the app passed to `watchAtoms` /
|
|
13
|
-
* `watchDefaultStoreAtoms` are visible. Derived (read-only) atoms are captured
|
|
14
|
-
* for inspection but never restored — they recompute from their sources.
|
|
15
|
-
* Caveat (documented): atoms with custom write functions (`atomWithReducer`)
|
|
16
|
-
* receive ACTIONS through `store.set`, so restoring a raw value can misbehave;
|
|
17
|
-
* nothing in the registry distinguishes them from plain atoms.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
function status() {
|
|
21
|
-
const atoms = _jotaiStateStore.jotaiStateStore.getAtoms();
|
|
22
|
-
if (atoms.length === 0) {
|
|
23
|
-
return {
|
|
24
|
-
canCapture: false,
|
|
25
|
-
canRestore: false,
|
|
26
|
-
detail: "No Jotai atoms registered — pass your atoms to watchAtoms() / watchDefaultStoreAtoms()"
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
const writable = atoms.filter(a => a.setValue).length;
|
|
30
|
-
return {
|
|
31
|
-
canCapture: true,
|
|
32
|
-
canRestore: writable > 0,
|
|
33
|
-
detail: writable < atoms.length ? `${atoms.length - writable} derived atom(s) recompute automatically and are not restored` : undefined
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
async function capture() {
|
|
37
|
-
const warnings = [];
|
|
38
|
-
const atoms = {};
|
|
39
|
-
let derived = 0;
|
|
40
|
-
for (const info of _jotaiStateStore.jotaiStateStore.getAtoms()) {
|
|
41
|
-
try {
|
|
42
|
-
const value = info.getValue();
|
|
43
|
-
if (typeof value === "function") {
|
|
44
|
-
warnings.push(`atom "${info.label}": function value not capturable`);
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (!info.setValue) derived += 1;
|
|
48
|
-
atoms[info.label] = {
|
|
49
|
-
value,
|
|
50
|
-
writable: Boolean(info.setValue)
|
|
51
|
-
};
|
|
52
|
-
} catch (e) {
|
|
53
|
-
warnings.push(`atom "${info.label}": capture failed (${String(e)})`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (derived > 0) {
|
|
57
|
-
warnings.push(`${derived} derived atom(s) captured for inspection only (recompute on restore)`);
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
data: {
|
|
61
|
-
atoms
|
|
62
|
-
},
|
|
63
|
-
warnings
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
async function restore(data, options) {
|
|
67
|
-
const snapshot = data;
|
|
68
|
-
const scope = (0, _sharedUi.createRestoreScope)(options);
|
|
69
|
-
const result = (0, _sharedUi.emptyRestoreResult)();
|
|
70
|
-
if (!snapshot?.atoms) {
|
|
71
|
-
return (0, _sharedUi.failedRestore)("empty jotai snapshot");
|
|
72
|
-
}
|
|
73
|
-
let derived = 0;
|
|
74
|
-
for (const [label, entry] of Object.entries(snapshot.atoms)) {
|
|
75
|
-
if (!scope.allows(label)) continue;
|
|
76
|
-
if (!entry.writable) {
|
|
77
|
-
derived += 1;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
const info = _jotaiStateStore.jotaiStateStore.getAtom(label);
|
|
81
|
-
if (!info) {
|
|
82
|
-
result.skipped.push({
|
|
83
|
-
id: label,
|
|
84
|
-
reason: "atom not registered (renamed, or unwatched since snapshot?)"
|
|
85
|
-
});
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (!info.setValue) {
|
|
89
|
-
result.skipped.push({
|
|
90
|
-
id: label,
|
|
91
|
-
reason: "atom is now read-only (derived)"
|
|
92
|
-
});
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
info.setValue(entry.value);
|
|
97
|
-
result.applied += 1;
|
|
98
|
-
} catch (e) {
|
|
99
|
-
result.skipped.push({
|
|
100
|
-
id: label,
|
|
101
|
-
reason: String(e)
|
|
102
|
-
});
|
|
103
|
-
result.ok = false;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (derived > 0) {
|
|
107
|
-
result.warnings.push(`${derived} derived atom(s) left to recompute`);
|
|
108
|
-
}
|
|
109
|
-
const excludedWarning = scope.excludedWarning("atom(s)");
|
|
110
|
-
if (excludedWarning) result.warnings.push(excludedWarning);
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Per-atom restore preview. `base` omitted = compare against live atoms. */
|
|
115
|
-
async function previewItems(target, base) {
|
|
116
|
-
const targetAtoms = target?.atoms ?? {};
|
|
117
|
-
const liveMode = base === undefined;
|
|
118
|
-
const baseAtoms = {};
|
|
119
|
-
if (liveMode) {
|
|
120
|
-
for (const info of _jotaiStateStore.jotaiStateStore.getAtoms()) {
|
|
121
|
-
try {
|
|
122
|
-
baseAtoms[info.label] = {
|
|
123
|
-
value: info.getValue(),
|
|
124
|
-
writable: Boolean(info.setValue)
|
|
125
|
-
};
|
|
126
|
-
} catch {
|
|
127
|
-
// unreadable atom — treated as absent
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
} else {
|
|
131
|
-
Object.assign(baseAtoms, base?.atoms ?? {});
|
|
132
|
-
}
|
|
133
|
-
const items = [];
|
|
134
|
-
const notes = [];
|
|
135
|
-
for (const label of (0, _sharedUi.unionKeys)(baseAtoms, targetAtoms)) {
|
|
136
|
-
const inTarget = label in targetAtoms;
|
|
137
|
-
if (!inTarget) {
|
|
138
|
-
if (liveMode) notes.push(`atom "${label}" not in snapshot — untouched`);else items.push({
|
|
139
|
-
key: label,
|
|
140
|
-
label,
|
|
141
|
-
verdict: "removed",
|
|
142
|
-
oldValue: baseAtoms[label]?.value
|
|
143
|
-
});
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
const entry = targetAtoms[label];
|
|
147
|
-
const equal = label in baseAtoms && (0, _dataViewer.boundedDeepEqual)(baseAtoms[label].value, entry.value);
|
|
148
|
-
if (!entry.writable) {
|
|
149
|
-
items.push({
|
|
150
|
-
key: label,
|
|
151
|
-
label,
|
|
152
|
-
verdict: equal ? "unchanged" : "recomputes",
|
|
153
|
-
reason: equal ? undefined : "derived atom — recomputes from its sources",
|
|
154
|
-
oldValue: baseAtoms[label]?.value,
|
|
155
|
-
newValue: entry.value
|
|
156
|
-
});
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (liveMode) {
|
|
160
|
-
const info = _jotaiStateStore.jotaiStateStore.getAtom(label);
|
|
161
|
-
if (!info) {
|
|
162
|
-
items.push({
|
|
163
|
-
key: label,
|
|
164
|
-
label,
|
|
165
|
-
verdict: "wont-apply",
|
|
166
|
-
reason: "atom not registered (renamed, or unwatched since snapshot?)"
|
|
167
|
-
});
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (!info.setValue) {
|
|
171
|
-
items.push({
|
|
172
|
-
key: label,
|
|
173
|
-
label,
|
|
174
|
-
verdict: "wont-apply",
|
|
175
|
-
reason: "atom is now read-only (derived)"
|
|
176
|
-
});
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (!(label in baseAtoms)) {
|
|
181
|
-
items.push({
|
|
182
|
-
key: label,
|
|
183
|
-
label,
|
|
184
|
-
verdict: "added",
|
|
185
|
-
newValue: entry.value
|
|
186
|
-
});
|
|
187
|
-
} else if (equal) {
|
|
188
|
-
items.push({
|
|
189
|
-
key: label,
|
|
190
|
-
label,
|
|
191
|
-
verdict: "unchanged"
|
|
192
|
-
});
|
|
193
|
-
} else {
|
|
194
|
-
items.push({
|
|
195
|
-
key: label,
|
|
196
|
-
label,
|
|
197
|
-
verdict: "changed",
|
|
198
|
-
oldValue: baseAtoms[label].value,
|
|
199
|
-
newValue: entry.value
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
items,
|
|
205
|
-
notes
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
(0, _sharedUi.registerStateSnapshotProvider)({
|
|
209
|
-
id: "jotai",
|
|
210
|
-
label: "Jotai",
|
|
211
|
-
status,
|
|
212
|
-
capture,
|
|
213
|
-
restore,
|
|
214
|
-
previewItems
|
|
215
|
-
});
|
|
1
|
+
"use strict";var _sharedUi=require("@buoy-gg/shared-ui");var _dataViewer=require("@buoy-gg/shared-ui/dataViewer");var _jotaiStateStore=require("./jotaiStateStore");function status(){const o=_jotaiStateStore.jotaiStateStore.getAtoms();if(o.length===0){return{canCapture:false,canRestore:false,detail:"No Jotai atoms registered \u2014 pass your atoms to watchAtoms() / watchDefaultStoreAtoms()"}}const r=o.filter(n=>n.setValue).length;return{canCapture:true,canRestore:r>0,detail:r<o.length?`${o.length-r} derived atom(s) recompute automatically and are not restored`:void 0}}async function capture(){const o=[];const r={};let n=0;for(const a of _jotaiStateStore.jotaiStateStore.getAtoms()){try{const t=a.getValue();if(typeof t==="function"){o.push(`atom "${a.label}": function value not capturable`);continue}if(!a.setValue)n+=1;r[a.label]={value:t,writable:Boolean(a.setValue)}}catch(t){o.push(`atom "${a.label}": capture failed (${String(t)})`)}}if(n>0){o.push(`${n} derived atom(s) captured for inspection only (recompute on restore)`)}return{data:{atoms:r},warnings:o}}async function restore(o,r){const n=o;const a=(0,_sharedUi.createRestoreScope)(r);const t=(0,_sharedUi.emptyRestoreResult)();if(!n?.atoms){return(0,_sharedUi.failedRestore)("empty jotai snapshot")}let s=0;for(const[e,c]of Object.entries(n.atoms)){if(!a.allows(e))continue;if(!c.writable){s+=1;continue}const i=_jotaiStateStore.jotaiStateStore.getAtom(e);if(!i){t.skipped.push({id:e,reason:"atom not registered (renamed, or unwatched since snapshot?)"});continue}if(!i.setValue){t.skipped.push({id:e,reason:"atom is now read-only (derived)"});continue}try{i.setValue(c.value);t.applied+=1}catch(l){t.skipped.push({id:e,reason:String(l)});t.ok=false}}if(s>0){t.warnings.push(`${s} derived atom(s) left to recompute`)}const u=a.excludedWarning("atom(s)");if(u)t.warnings.push(u);return t}async function previewItems(o,r){const n=o?.atoms??{};const a=r===void 0;const t={};if(a){for(const e of _jotaiStateStore.jotaiStateStore.getAtoms()){try{t[e.label]={value:e.getValue(),writable:Boolean(e.setValue)}}catch{}}}else{Object.assign(t,r?.atoms??{})}const s=[];const u=[];for(const e of(0,_sharedUi.unionKeys)(t,n)){const c=e in n;if(!c){if(a)u.push(`atom "${e}" not in snapshot \u2014 untouched`);else s.push({key:e,label:e,verdict:"removed",oldValue:t[e]?.value});continue}const i=n[e];const l=e in t&&(0,_dataViewer.boundedDeepEqual)(t[e].value,i.value);if(!i.writable){s.push({key:e,label:e,verdict:l?"unchanged":"recomputes",reason:l?void 0:"derived atom \u2014 recomputes from its sources",oldValue:t[e]?.value,newValue:i.value});continue}if(a){const d=_jotaiStateStore.jotaiStateStore.getAtom(e);if(!d){s.push({key:e,label:e,verdict:"wont-apply",reason:"atom not registered (renamed, or unwatched since snapshot?)"});continue}if(!d.setValue){s.push({key:e,label:e,verdict:"wont-apply",reason:"atom is now read-only (derived)"});continue}}if(!(e in t)){s.push({key:e,label:e,verdict:"added",newValue:i.value})}else if(l){s.push({key:e,label:e,verdict:"unchanged"})}else{s.push({key:e,label:e,verdict:"changed",oldValue:t[e].value,newValue:i.value})}}return{items:s,notes:u}}(0,_sharedUi.registerStateSnapshotProvider)({id:"jotai",label:"Jotai",status,capture,restore,previewItems});
|
|
@@ -1,90 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.JOTAI_TEST_ID = void 0;
|
|
7
|
-
exports.atomRowTestID = atomRowTestID;
|
|
8
|
-
exports.buildChangeRowTestIDs = buildChangeRowTestIDs;
|
|
9
|
-
exports.changeRowTestID = changeRowTestID;
|
|
10
|
-
exports.slugForTestID = slugForTestID;
|
|
11
|
-
/**
|
|
12
|
-
* Jotai tool test handles.
|
|
13
|
-
*
|
|
14
|
-
* Every tappable and every value the e2e suite reads back is addressed
|
|
15
|
-
* `buoy-jotai-<thing>`. Two rules need explaining, and the second is the one
|
|
16
|
-
* this tool does not share with any other.
|
|
17
|
-
*
|
|
18
|
-
* **A change row's handle carries an ORDINAL, and it counts per ATOM.** Same
|
|
19
|
-
* reason as the Zustand tool: an atom a screen writes to fifty times produces
|
|
20
|
-
* fifty separate, equally real rows, and addressing them by label alone would
|
|
21
|
-
* give fifty views one handle — Detox fails a *matcher* that hits more than
|
|
22
|
-
* one element, so the suite would break on `toExist()` before any assertion,
|
|
23
|
-
* with an error that says nothing about duplicate changes. The handle is
|
|
24
|
-
* `buoy-jotai-row-<slug(atomLabel)>-<n>`, n counting back from the newest row
|
|
25
|
-
* for that atom (1 = newest). Deliberately relative: an atom change has no
|
|
26
|
-
* identity a host could know in advance, and what a test CAN know is "the
|
|
27
|
-
* write I just fired is the newest change to that atom". Read and tap in one
|
|
28
|
-
* beat — a row's ordinal moves the instant another change lands.
|
|
29
|
-
*
|
|
30
|
-
* **The ordinal is per ATOM, and one write can move several atoms.** This is
|
|
31
|
-
* where jotai differs from every store-shaped tool. Atoms form a dependency
|
|
32
|
-
* graph, and this tool watches NODES: a single `set` on a primitive atom
|
|
33
|
-
* notifies every mounted derived atom whose value actually moved, so one
|
|
34
|
-
* press can add a row under three different labels — and add nothing at all
|
|
35
|
-
* under a fourth whose recomputed value happened to be unchanged. Counting
|
|
36
|
-
* ordinals across the whole list would make a row's handle depend on how many
|
|
37
|
-
* *other* atoms the write happened to touch, which is exactly the thing a
|
|
38
|
-
* test of a graph must not assume.
|
|
39
|
-
*
|
|
40
|
-
* **An atom row's handle does NOT carry an ordinal.** An atom is registered
|
|
41
|
-
* once under a label that is unique by construction
|
|
42
|
-
* (`jotaiStateStore.registerAtom` returns early on a label it already holds,
|
|
43
|
-
* and `watchAtoms` marks the atom with a per-label symbol), so
|
|
44
|
-
* `buoy-jotai-atom-<slug(label)>` is as stable as the registration itself —
|
|
45
|
-
* and unlike a change, it is a thing the host knows the name of before the
|
|
46
|
-
* app has booted.
|
|
47
|
-
*
|
|
48
|
-
* The suite mirrors these rules in CommonJS (`example-dev-build/utils/
|
|
49
|
-
* jotaiBench.js`) and asserts the two agree — Detox runs in Node on the host
|
|
50
|
-
* and cannot import TypeScript.
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
/** Prefix every handle in this tool shares. */
|
|
54
|
-
const JOTAI_TEST_ID = exports.JOTAI_TEST_ID = "buoy-jotai";
|
|
55
|
-
|
|
56
|
-
/** `jbenchCount` → `jbenchcount`. Shared with every Buoy tool. */
|
|
57
|
-
function slugForTestID(value) {
|
|
58
|
-
return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Handle for one change row: `buoy-jotai-row-jbenchcount-1`.
|
|
63
|
-
*
|
|
64
|
-
* `ordinal` is 1-based and counts back from the newest row for this atom.
|
|
65
|
-
*/
|
|
66
|
-
function changeRowTestID(atomLabel, ordinal) {
|
|
67
|
-
return `${JOTAI_TEST_ID}-row-${slugForTestID(atomLabel)}-${ordinal}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Handle for one atom row in the browser: `buoy-jotai-atom-jbenchcount`. */
|
|
71
|
-
function atomRowTestID(atomLabel) {
|
|
72
|
-
return `${JOTAI_TEST_ID}-atom-${slugForTestID(atomLabel)}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Row handle per change id for one rendered list, newest-first.
|
|
77
|
-
*
|
|
78
|
-
* Built once for the whole list rather than per row, because an ordinal is a
|
|
79
|
-
* fact about the list and a row cannot compute its own.
|
|
80
|
-
*/
|
|
81
|
-
function buildChangeRowTestIDs(changes) {
|
|
82
|
-
const seen = new Map();
|
|
83
|
-
const handles = new Map();
|
|
84
|
-
for (const change of changes) {
|
|
85
|
-
const ordinal = (seen.get(change.atomLabel) ?? 0) + 1;
|
|
86
|
-
seen.set(change.atomLabel, ordinal);
|
|
87
|
-
handles.set(change.id, changeRowTestID(change.atomLabel, ordinal));
|
|
88
|
-
}
|
|
89
|
-
return handles;
|
|
90
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.JOTAI_TEST_ID=void 0;exports.atomRowTestID=atomRowTestID;exports.buildChangeRowTestIDs=buildChangeRowTestIDs;exports.changeRowTestID=changeRowTestID;exports.slugForTestID=slugForTestID;const JOTAI_TEST_ID=exports.JOTAI_TEST_ID="buoy-jotai";function slugForTestID(e){return String(e??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unknown"}function changeRowTestID(e,t){return`${JOTAI_TEST_ID}-row-${slugForTestID(e)}-${t}`}function atomRowTestID(e){return`${JOTAI_TEST_ID}-atom-${slugForTestID(e)}`}function buildChangeRowTestIDs(e){const t=new Map;const n=new Map;for(const o of e){const s=(t.get(o.atomLabel)??0)+1;t.set(o.atomLabel,s);n.set(o.id,changeRowTestID(o.atomLabel,s))}return n}
|