@buoy-gg/jotai 3.0.1 → 4.0.1
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/index.js +98 -1
- package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +300 -1
- package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +113 -1
- package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +754 -1
- package/lib/commonjs/jotai/components/JotaiEventFilterView.js +305 -1
- package/lib/commonjs/jotai/components/JotaiIcon.js +35 -1
- package/lib/commonjs/jotai/components/JotaiModal.js +567 -1
- package/lib/commonjs/jotai/components/index.js +59 -1
- package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +83 -1
- package/lib/commonjs/jotai/index.js +85 -1
- package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +38 -0
- package/lib/commonjs/jotai/utils/jotaiStateStore.js +399 -1
- package/lib/commonjs/jotai/utils/watchAtoms.js +149 -1
- package/lib/commonjs/preset.js +98 -1
- package/lib/module/index.js +79 -1
- package/lib/module/jotai/components/JotaiAtomBrowser.js +296 -1
- package/lib/module/jotai/components/JotaiAtomChangeItem.js +109 -1
- package/lib/module/jotai/components/JotaiAtomDetailContent.js +748 -1
- package/lib/module/jotai/components/JotaiEventFilterView.js +301 -1
- package/lib/module/jotai/components/JotaiIcon.js +31 -1
- package/lib/module/jotai/components/JotaiModal.js +563 -1
- package/lib/module/jotai/components/index.js +8 -1
- package/lib/module/jotai/hooks/useJotaiAtomChanges.js +79 -1
- package/lib/module/jotai/index.js +10 -1
- package/lib/module/jotai/sync/jotaiSyncAdapter.js +35 -0
- package/lib/module/jotai/utils/jotaiStateStore.js +395 -1
- package/lib/module/jotai/utils/watchAtoms.js +144 -1
- package/lib/module/preset.js +94 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -0
- package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -0
- package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -0
- package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -0
- package/lib/typescript/jotai/components/JotaiIcon.d.ts.map +1 -0
- package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -0
- package/lib/typescript/jotai/components/index.d.ts.map +1 -0
- package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +1 -0
- package/lib/typescript/jotai/index.d.ts.map +1 -0
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +23 -0
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -0
- package/lib/typescript/jotai/types/index.d.ts +11 -0
- package/lib/typescript/jotai/types/index.d.ts.map +1 -0
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +29 -1
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -0
- package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/package.json +3 -3
|
@@ -1 +1,83 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useJotaiAtomChanges = useJotaiAtomChanges;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _jotaiStateStore = require("../utils/jotaiStateStore");
|
|
9
|
+
/**
|
|
10
|
+
* Hook for consuming Jotai atom changes from the store
|
|
11
|
+
*
|
|
12
|
+
* Mirrors useZustandStateChanges.ts from @buoy-gg/zustand
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function useJotaiAtomChanges() {
|
|
16
|
+
const [atomChanges, setAtomChanges] = (0, _react.useState)(() => _jotaiStateStore.jotaiStateStore.getAtomChanges());
|
|
17
|
+
const [atoms, setAtoms] = (0, _react.useState)(() => _jotaiStateStore.jotaiStateStore.getAtoms());
|
|
18
|
+
const [filter, setFilter] = (0, _react.useState)({});
|
|
19
|
+
const [isEnabled, setIsEnabled] = (0, _react.useState)(() => _jotaiStateStore.jotaiStateStore.getEnabled());
|
|
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]);
|
|
46
|
+
const stats = (0, _react.useMemo)(() => {
|
|
47
|
+
const total = atomChanges.length;
|
|
48
|
+
const withChanges = atomChanges.filter(c => c.hasValueChange).length;
|
|
49
|
+
return {
|
|
50
|
+
totalChanges: total,
|
|
51
|
+
changesWithValueChange: withChanges,
|
|
52
|
+
changesWithoutValueChange: total - withChanges,
|
|
53
|
+
atomCount: atoms.length
|
|
54
|
+
};
|
|
55
|
+
}, [atomChanges, atoms]);
|
|
56
|
+
const atomLabels = (0, _react.useMemo)(() => {
|
|
57
|
+
return _jotaiStateStore.jotaiStateStore.getUniqueAtomLabels();
|
|
58
|
+
}, [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
|
+
const getChangeById = (0, _react.useCallback)(id => {
|
|
68
|
+
return _jotaiStateStore.jotaiStateStore.getAtomChangeById(id);
|
|
69
|
+
}, []);
|
|
70
|
+
return {
|
|
71
|
+
atomChanges,
|
|
72
|
+
filteredChanges,
|
|
73
|
+
filter,
|
|
74
|
+
setFilter,
|
|
75
|
+
stats,
|
|
76
|
+
atoms,
|
|
77
|
+
clearChanges,
|
|
78
|
+
isEnabled,
|
|
79
|
+
toggleCapture,
|
|
80
|
+
atomLabels,
|
|
81
|
+
getChangeById
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -1 +1,85 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "JOTAI_ICON_COLOR", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _JotaiIcon.JOTAI_ICON_COLOR;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "JotaiAtomBrowser", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _JotaiAtomBrowser.JotaiAtomBrowser;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "JotaiAtomChangeItem", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _JotaiAtomChangeItem.JotaiAtomChangeItem;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "JotaiAtomDetailContent", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () {
|
|
27
|
+
return _JotaiAtomDetailContent.JotaiAtomDetailContent;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(exports, "JotaiAtomDetailFooter", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () {
|
|
33
|
+
return _JotaiAtomDetailContent.JotaiAtomDetailFooter;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(exports, "JotaiIcon", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: function () {
|
|
39
|
+
return _JotaiIcon.JotaiIcon;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(exports, "JotaiModal", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
get: function () {
|
|
45
|
+
return _JotaiModal.JotaiModal;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(exports, "isAtomWatched", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
get: function () {
|
|
51
|
+
return _watchAtoms.isAtomWatched;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
Object.defineProperty(exports, "jotaiStateStore", {
|
|
55
|
+
enumerable: true,
|
|
56
|
+
get: function () {
|
|
57
|
+
return _jotaiStateStore.jotaiStateStore;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
Object.defineProperty(exports, "useJotaiAtomChanges", {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get: function () {
|
|
63
|
+
return _useJotaiAtomChanges.useJotaiAtomChanges;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
Object.defineProperty(exports, "watchAtoms", {
|
|
67
|
+
enumerable: true,
|
|
68
|
+
get: function () {
|
|
69
|
+
return _watchAtoms.watchAtoms;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
Object.defineProperty(exports, "watchDefaultStoreAtoms", {
|
|
73
|
+
enumerable: true,
|
|
74
|
+
get: function () {
|
|
75
|
+
return _watchAtoms.watchDefaultStoreAtoms;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
var _JotaiModal = require("./components/JotaiModal");
|
|
79
|
+
var _JotaiIcon = require("./components/JotaiIcon");
|
|
80
|
+
var _JotaiAtomChangeItem = require("./components/JotaiAtomChangeItem");
|
|
81
|
+
var _JotaiAtomDetailContent = require("./components/JotaiAtomDetailContent");
|
|
82
|
+
var _JotaiAtomBrowser = require("./components/JotaiAtomBrowser");
|
|
83
|
+
var _jotaiStateStore = require("./utils/jotaiStateStore");
|
|
84
|
+
var _watchAtoms = require("./utils/watchAtoms");
|
|
85
|
+
var _useJotaiAtomChanges = require("./hooks/useJotaiAtomChanges");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.jotaiSyncAdapter = void 0;
|
|
7
|
+
var _jotaiStateStore = require("../utils/jotaiStateStore");
|
|
8
|
+
/**
|
|
9
|
+
* Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
|
|
10
|
+
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
11
|
+
* this package doesn't need a dependency on it).
|
|
12
|
+
*
|
|
13
|
+
* Atom changes are captured by watchAtoms(), independent of whether a
|
|
14
|
+
* dashboard is watching — subscribing here only streams what the store
|
|
15
|
+
* already records. The snapshot carries both the change timeline and the
|
|
16
|
+
* atom registry (with each atom's current value) so the dashboard can render
|
|
17
|
+
* the atom browser.
|
|
18
|
+
*/
|
|
19
|
+
const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
|
|
20
|
+
version: 1,
|
|
21
|
+
getSnapshot: () => ({
|
|
22
|
+
changes: _jotaiStateStore.jotaiStateStore.getAtomChanges(),
|
|
23
|
+
atoms: _jotaiStateStore.jotaiStateStore.getAtomSnapshots()
|
|
24
|
+
}),
|
|
25
|
+
subscribe: onChange => {
|
|
26
|
+
const unsubscribeChanges = _jotaiStateStore.jotaiStateStore.subscribe(onChange);
|
|
27
|
+
const unsubscribeAtoms = _jotaiStateStore.jotaiStateStore.subscribeToAtoms(onChange);
|
|
28
|
+
return () => {
|
|
29
|
+
unsubscribeChanges();
|
|
30
|
+
unsubscribeAtoms();
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
actions: {
|
|
34
|
+
clearEvents: () => {
|
|
35
|
+
_jotaiStateStore.jotaiStateStore.clearAtomChanges();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -1 +1,399 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.jotaiStateStore = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Jotai state store — captures and stores Jotai atom changes
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the architecture of zustandStateStore.ts from @buoy-gg/zustand
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ============================================
|
|
14
|
+
// Atom Color Palette
|
|
15
|
+
// ============================================
|
|
16
|
+
|
|
17
|
+
const ATOM_COLORS = {
|
|
18
|
+
count: "#10B981",
|
|
19
|
+
// emerald
|
|
20
|
+
auth: "#8B5CF6",
|
|
21
|
+
// purple
|
|
22
|
+
user: "#3B82F6",
|
|
23
|
+
// blue
|
|
24
|
+
cart: "#EC4899",
|
|
25
|
+
// pink
|
|
26
|
+
app: "#6366F1",
|
|
27
|
+
// indigo
|
|
28
|
+
ui: "#F59E0B",
|
|
29
|
+
// amber
|
|
30
|
+
settings: "#14B8A6",
|
|
31
|
+
// teal
|
|
32
|
+
theme: "#06B6D4",
|
|
33
|
+
// cyan
|
|
34
|
+
nav: "#F97316",
|
|
35
|
+
// orange
|
|
36
|
+
form: "#EF4444",
|
|
37
|
+
// red
|
|
38
|
+
modal: "#A855F7",
|
|
39
|
+
// violet
|
|
40
|
+
filter: "#84CC16" // lime
|
|
41
|
+
};
|
|
42
|
+
function getAtomColor(label) {
|
|
43
|
+
const lower = label.toLowerCase();
|
|
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)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================
|
|
84
|
+
// Helper Functions
|
|
85
|
+
// ============================================
|
|
86
|
+
|
|
87
|
+
function formatValuePreview(value, maxLength = 40) {
|
|
88
|
+
if (value === undefined) return "undefined";
|
|
89
|
+
if (value === null) return "null";
|
|
90
|
+
try {
|
|
91
|
+
if (typeof value === "function") return "(fn)";
|
|
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
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================
|
|
161
|
+
// Jotai State Store
|
|
162
|
+
// ============================================
|
|
163
|
+
|
|
164
|
+
class JotaiStateStore {
|
|
165
|
+
atomChanges = [];
|
|
166
|
+
atoms = new Map();
|
|
167
|
+
listeners = new Set();
|
|
168
|
+
atomListeners = new Set();
|
|
169
|
+
clearListeners = new Set();
|
|
170
|
+
maxChanges = 200;
|
|
171
|
+
idCounter = 0;
|
|
172
|
+
isEnabled = true;
|
|
173
|
+
captureSuppressed = false;
|
|
174
|
+
|
|
175
|
+
// ---- Change Tracking ----
|
|
176
|
+
|
|
177
|
+
addAtomChange(params) {
|
|
178
|
+
if (!this.isEnabled || this.captureSuppressed) return;
|
|
179
|
+
const {
|
|
180
|
+
atomLabel,
|
|
181
|
+
prevValue,
|
|
182
|
+
nextValue,
|
|
183
|
+
category = "write"
|
|
184
|
+
} = params;
|
|
185
|
+
const hasValueChange = prevValue !== nextValue;
|
|
186
|
+
const {
|
|
187
|
+
summary,
|
|
188
|
+
changedKeys,
|
|
189
|
+
changedCount
|
|
190
|
+
} = getValueDiffSummary(prevValue, nextValue);
|
|
191
|
+
const valuePreview = formatValuePreview(nextValue);
|
|
192
|
+
const change = {
|
|
193
|
+
id: `${Date.now()}-${++this.idCounter}`,
|
|
194
|
+
atomLabel,
|
|
195
|
+
timestamp: Date.now(),
|
|
196
|
+
prevValue,
|
|
197
|
+
nextValue,
|
|
198
|
+
hasValueChange,
|
|
199
|
+
category,
|
|
200
|
+
changedKeys,
|
|
201
|
+
changedKeysCount: changedCount,
|
|
202
|
+
diffSummary: summary,
|
|
203
|
+
valuePreview,
|
|
204
|
+
isSlowUpdate: false
|
|
205
|
+
};
|
|
206
|
+
this.atomChanges = [change, ...this.atomChanges].slice(0, this.maxChanges);
|
|
207
|
+
const atomInfo = this.atoms.get(atomLabel);
|
|
208
|
+
if (atomInfo) {
|
|
209
|
+
atomInfo.changeCount++;
|
|
210
|
+
}
|
|
211
|
+
this.notifyListeners();
|
|
212
|
+
this.notifyAtomListeners();
|
|
213
|
+
}
|
|
214
|
+
getAtomChanges() {
|
|
215
|
+
return [...this.atomChanges];
|
|
216
|
+
}
|
|
217
|
+
getAtomChangeById(id) {
|
|
218
|
+
return this.atomChanges.find(c => c.id === id);
|
|
219
|
+
}
|
|
220
|
+
clearAtomChanges() {
|
|
221
|
+
this.atomChanges = [];
|
|
222
|
+
for (const atom of this.atoms.values()) {
|
|
223
|
+
atom.changeCount = 0;
|
|
224
|
+
}
|
|
225
|
+
this.notifyListeners();
|
|
226
|
+
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
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- Remote Mirror Mode ----
|
|
248
|
+
|
|
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
|
+
/**
|
|
264
|
+
* Serializable snapshot of the tracked atoms (registry metadata plus each
|
|
265
|
+
* atom's current value). Used by the sync adapter on the device side —
|
|
266
|
+
* the live `getValue` handles can't go over the wire.
|
|
267
|
+
*/
|
|
268
|
+
getAtomSnapshots() {
|
|
269
|
+
return Array.from(this.atoms.values()).map(atom => {
|
|
270
|
+
let currentValue;
|
|
271
|
+
try {
|
|
272
|
+
currentValue = atom.getValue();
|
|
273
|
+
} catch {
|
|
274
|
+
currentValue = undefined;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
label: atom.label,
|
|
278
|
+
changeCount: atom.changeCount,
|
|
279
|
+
color: atom.color,
|
|
280
|
+
currentValue
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Replace the entire mirror contents from a synced device snapshot. The
|
|
287
|
+
* rebuilt registry entries get stub `getValue` handles serving the
|
|
288
|
+
* snapshotted value. Respects the enabled flag so the UI's pause button
|
|
289
|
+
* freezes the mirror.
|
|
290
|
+
*/
|
|
291
|
+
replaceFromSnapshot(changes, atomSnapshots) {
|
|
292
|
+
if (!this.isEnabled) return;
|
|
293
|
+
this.atomChanges = changes.slice(0, this.maxChanges);
|
|
294
|
+
this.atoms = new Map(atomSnapshots.map(snapshot => [snapshot.label, {
|
|
295
|
+
label: snapshot.label,
|
|
296
|
+
changeCount: snapshot.changeCount,
|
|
297
|
+
color: snapshot.color,
|
|
298
|
+
getValue: () => snapshot.currentValue
|
|
299
|
+
}]));
|
|
300
|
+
this.notifyListeners();
|
|
301
|
+
this.notifyAtomListeners();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---- Atom Registry ----
|
|
305
|
+
|
|
306
|
+
registerAtom(label, getValue) {
|
|
307
|
+
if (this.atoms.has(label)) return;
|
|
308
|
+
const atomInfo = {
|
|
309
|
+
label,
|
|
310
|
+
changeCount: 0,
|
|
311
|
+
color: getAtomColor(label),
|
|
312
|
+
getValue
|
|
313
|
+
};
|
|
314
|
+
this.atoms.set(label, atomInfo);
|
|
315
|
+
this.notifyAtomListeners();
|
|
316
|
+
}
|
|
317
|
+
unregisterAtom(label) {
|
|
318
|
+
this.atoms.delete(label);
|
|
319
|
+
this.notifyAtomListeners();
|
|
320
|
+
}
|
|
321
|
+
getAtoms() {
|
|
322
|
+
return Array.from(this.atoms.values());
|
|
323
|
+
}
|
|
324
|
+
getAtom(label) {
|
|
325
|
+
return this.atoms.get(label);
|
|
326
|
+
}
|
|
327
|
+
getAtomColor(label) {
|
|
328
|
+
return this.atoms.get(label)?.color ?? getAtomColor(label);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---- Filtering ----
|
|
332
|
+
|
|
333
|
+
filterAtomChanges(filter) {
|
|
334
|
+
let filtered = [...this.atomChanges];
|
|
335
|
+
if (filter.searchText) {
|
|
336
|
+
const search = filter.searchText.toLowerCase();
|
|
337
|
+
filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
|
|
338
|
+
}
|
|
339
|
+
if (filter.atomLabels && filter.atomLabels.length > 0) {
|
|
340
|
+
filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
|
|
341
|
+
}
|
|
342
|
+
if (filter.onlyWithChanges) {
|
|
343
|
+
filtered = filtered.filter(c => c.hasValueChange);
|
|
344
|
+
}
|
|
345
|
+
return filtered;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---- Stats ----
|
|
349
|
+
|
|
350
|
+
getStats() {
|
|
351
|
+
const total = this.atomChanges.length;
|
|
352
|
+
const withChanges = this.atomChanges.filter(c => c.hasValueChange).length;
|
|
353
|
+
return {
|
|
354
|
+
totalChanges: total,
|
|
355
|
+
changesWithValueChange: withChanges,
|
|
356
|
+
changesWithoutValueChange: total - withChanges,
|
|
357
|
+
atomCount: this.atoms.size
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
getUniqueAtomLabels() {
|
|
361
|
+
return Array.from(this.atoms.keys()).sort();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---- Enable / Disable ----
|
|
365
|
+
|
|
366
|
+
setEnabled(enabled) {
|
|
367
|
+
this.isEnabled = enabled;
|
|
368
|
+
}
|
|
369
|
+
getEnabled() {
|
|
370
|
+
return this.isEnabled;
|
|
371
|
+
}
|
|
372
|
+
setMaxChanges(max) {
|
|
373
|
+
this.maxChanges = max;
|
|
374
|
+
if (this.atomChanges.length > max) {
|
|
375
|
+
this.atomChanges = this.atomChanges.slice(0, max);
|
|
376
|
+
this.notifyListeners();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ---- Subscriptions ----
|
|
381
|
+
|
|
382
|
+
subscribe(listener) {
|
|
383
|
+
this.listeners.add(listener);
|
|
384
|
+
return () => this.listeners.delete(listener);
|
|
385
|
+
}
|
|
386
|
+
subscribeToAtoms(listener) {
|
|
387
|
+
this.atomListeners.add(listener);
|
|
388
|
+
return () => this.atomListeners.delete(listener);
|
|
389
|
+
}
|
|
390
|
+
notifyListeners() {
|
|
391
|
+
const changes = this.getAtomChanges();
|
|
392
|
+
this.listeners.forEach(listener => listener(changes));
|
|
393
|
+
}
|
|
394
|
+
notifyAtomListeners() {
|
|
395
|
+
const atoms = this.getAtoms();
|
|
396
|
+
this.atomListeners.forEach(listener => listener(atoms));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const jotaiStateStore = exports.jotaiStateStore = new JotaiStateStore();
|