@buoy-gg/jotai 7.0.33 → 7.0.34
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/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +7 -52
- package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +8 -87
- package/lib/commonjs/jotai/components/JotaiEventFilterView.js +21 -299
- package/lib/commonjs/jotai/components/JotaiModal.js +20 -85
- package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +33 -49
- package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +19 -66
- package/lib/commonjs/jotai/utils/jotaiStateStore.js +23 -188
- package/lib/commonjs/jotai/utils/snapshotProvider.js +7 -25
- package/lib/commonjs/preset.js +14 -22
- package/lib/module/jotai/components/JotaiAtomChangeItem.js +9 -54
- package/lib/module/jotai/components/JotaiAtomDetailContent.js +9 -88
- package/lib/module/jotai/components/JotaiEventFilterView.js +24 -302
- package/lib/module/jotai/components/JotaiModal.js +22 -87
- package/lib/module/jotai/hooks/useJotaiAtomChanges.js +34 -50
- package/lib/module/jotai/sync/jotaiSyncAdapter.js +20 -67
- package/lib/module/jotai/utils/jotaiStateStore.js +23 -188
- package/lib/module/jotai/utils/snapshotProvider.js +8 -26
- package/lib/module/preset.js +13 -20
- package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts +2 -2
- package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
- package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts +2 -1
- package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +1 -1
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +54 -68
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +2 -25
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
- package/lib/typescript/preset.d.ts +2 -8
- package/lib/typescript/preset.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -5,78 +5,62 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.useJotaiAtomChanges = useJotaiAtomChanges;
|
|
7
7
|
var _react = require("react");
|
|
8
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
8
9
|
var _jotaiStateStore = require("../utils/jotaiStateStore");
|
|
9
10
|
/**
|
|
10
11
|
* Hook for consuming Jotai atom changes from the store
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
+
* The subscribe / filter / pause / clear plumbing is shared `useChangeLog`;
|
|
14
|
+
* this file owns the jotai-specific filter, stats and atom registry.
|
|
13
15
|
*/
|
|
14
16
|
|
|
17
|
+
function filterChanges(changes, filter) {
|
|
18
|
+
let filtered = changes;
|
|
19
|
+
if (filter.searchText) {
|
|
20
|
+
const search = filter.searchText.toLowerCase();
|
|
21
|
+
filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
|
|
22
|
+
}
|
|
23
|
+
if (filter.atomLabels && filter.atomLabels.length > 0) {
|
|
24
|
+
filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
|
|
25
|
+
}
|
|
26
|
+
if (filter.onlyWithChanges) {
|
|
27
|
+
filtered = filtered.filter(c => c.hasValueChange);
|
|
28
|
+
}
|
|
29
|
+
return filtered;
|
|
30
|
+
}
|
|
31
|
+
const readAtoms = () => _jotaiStateStore.jotaiStateStore.getAtoms();
|
|
32
|
+
const subscribeToAtoms = listener => _jotaiStateStore.jotaiStateStore.subscribeToAtoms(listener);
|
|
15
33
|
function useJotaiAtomChanges() {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
(0, _react.useEffect)(() => {
|
|
21
|
-
const unsubChanges = _jotaiStateStore.jotaiStateStore.subscribe(newChanges => {
|
|
22
|
-
setAtomChanges(newChanges);
|
|
23
|
-
});
|
|
24
|
-
const unsubAtoms = _jotaiStateStore.jotaiStateStore.subscribeToAtoms(newAtoms => {
|
|
25
|
-
setAtoms(newAtoms);
|
|
26
|
-
});
|
|
27
|
-
return () => {
|
|
28
|
-
unsubChanges();
|
|
29
|
-
unsubAtoms();
|
|
30
|
-
};
|
|
31
|
-
}, []);
|
|
32
|
-
const filteredChanges = (0, _react.useMemo)(() => {
|
|
33
|
-
let filtered = atomChanges;
|
|
34
|
-
if (filter.searchText) {
|
|
35
|
-
const search = filter.searchText.toLowerCase();
|
|
36
|
-
filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
|
|
37
|
-
}
|
|
38
|
-
if (filter.atomLabels && filter.atomLabels.length > 0) {
|
|
39
|
-
filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
|
|
40
|
-
}
|
|
41
|
-
if (filter.onlyWithChanges) {
|
|
42
|
-
filtered = filtered.filter(c => c.hasValueChange);
|
|
43
|
-
}
|
|
44
|
-
return filtered;
|
|
45
|
-
}, [atomChanges, filter]);
|
|
34
|
+
const log = (0, _sharedUi.useChangeLog)(_jotaiStateStore.jotaiStateStore, {
|
|
35
|
+
filterItems: filterChanges
|
|
36
|
+
});
|
|
37
|
+
const atoms = (0, _sharedUi.useSubscribedValue)(readAtoms, subscribeToAtoms);
|
|
46
38
|
const stats = (0, _react.useMemo)(() => {
|
|
47
|
-
const total =
|
|
48
|
-
const withChanges =
|
|
39
|
+
const total = log.items.length;
|
|
40
|
+
const withChanges = log.items.filter(c => c.hasValueChange).length;
|
|
49
41
|
return {
|
|
50
42
|
totalChanges: total,
|
|
51
43
|
changesWithValueChange: withChanges,
|
|
52
44
|
changesWithoutValueChange: total - withChanges,
|
|
53
45
|
atomCount: atoms.length
|
|
54
46
|
};
|
|
55
|
-
}, [
|
|
47
|
+
}, [log.items, atoms]);
|
|
56
48
|
const atomLabels = (0, _react.useMemo)(() => {
|
|
57
49
|
return _jotaiStateStore.jotaiStateStore.getUniqueAtomLabels();
|
|
58
50
|
}, [atoms]);
|
|
59
|
-
const clearChanges = (0, _react.useCallback)(() => {
|
|
60
|
-
_jotaiStateStore.jotaiStateStore.clearAtomChanges();
|
|
61
|
-
}, []);
|
|
62
|
-
const toggleCapture = (0, _react.useCallback)(() => {
|
|
63
|
-
const newEnabled = !isEnabled;
|
|
64
|
-
_jotaiStateStore.jotaiStateStore.setEnabled(newEnabled);
|
|
65
|
-
setIsEnabled(newEnabled);
|
|
66
|
-
}, [isEnabled]);
|
|
67
51
|
const getChangeById = (0, _react.useCallback)(id => {
|
|
68
52
|
return _jotaiStateStore.jotaiStateStore.getAtomChangeById(id);
|
|
69
53
|
}, []);
|
|
70
54
|
return {
|
|
71
|
-
atomChanges,
|
|
72
|
-
filteredChanges,
|
|
73
|
-
filter,
|
|
74
|
-
setFilter,
|
|
55
|
+
atomChanges: log.items,
|
|
56
|
+
filteredChanges: log.filteredItems,
|
|
57
|
+
filter: log.filter,
|
|
58
|
+
setFilter: log.setFilter,
|
|
75
59
|
stats,
|
|
76
60
|
atoms,
|
|
77
|
-
clearChanges,
|
|
78
|
-
isEnabled,
|
|
79
|
-
toggleCapture,
|
|
61
|
+
clearChanges: log.clear,
|
|
62
|
+
isEnabled: log.isEnabled,
|
|
63
|
+
toggleCapture: log.toggleCapture,
|
|
80
64
|
atomLabels,
|
|
81
65
|
getChangeById
|
|
82
66
|
};
|
|
@@ -11,36 +11,21 @@ var _sharedUi = require("@buoy-gg/shared-ui");
|
|
|
11
11
|
* Same hazard as redux/zustand: up to 200 changes × 2 values on every
|
|
12
12
|
* snapshot. Dashboards fetch values via `getChangeDetail`.
|
|
13
13
|
*/
|
|
14
|
-
const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE =
|
|
15
|
-
__buoyValueOnDevice: true,
|
|
16
|
-
note: "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue."
|
|
17
|
-
});
|
|
18
|
-
const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
|
|
19
|
-
const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
|
|
14
|
+
const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE = (0, _sharedUi.onDeviceMarker)("Value", "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue.");
|
|
20
15
|
/**
|
|
21
16
|
* Per-change wire cache — the store prepends and never mutates a recorded
|
|
22
17
|
* change, so identity is a correct, self-invalidating key.
|
|
23
18
|
*/
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
prevValue: VALUE_ON_DEVICE,
|
|
31
|
-
nextValue: VALUE_ON_DEVICE
|
|
32
|
-
};
|
|
33
|
-
changeCache.set(change, wire);
|
|
34
|
-
return wire;
|
|
35
|
-
}
|
|
36
|
-
function toWireValue(value) {
|
|
37
|
-
return (0, _sharedUi.isOverWireBudget)(value, MAX_WIRE_PAYLOAD_BYTES) ? VALUE_ON_DEVICE : value;
|
|
38
|
-
}
|
|
19
|
+
const toWireChange = (0, _sharedUi.memoByIdentity)(change => ({
|
|
20
|
+
...change,
|
|
21
|
+
prevValue: VALUE_ON_DEVICE,
|
|
22
|
+
nextValue: VALUE_ON_DEVICE
|
|
23
|
+
}));
|
|
24
|
+
const toWireValue = value => (0, _sharedUi.toWire)(value, VALUE_ON_DEVICE);
|
|
39
25
|
|
|
40
26
|
/**
|
|
41
27
|
* Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
|
|
42
|
-
* `useExternalSync
|
|
43
|
-
* this package doesn't need a dependency on it).
|
|
28
|
+
* `useExternalSync`.
|
|
44
29
|
*
|
|
45
30
|
* Atom changes are captured by watchAtoms(), independent of whether a
|
|
46
31
|
* dashboard is watching — subscribing here only streams what the store
|
|
@@ -52,7 +37,7 @@ function toWireValue(value) {
|
|
|
52
37
|
* Dashboards fetch values via `getChangeDetail`. Oversized / unserializable
|
|
53
38
|
* `atoms[].currentValue` uses the same marker and is fetched via `getAtomValue`.
|
|
54
39
|
*/
|
|
55
|
-
const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
40
|
+
const jotaiSyncAdapter = exports.jotaiSyncAdapter = (0, _sharedUi.createSyncAdapter)({
|
|
56
41
|
version: 2,
|
|
57
42
|
getSnapshot: () => ({
|
|
58
43
|
changes: _jotaiStateStore.jotaiStateStore.getAtomChanges().map(toWireChange),
|
|
@@ -61,14 +46,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
|
61
46
|
currentValue: toWireValue(atom.currentValue)
|
|
62
47
|
}))
|
|
63
48
|
}),
|
|
64
|
-
subscribe: onChange =>
|
|
65
|
-
const unsubscribeChanges = _jotaiStateStore.jotaiStateStore.subscribe(onChange);
|
|
66
|
-
const unsubscribeAtoms = _jotaiStateStore.jotaiStateStore.subscribeToAtoms(onChange);
|
|
67
|
-
return () => {
|
|
68
|
-
unsubscribeChanges();
|
|
69
|
-
unsubscribeAtoms();
|
|
70
|
-
};
|
|
71
|
-
},
|
|
49
|
+
subscribe: [onChange => _jotaiStateStore.jotaiStateStore.subscribe(onChange), onChange => _jotaiStateStore.jotaiStateStore.subscribeToAtoms(onChange)],
|
|
72
50
|
actions: {
|
|
73
51
|
clearEvents: () => {
|
|
74
52
|
_jotaiStateStore.jotaiStateStore.clearAtomChanges();
|
|
@@ -78,31 +56,10 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
|
78
56
|
* The per-snapshot stream stays value-free; this is the explicit,
|
|
79
57
|
* size-guarded channel for the detail pane.
|
|
80
58
|
*/
|
|
81
|
-
getChangeDetail:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!id) return {
|
|
86
|
-
found: false,
|
|
87
|
-
reason: "missing id"
|
|
88
|
-
};
|
|
89
|
-
const change = _jotaiStateStore.jotaiStateStore.getAtomChangeById(id);
|
|
90
|
-
if (!change) return {
|
|
91
|
-
found: false,
|
|
92
|
-
reason: "unknown id"
|
|
93
|
-
};
|
|
94
|
-
const cappedNote = {
|
|
95
|
-
__buoyTruncated: true,
|
|
96
|
-
note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
97
|
-
};
|
|
98
|
-
const cap = value => (0, _sharedUi.isOverWireBudget)(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
|
|
99
|
-
return {
|
|
100
|
-
found: true,
|
|
101
|
-
id,
|
|
102
|
-
prevValue: cap(change.prevValue),
|
|
103
|
-
nextValue: cap(change.nextValue)
|
|
104
|
-
};
|
|
105
|
-
},
|
|
59
|
+
getChangeDetail: (0, _sharedUi.detailByIdAction)(id => _jotaiStateStore.jotaiStateStore.getAtomChangeById(id), (change, cap) => ({
|
|
60
|
+
prevValue: cap(change.prevValue),
|
|
61
|
+
nextValue: cap(change.nextValue)
|
|
62
|
+
})),
|
|
106
63
|
/**
|
|
107
64
|
* On-demand current value for one atom. Wire-form snapshots replace
|
|
108
65
|
* oversized / unserializable `currentValue` with VALUE_ON_DEVICE.
|
|
@@ -110,7 +67,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
|
110
67
|
getAtomValue: params => {
|
|
111
68
|
const {
|
|
112
69
|
label
|
|
113
|
-
} = params
|
|
70
|
+
} = (0, _sharedUi.readParams)(params);
|
|
114
71
|
if (!label) return {
|
|
115
72
|
found: false,
|
|
116
73
|
reason: "missing label"
|
|
@@ -126,10 +83,6 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
|
126
83
|
} catch {
|
|
127
84
|
currentValue = undefined;
|
|
128
85
|
}
|
|
129
|
-
const cappedNote = {
|
|
130
|
-
__buoyTruncated: true,
|
|
131
|
-
note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
|
|
132
|
-
};
|
|
133
86
|
try {
|
|
134
87
|
JSON.stringify(currentValue);
|
|
135
88
|
} catch {
|
|
@@ -142,7 +95,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
|
142
95
|
return {
|
|
143
96
|
found: true,
|
|
144
97
|
label,
|
|
145
|
-
currentValue: (0, _sharedUi.
|
|
98
|
+
currentValue: (0, _sharedUi.capDetail)(currentValue, _sharedUi.WIRE_DETAIL_LIMIT_BYTES)
|
|
146
99
|
};
|
|
147
100
|
},
|
|
148
101
|
/**
|
|
@@ -155,7 +108,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
|
155
108
|
const {
|
|
156
109
|
includeValues = false,
|
|
157
110
|
limit
|
|
158
|
-
} = params
|
|
111
|
+
} = (0, _sharedUi.readParams)(params);
|
|
159
112
|
const all = _jotaiStateStore.jotaiStateStore.getAtoms();
|
|
160
113
|
const capped = typeof limit === "number" && limit > 0 ? all.slice(0, limit) : all;
|
|
161
114
|
const atoms = capped.map(a => {
|
|
@@ -188,9 +141,9 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
|
188
141
|
const {
|
|
189
142
|
label,
|
|
190
143
|
value
|
|
191
|
-
} = params
|
|
144
|
+
} = (0, _sharedUi.readParams)(params);
|
|
192
145
|
if (!label) throw new Error("setAtom requires a `label`");
|
|
193
146
|
_jotaiStateStore.jotaiStateStore.setAtomValue(label, value);
|
|
194
147
|
}
|
|
195
148
|
}
|
|
196
|
-
};
|
|
149
|
+
});
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.jotaiStateStore = void 0;
|
|
7
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
7
8
|
/**
|
|
8
9
|
* Jotai state store — captures and stores Jotai atom changes
|
|
9
10
|
*
|
|
@@ -40,44 +41,7 @@ const ATOM_COLORS = {
|
|
|
40
41
|
filter: "#84CC16" // lime
|
|
41
42
|
};
|
|
42
43
|
function getAtomColor(label) {
|
|
43
|
-
|
|
44
|
-
if (ATOM_COLORS[lower]) return ATOM_COLORS[lower];
|
|
45
|
-
for (const [key, color] of Object.entries(ATOM_COLORS)) {
|
|
46
|
-
if (lower.includes(key)) return color;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Generate consistent hex from name hash
|
|
50
|
-
const hash = label.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
51
|
-
const hue = hash * 137 % 360;
|
|
52
|
-
const s = 0.7;
|
|
53
|
-
const l = 0.6;
|
|
54
|
-
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
55
|
-
const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
|
|
56
|
-
const m = l - c / 2;
|
|
57
|
-
let r = 0,
|
|
58
|
-
g = 0,
|
|
59
|
-
b = 0;
|
|
60
|
-
if (hue < 60) {
|
|
61
|
-
r = c;
|
|
62
|
-
g = x;
|
|
63
|
-
} else if (hue < 120) {
|
|
64
|
-
r = x;
|
|
65
|
-
g = c;
|
|
66
|
-
} else if (hue < 180) {
|
|
67
|
-
g = c;
|
|
68
|
-
b = x;
|
|
69
|
-
} else if (hue < 240) {
|
|
70
|
-
g = x;
|
|
71
|
-
b = c;
|
|
72
|
-
} else if (hue < 300) {
|
|
73
|
-
r = x;
|
|
74
|
-
b = c;
|
|
75
|
-
} else {
|
|
76
|
-
r = c;
|
|
77
|
-
b = x;
|
|
78
|
-
}
|
|
79
|
-
const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0");
|
|
80
|
-
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
44
|
+
return (0, _sharedUi.entityColor)(label, ATOM_COLORS);
|
|
81
45
|
}
|
|
82
46
|
|
|
83
47
|
// ============================================
|
|
@@ -85,97 +49,25 @@ function getAtomColor(label) {
|
|
|
85
49
|
// ============================================
|
|
86
50
|
|
|
87
51
|
function formatValuePreview(value, maxLength = 40) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (typeof value === "string") {
|
|
93
|
-
return value.length > maxLength ? `"${value.slice(0, maxLength - 3)}..."` : `"${value}"`;
|
|
94
|
-
}
|
|
95
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
96
|
-
return String(value);
|
|
97
|
-
}
|
|
98
|
-
if (Array.isArray(value)) {
|
|
99
|
-
if (value.length === 0) return "[]";
|
|
100
|
-
const preview = JSON.stringify(value);
|
|
101
|
-
return preview.length > maxLength ? `[${value.length} items]` : preview;
|
|
102
|
-
}
|
|
103
|
-
if (typeof value === "object") {
|
|
104
|
-
const keys = Object.keys(value);
|
|
105
|
-
if (keys.length === 0) return "{}";
|
|
106
|
-
const preview = JSON.stringify(value);
|
|
107
|
-
if (preview.length <= maxLength) return preview;
|
|
108
|
-
return `{ ${keys.length} keys }`;
|
|
109
|
-
}
|
|
110
|
-
return String(value).slice(0, maxLength);
|
|
111
|
-
} catch {
|
|
112
|
-
return "[complex]";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function getValueDiffSummary(prevValue, nextValue) {
|
|
116
|
-
if (prevValue === nextValue) {
|
|
117
|
-
return {
|
|
118
|
-
summary: "no change",
|
|
119
|
-
changedKeys: [],
|
|
120
|
-
changedCount: 0
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
if (typeof prevValue !== "object" || typeof nextValue !== "object" || prevValue === null || nextValue === null) {
|
|
124
|
-
return {
|
|
125
|
-
summary: "changed",
|
|
126
|
-
changedKeys: [],
|
|
127
|
-
changedCount: 0
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
const prevKeys = Object.keys(prevValue);
|
|
131
|
-
const nextKeys = Object.keys(nextValue);
|
|
132
|
-
const added = nextKeys.filter(k => !prevKeys.includes(k));
|
|
133
|
-
const removed = prevKeys.filter(k => !nextKeys.includes(k));
|
|
134
|
-
const changed = [];
|
|
135
|
-
for (const key of prevKeys) {
|
|
136
|
-
if (nextKeys.includes(key) && prevValue[key] !== nextValue[key]) {
|
|
137
|
-
changed.push(key);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
const allChangedKeys = [...added, ...removed, ...changed];
|
|
141
|
-
const parts = [];
|
|
142
|
-
if (added.length > 0) parts.push(`+${added.length}`);
|
|
143
|
-
if (removed.length > 0) parts.push(`-${removed.length}`);
|
|
144
|
-
if (changed.length > 0) parts.push(`~${changed.length}`);
|
|
145
|
-
const total = allChangedKeys.length;
|
|
146
|
-
if (parts.length === 0) {
|
|
147
|
-
return {
|
|
148
|
-
summary: "nested change",
|
|
149
|
-
changedKeys: [],
|
|
150
|
-
changedCount: 0
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
summary: `${parts.join(" ")} ${total === 1 ? "key" : "keys"}`,
|
|
155
|
-
changedKeys: allChangedKeys,
|
|
156
|
-
changedCount: total
|
|
157
|
-
};
|
|
52
|
+
return (0, _sharedUi.formatValuePreview)(value, {
|
|
53
|
+
maxLength,
|
|
54
|
+
functionLabel: "(fn)"
|
|
55
|
+
});
|
|
158
56
|
}
|
|
57
|
+
const getValueDiffSummary = _sharedUi.shallowDiffSummary;
|
|
159
58
|
|
|
160
59
|
// ============================================
|
|
161
60
|
// Jotai State Store
|
|
162
61
|
// ============================================
|
|
163
62
|
|
|
164
|
-
class JotaiStateStore {
|
|
165
|
-
atomChanges = [];
|
|
63
|
+
class JotaiStateStore extends _sharedUi.ChangeLog {
|
|
166
64
|
atoms = new Map();
|
|
167
|
-
listeners = new Set();
|
|
168
65
|
atomListeners = new Set();
|
|
169
|
-
clearListeners = new Set();
|
|
170
|
-
maxChanges = 200;
|
|
171
|
-
idCounter = 0;
|
|
172
|
-
isEnabled = true;
|
|
173
|
-
captureSuppressed = false;
|
|
174
66
|
|
|
175
67
|
// ---- Change Tracking ----
|
|
176
68
|
|
|
177
69
|
addAtomChange(params) {
|
|
178
|
-
if (!this.
|
|
70
|
+
if (!this.canRecord()) return;
|
|
179
71
|
const {
|
|
180
72
|
atomLabel,
|
|
181
73
|
prevValue,
|
|
@@ -190,7 +82,7 @@ class JotaiStateStore {
|
|
|
190
82
|
} = getValueDiffSummary(prevValue, nextValue);
|
|
191
83
|
const valuePreview = formatValuePreview(nextValue);
|
|
192
84
|
const change = {
|
|
193
|
-
id:
|
|
85
|
+
id: this.nextId(),
|
|
194
86
|
atomLabel,
|
|
195
87
|
timestamp: Date.now(),
|
|
196
88
|
prevValue,
|
|
@@ -203,7 +95,7 @@ class JotaiStateStore {
|
|
|
203
95
|
valuePreview,
|
|
204
96
|
isSlowUpdate: false
|
|
205
97
|
};
|
|
206
|
-
this.
|
|
98
|
+
this.push(change);
|
|
207
99
|
const atomInfo = this.atoms.get(atomLabel);
|
|
208
100
|
if (atomInfo) {
|
|
209
101
|
atomInfo.changeCount++;
|
|
@@ -212,54 +104,21 @@ class JotaiStateStore {
|
|
|
212
104
|
this.notifyAtomListeners();
|
|
213
105
|
}
|
|
214
106
|
getAtomChanges() {
|
|
215
|
-
return
|
|
107
|
+
return this.getAll();
|
|
216
108
|
}
|
|
217
109
|
getAtomChangeById(id) {
|
|
218
|
-
return this.
|
|
110
|
+
return this.getById(id);
|
|
219
111
|
}
|
|
220
112
|
clearAtomChanges() {
|
|
221
|
-
this.atomChanges = [];
|
|
222
113
|
for (const atom of this.atoms.values()) {
|
|
223
114
|
atom.changeCount = 0;
|
|
224
115
|
}
|
|
225
|
-
this.
|
|
116
|
+
this.clear();
|
|
226
117
|
this.notifyAtomListeners();
|
|
227
|
-
this.clearListeners.forEach(listener => {
|
|
228
|
-
try {
|
|
229
|
-
listener();
|
|
230
|
-
} catch {
|
|
231
|
-
// Ignore listener errors
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Listen for clearAtomChanges() calls. Used in remote mirror mode to
|
|
238
|
-
* forward a clear performed in the dashboard UI to the synced device.
|
|
239
|
-
*/
|
|
240
|
-
onClear(listener) {
|
|
241
|
-
this.clearListeners.add(listener);
|
|
242
|
-
return () => {
|
|
243
|
-
this.clearListeners.delete(listener);
|
|
244
|
-
};
|
|
245
118
|
}
|
|
246
119
|
|
|
247
120
|
// ---- Remote Mirror Mode ----
|
|
248
121
|
|
|
249
|
-
/**
|
|
250
|
-
* Permanently suppress local capture. Use when this store acts as a mirror
|
|
251
|
-
* of a remote device's atom changes (e.g. the desktop dashboard): data
|
|
252
|
-
* arrives via replaceFromSnapshot() and local watchers must never record.
|
|
253
|
-
*/
|
|
254
|
-
disableCapture() {
|
|
255
|
-
this.captureSuppressed = true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Whether the store is in remote mirror mode (capture suppressed). */
|
|
259
|
-
isCaptureSuppressed() {
|
|
260
|
-
return this.captureSuppressed;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
122
|
/**
|
|
264
123
|
* Serializable snapshot of the tracked atoms (registry metadata plus each
|
|
265
124
|
* atom's current value). Used by the sync adapter on the device side —
|
|
@@ -290,18 +149,18 @@ class JotaiStateStore {
|
|
|
290
149
|
* freezes the mirror.
|
|
291
150
|
*/
|
|
292
151
|
replaceFromSnapshot(changes, atomSnapshots) {
|
|
293
|
-
if (!this.
|
|
152
|
+
if (!this.getEnabled()) return;
|
|
294
153
|
|
|
295
154
|
// Preserve values a remote mirror already hydrated — wire-form snapshots
|
|
296
155
|
// always carry markers, and a later snapshot must not blank the detail pane.
|
|
297
156
|
const hydrated = new Map();
|
|
298
|
-
for (const existing of this.
|
|
157
|
+
for (const existing of this.items) {
|
|
299
158
|
const prev = existing.prevValue;
|
|
300
159
|
if (!prev || prev.__buoyValueOnDevice !== true) {
|
|
301
160
|
hydrated.set(existing.id, existing);
|
|
302
161
|
}
|
|
303
162
|
}
|
|
304
|
-
|
|
163
|
+
const merged = changes.slice(0, this.max).map(change => {
|
|
305
164
|
const kept = hydrated.get(change.id);
|
|
306
165
|
const incoming = change.prevValue;
|
|
307
166
|
return kept && incoming?.__buoyValueOnDevice === true ? {
|
|
@@ -339,7 +198,7 @@ class JotaiStateStore {
|
|
|
339
198
|
writeKind: snapshot.writeKind
|
|
340
199
|
}];
|
|
341
200
|
}));
|
|
342
|
-
this.
|
|
201
|
+
this.setItems(merged);
|
|
343
202
|
this.notifyAtomListeners();
|
|
344
203
|
}
|
|
345
204
|
|
|
@@ -348,13 +207,7 @@ class JotaiStateStore {
|
|
|
348
207
|
* device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
|
|
349
208
|
*/
|
|
350
209
|
hydrateChange(id, patch) {
|
|
351
|
-
|
|
352
|
-
if (index === -1) return;
|
|
353
|
-
this.atomChanges = [...this.atomChanges.slice(0, index), {
|
|
354
|
-
...this.atomChanges[index],
|
|
355
|
-
...patch
|
|
356
|
-
}, ...this.atomChanges.slice(index + 1)];
|
|
357
|
-
this.notifyListeners();
|
|
210
|
+
this.hydrateById(id, patch);
|
|
358
211
|
}
|
|
359
212
|
|
|
360
213
|
/**
|
|
@@ -418,7 +271,7 @@ class JotaiStateStore {
|
|
|
418
271
|
// ---- Filtering ----
|
|
419
272
|
|
|
420
273
|
filterAtomChanges(filter) {
|
|
421
|
-
let filtered =
|
|
274
|
+
let filtered = this.getAll();
|
|
422
275
|
if (filter.searchText) {
|
|
423
276
|
const search = filter.searchText.toLowerCase();
|
|
424
277
|
filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
|
|
@@ -435,8 +288,8 @@ class JotaiStateStore {
|
|
|
435
288
|
// ---- Stats ----
|
|
436
289
|
|
|
437
290
|
getStats() {
|
|
438
|
-
const total = this.
|
|
439
|
-
const withChanges = this.
|
|
291
|
+
const total = this.items.length;
|
|
292
|
+
const withChanges = this.items.filter(c => c.hasValueChange).length;
|
|
440
293
|
return {
|
|
441
294
|
totalChanges: total,
|
|
442
295
|
changesWithValueChange: withChanges,
|
|
@@ -450,34 +303,16 @@ class JotaiStateStore {
|
|
|
450
303
|
|
|
451
304
|
// ---- Enable / Disable ----
|
|
452
305
|
|
|
453
|
-
setEnabled(enabled) {
|
|
454
|
-
this.isEnabled = enabled;
|
|
455
|
-
}
|
|
456
|
-
getEnabled() {
|
|
457
|
-
return this.isEnabled;
|
|
458
|
-
}
|
|
459
306
|
setMaxChanges(max) {
|
|
460
|
-
this.
|
|
461
|
-
if (this.atomChanges.length > max) {
|
|
462
|
-
this.atomChanges = this.atomChanges.slice(0, max);
|
|
463
|
-
this.notifyListeners();
|
|
464
|
-
}
|
|
307
|
+
this.setMax(max);
|
|
465
308
|
}
|
|
466
309
|
|
|
467
310
|
// ---- Subscriptions ----
|
|
468
311
|
|
|
469
|
-
subscribe(listener) {
|
|
470
|
-
this.listeners.add(listener);
|
|
471
|
-
return () => this.listeners.delete(listener);
|
|
472
|
-
}
|
|
473
312
|
subscribeToAtoms(listener) {
|
|
474
313
|
this.atomListeners.add(listener);
|
|
475
314
|
return () => this.atomListeners.delete(listener);
|
|
476
315
|
}
|
|
477
|
-
notifyListeners() {
|
|
478
|
-
const changes = this.getAtomChanges();
|
|
479
|
-
this.listeners.forEach(listener => listener(changes));
|
|
480
|
-
}
|
|
481
316
|
notifyAtomListeners() {
|
|
482
317
|
const atoms = this.getAtoms();
|
|
483
318
|
this.atomListeners.forEach(listener => listener(atoms));
|
|
@@ -65,30 +65,14 @@ async function capture() {
|
|
|
65
65
|
}
|
|
66
66
|
async function restore(data, options) {
|
|
67
67
|
const snapshot = data;
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
|
|
71
|
-
let excludedHits = 0;
|
|
72
|
-
const result = {
|
|
73
|
-
ok: true,
|
|
74
|
-
applied: 0,
|
|
75
|
-
skipped: [],
|
|
76
|
-
warnings: []
|
|
77
|
-
};
|
|
68
|
+
const scope = (0, _sharedUi.createRestoreScope)(options);
|
|
69
|
+
const result = (0, _sharedUi.emptyRestoreResult)();
|
|
78
70
|
if (!snapshot?.atoms) {
|
|
79
|
-
return
|
|
80
|
-
ok: false,
|
|
81
|
-
applied: 0,
|
|
82
|
-
skipped: [],
|
|
83
|
-
warnings: ["empty jotai snapshot"]
|
|
84
|
-
};
|
|
71
|
+
return (0, _sharedUi.failedRestore)("empty jotai snapshot");
|
|
85
72
|
}
|
|
86
73
|
let derived = 0;
|
|
87
74
|
for (const [label, entry] of Object.entries(snapshot.atoms)) {
|
|
88
|
-
if (!
|
|
89
|
-
excludedHits += 1;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
75
|
+
if (!scope.allows(label)) continue;
|
|
92
76
|
if (!entry.writable) {
|
|
93
77
|
derived += 1;
|
|
94
78
|
continue;
|
|
@@ -122,9 +106,8 @@ async function restore(data, options) {
|
|
|
122
106
|
if (derived > 0) {
|
|
123
107
|
result.warnings.push(`${derived} derived atom(s) left to recompute`);
|
|
124
108
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
109
|
+
const excludedWarning = scope.excludedWarning("atom(s)");
|
|
110
|
+
if (excludedWarning) result.warnings.push(excludedWarning);
|
|
128
111
|
return result;
|
|
129
112
|
}
|
|
130
113
|
|
|
@@ -149,8 +132,7 @@ async function previewItems(target, base) {
|
|
|
149
132
|
}
|
|
150
133
|
const items = [];
|
|
151
134
|
const notes = [];
|
|
152
|
-
const
|
|
153
|
-
for (const label of labels) {
|
|
135
|
+
for (const label of (0, _sharedUi.unionKeys)(baseAtoms, targetAtoms)) {
|
|
154
136
|
const inTarget = label in targetAtoms;
|
|
155
137
|
if (!inTarget) {
|
|
156
138
|
if (liveMode) notes.push(`atom "${label}" not in snapshot — untouched`);else items.push({
|