@buoy-gg/jotai 7.0.35 → 7.0.38
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 +32 -5
- 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 -67
- package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +1 -149
- 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 -63
- package/lib/module/jotai/sync/jotaiSyncAdapter.js +1 -146
- 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/typescript/jotai/sync/jotaiSyncAdapter.d.ts +52 -2
- package/package.json +4 -4
- 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,63 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Hook for consuming Jotai atom changes from the store
|
|
5
|
-
*
|
|
6
|
-
* The subscribe / filter / pause / clear plumbing is shared `useChangeLog`;
|
|
7
|
-
* this file owns the jotai-specific filter, stats and atom registry.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { useCallback, useMemo } from "react";
|
|
11
|
-
import { useChangeLog, useSubscribedValue } from "@buoy-gg/shared-ui";
|
|
12
|
-
import { jotaiStateStore } from "../utils/jotaiStateStore";
|
|
13
|
-
function filterChanges(changes, filter) {
|
|
14
|
-
let filtered = changes;
|
|
15
|
-
if (filter.searchText) {
|
|
16
|
-
const search = filter.searchText.toLowerCase();
|
|
17
|
-
filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
|
|
18
|
-
}
|
|
19
|
-
if (filter.atomLabels && filter.atomLabels.length > 0) {
|
|
20
|
-
filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
|
|
21
|
-
}
|
|
22
|
-
if (filter.onlyWithChanges) {
|
|
23
|
-
filtered = filtered.filter(c => c.hasValueChange);
|
|
24
|
-
}
|
|
25
|
-
return filtered;
|
|
26
|
-
}
|
|
27
|
-
const readAtoms = () => jotaiStateStore.getAtoms();
|
|
28
|
-
const subscribeToAtoms = listener => jotaiStateStore.subscribeToAtoms(listener);
|
|
29
|
-
export function useJotaiAtomChanges() {
|
|
30
|
-
const log = useChangeLog(jotaiStateStore, {
|
|
31
|
-
filterItems: filterChanges
|
|
32
|
-
});
|
|
33
|
-
const atoms = useSubscribedValue(readAtoms, subscribeToAtoms);
|
|
34
|
-
const stats = useMemo(() => {
|
|
35
|
-
const total = log.items.length;
|
|
36
|
-
const withChanges = log.items.filter(c => c.hasValueChange).length;
|
|
37
|
-
return {
|
|
38
|
-
totalChanges: total,
|
|
39
|
-
changesWithValueChange: withChanges,
|
|
40
|
-
changesWithoutValueChange: total - withChanges,
|
|
41
|
-
atomCount: atoms.length
|
|
42
|
-
};
|
|
43
|
-
}, [log.items, atoms]);
|
|
44
|
-
const atomLabels = useMemo(() => {
|
|
45
|
-
return jotaiStateStore.getUniqueAtomLabels();
|
|
46
|
-
}, [atoms]);
|
|
47
|
-
const getChangeById = useCallback(id => {
|
|
48
|
-
return jotaiStateStore.getAtomChangeById(id);
|
|
49
|
-
}, []);
|
|
50
|
-
return {
|
|
51
|
-
atomChanges: log.items,
|
|
52
|
-
filteredChanges: log.filteredItems,
|
|
53
|
-
filter: log.filter,
|
|
54
|
-
setFilter: log.setFilter,
|
|
55
|
-
stats,
|
|
56
|
-
atoms,
|
|
57
|
-
clearChanges: log.clear,
|
|
58
|
-
isEnabled: log.isEnabled,
|
|
59
|
-
toggleCapture: log.toggleCapture,
|
|
60
|
-
atomLabels,
|
|
61
|
-
getChangeById
|
|
62
|
-
};
|
|
63
|
-
}
|
|
1
|
+
"use strict";import{useCallback,useMemo}from"react";import{useChangeLog,useSubscribedValue}from"@buoy-gg/shared-ui";import{jotaiStateStore}from"../utils/jotaiStateStore";function filterChanges(e,t){let a=e;if(t.searchText){const e=t.searchText.toLowerCase();a=a.filter(t=>t.atomLabel.toLowerCase().includes(e)||t.valuePreview.toLowerCase().includes(e)||t.changedKeys.some(t=>t.toLowerCase().includes(e)))}return t.atomLabels&&t.atomLabels.length>0&&(a=a.filter(e=>t.atomLabels.includes(e.atomLabel))),t.onlyWithChanges&&(a=a.filter(e=>e.hasValueChange)),a}const readAtoms=()=>jotaiStateStore.getAtoms(),subscribeToAtoms=e=>jotaiStateStore.subscribeToAtoms(e);export function useJotaiAtomChanges(){const e=useChangeLog(jotaiStateStore,{filterItems:filterChanges}),t=useSubscribedValue(readAtoms,subscribeToAtoms),a=useMemo(()=>{const a=e.items.length,o=e.items.filter(e=>e.hasValueChange).length;return{totalChanges:a,changesWithValueChange:o,changesWithoutValueChange:a-o,atomCount:t.length}},[e.items,t]),o=useMemo(()=>jotaiStateStore.getUniqueAtomLabels(),[t]),s=useCallback(e=>jotaiStateStore.getAtomChangeById(e),[]);return{atomChanges:e.items,filteredChanges:e.filteredItems,filter:e.filter,setFilter:e.setFilter,stats:a,atoms:t,clearChanges:jotaiStateStore.clearAtomChanges.bind(jotaiStateStore),isEnabled:e.isEnabled,toggleCapture:e.toggleCapture,atomLabels:o,getChangeById:s}}
|
|
@@ -1,146 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { jotaiStateStore } from "../utils/jotaiStateStore";
|
|
4
|
-
import { WIRE_DETAIL_LIMIT_BYTES, createSyncAdapter, capDetail, detailByIdAction, memoByIdentity, onDeviceMarker, readParams, toWire } from "@buoy-gg/shared-ui";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Sentinel used in WIRE-FORM snapshots in place of raw prevValue/nextValue.
|
|
8
|
-
* Same hazard as redux/zustand: up to 200 changes × 2 values on every
|
|
9
|
-
* snapshot. Dashboards fetch values via `getChangeDetail`.
|
|
10
|
-
*/
|
|
11
|
-
export const VALUE_ON_DEVICE = onDeviceMarker("Value", "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue.");
|
|
12
|
-
/**
|
|
13
|
-
* Per-change wire cache — the store prepends and never mutates a recorded
|
|
14
|
-
* change, so identity is a correct, self-invalidating key.
|
|
15
|
-
*/
|
|
16
|
-
const toWireChange = memoByIdentity(change => ({
|
|
17
|
-
...change,
|
|
18
|
-
prevValue: VALUE_ON_DEVICE,
|
|
19
|
-
nextValue: VALUE_ON_DEVICE
|
|
20
|
-
}));
|
|
21
|
-
const toWireValue = value => toWire(value, VALUE_ON_DEVICE);
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
|
|
25
|
-
* `useExternalSync`.
|
|
26
|
-
*
|
|
27
|
-
* Atom changes are captured by watchAtoms(), independent of whether a
|
|
28
|
-
* dashboard is watching — subscribing here only streams what the store
|
|
29
|
-
* already records. The snapshot carries both the change timeline and the
|
|
30
|
-
* atom registry (with each atom's current value) so the dashboard can render
|
|
31
|
-
* the atom browser.
|
|
32
|
-
*
|
|
33
|
-
* v2: change-log prevValue/nextValue are replaced with on-device markers.
|
|
34
|
-
* Dashboards fetch values via `getChangeDetail`. Oversized / unserializable
|
|
35
|
-
* `atoms[].currentValue` uses the same marker and is fetched via `getAtomValue`.
|
|
36
|
-
*/
|
|
37
|
-
export const jotaiSyncAdapter = createSyncAdapter({
|
|
38
|
-
version: 2,
|
|
39
|
-
getSnapshot: () => ({
|
|
40
|
-
changes: jotaiStateStore.getAtomChanges().map(toWireChange),
|
|
41
|
-
atoms: jotaiStateStore.getAtomSnapshots().map(atom => ({
|
|
42
|
-
...atom,
|
|
43
|
-
currentValue: toWireValue(atom.currentValue)
|
|
44
|
-
}))
|
|
45
|
-
}),
|
|
46
|
-
subscribe: [onChange => jotaiStateStore.subscribe(onChange), onChange => jotaiStateStore.subscribeToAtoms(onChange)],
|
|
47
|
-
actions: {
|
|
48
|
-
clearEvents: () => {
|
|
49
|
-
jotaiStateStore.clearAtomChanges();
|
|
50
|
-
},
|
|
51
|
-
/**
|
|
52
|
-
* On-demand detail: one change's real prevValue/nextValue.
|
|
53
|
-
* The per-snapshot stream stays value-free; this is the explicit,
|
|
54
|
-
* size-guarded channel for the detail pane.
|
|
55
|
-
*/
|
|
56
|
-
getChangeDetail: detailByIdAction(id => jotaiStateStore.getAtomChangeById(id), (change, cap) => ({
|
|
57
|
-
prevValue: cap(change.prevValue),
|
|
58
|
-
nextValue: cap(change.nextValue)
|
|
59
|
-
})),
|
|
60
|
-
/**
|
|
61
|
-
* On-demand current value for one atom. Wire-form snapshots replace
|
|
62
|
-
* oversized / unserializable `currentValue` with VALUE_ON_DEVICE.
|
|
63
|
-
*/
|
|
64
|
-
getAtomValue: params => {
|
|
65
|
-
const {
|
|
66
|
-
label
|
|
67
|
-
} = readParams(params);
|
|
68
|
-
if (!label) return {
|
|
69
|
-
found: false,
|
|
70
|
-
reason: "missing label"
|
|
71
|
-
};
|
|
72
|
-
const atom = jotaiStateStore.getAtom(label);
|
|
73
|
-
if (!atom) return {
|
|
74
|
-
found: false,
|
|
75
|
-
reason: "unknown label"
|
|
76
|
-
};
|
|
77
|
-
let currentValue;
|
|
78
|
-
try {
|
|
79
|
-
currentValue = atom.getValue();
|
|
80
|
-
} catch {
|
|
81
|
-
currentValue = undefined;
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
JSON.stringify(currentValue);
|
|
85
|
-
} catch {
|
|
86
|
-
return {
|
|
87
|
-
found: true,
|
|
88
|
-
label,
|
|
89
|
-
currentValue: VALUE_ON_DEVICE
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
found: true,
|
|
94
|
-
label,
|
|
95
|
-
currentValue: capDetail(currentValue, WIRE_DETAIL_LIMIT_BYTES)
|
|
96
|
-
};
|
|
97
|
-
},
|
|
98
|
-
/**
|
|
99
|
-
* Compact current-value reader for a remote driver (MCP/LLM): registered
|
|
100
|
-
* atoms with light metadata; each atom's `currentValue` stays on the device
|
|
101
|
-
* unless `includeValues` is set. `writable` tells the caller which atoms
|
|
102
|
-
* can be set via `setAtom`.
|
|
103
|
-
*/
|
|
104
|
-
listAtoms: params => {
|
|
105
|
-
const {
|
|
106
|
-
includeValues = false,
|
|
107
|
-
limit
|
|
108
|
-
} = readParams(params);
|
|
109
|
-
const all = jotaiStateStore.getAtoms();
|
|
110
|
-
const capped = typeof limit === "number" && limit > 0 ? all.slice(0, limit) : all;
|
|
111
|
-
const atoms = capped.map(a => {
|
|
112
|
-
let value;
|
|
113
|
-
if (includeValues) {
|
|
114
|
-
try {
|
|
115
|
-
value = a.getValue();
|
|
116
|
-
} catch {
|
|
117
|
-
value = undefined;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
label: a.label,
|
|
122
|
-
changes: a.changeCount,
|
|
123
|
-
writable: typeof a.setValue === "function",
|
|
124
|
-
...(includeValues ? {
|
|
125
|
-
currentValue: value
|
|
126
|
-
} : {})
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
return {
|
|
130
|
-
atoms,
|
|
131
|
-
total: all.length,
|
|
132
|
-
returned: atoms.length,
|
|
133
|
-
includedValues: includeValues
|
|
134
|
-
};
|
|
135
|
-
},
|
|
136
|
-
/** Write a value to a writable atom (throws for unknown/read-only atoms). */
|
|
137
|
-
setAtom: params => {
|
|
138
|
-
const {
|
|
139
|
-
label,
|
|
140
|
-
value
|
|
141
|
-
} = readParams(params);
|
|
142
|
-
if (!label) throw new Error("setAtom requires a `label`");
|
|
143
|
-
jotaiStateStore.setAtomValue(label, value);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
});
|
|
1
|
+
"use strict";import{jotaiStateStore}from"../utils/jotaiStateStore";import{WIRE_DETAIL_LIMIT_BYTES,createSyncAdapter,capDetail,detailByIdAction,expandKeyedArrays,memoByIdentity,onDeviceMarker,patchFromPath,readParams,readPath,refusalText,shapeSummary,suggestedPatch,toWire,typedEditViolations,unshapedLists}from"@buoy-gg/shared-ui";export const VALUE_ON_DEVICE=onDeviceMarker("Value","Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue.");const isPlainObj=e=>!!e&&"object"==typeof e&&!Array.isArray(e);function deepMergeInto(e,t){if(!isPlainObj(e)||!isPlainObj(t))return t;const a={...e};for(const[r,o]of Object.entries(t))a[r]=r in e?deepMergeInto(e[r],o):o;return a}function touchesList(e,t){let a=e;for(const e of t.split(".")){if(!isPlainObj(a))return!1;if(!(e in a))return!1;a=a[e]}return Array.isArray(a)?a.length>0:!!a&&"object"==typeof a}const toWireChange=memoByIdentity(e=>({...e,prevValue:VALUE_ON_DEVICE,nextValue:VALUE_ON_DEVICE})),toWireValue=e=>toWire(e,VALUE_ON_DEVICE);export const jotaiSyncAdapter=createSyncAdapter({version:2,getSnapshot:()=>({changes:jotaiStateStore.getAtomChanges().map(toWireChange),atoms:jotaiStateStore.getAtomSnapshots().map(e=>({...e,currentValue:toWireValue(e.currentValue)}))}),subscribe:[e=>jotaiStateStore.subscribe(e),e=>jotaiStateStore.subscribeToAtoms(e)],actions:{clearEvents:()=>{jotaiStateStore.clearAtomChanges()},getChangeDetail:detailByIdAction(e=>jotaiStateStore.getAtomChangeById(e),(e,t)=>({prevValue:t(e.prevValue),nextValue:t(e.nextValue)})),getAtomValue:e=>{const{label:t,path:a}=readParams(e);if(!t)return{found:!1,reason:"missing label"};const r=jotaiStateStore.getAtom(t);if(!r)return{found:!1,reason:"unknown label"};let o;try{o=r.getValue()}catch{o=void 0}try{JSON.stringify(o)}catch{return{found:!0,label:t,currentValue:VALUE_ON_DEVICE}}if(a){const e=readPath(o,a);return e.ok?{found:!0,label:t,path:a,shape:shapeSummary(e.value),currentValue:e.value}:{found:!1,reason:e.error}}return{found:!0,label:t,...shapeSummary(o)?{shape:shapeSummary(o)}:{},currentValue:capDetail(o,WIRE_DETAIL_LIMIT_BYTES)}},listAtoms:e=>{const{includeValues:t=!1,limit:a}=readParams(e),r=jotaiStateStore.getAtoms(),o=("number"==typeof a&&a>0?r.slice(0,a):r).map(e=>{let a,r;if(t)try{a=e.getValue()}catch{a=void 0}try{r=shapeSummary(e.getValue())}catch{r=void 0}return{label:e.label,changes:e.changeCount,writable:"function"==typeof e.setValue,...r?{shape:r}:{},...t?{currentValue:a}:{}}});return{atoms:o,total:r.length,returned:o.length,includedValues:t}},setAtom:e=>{const{label:t,value:a,merge:r,path:o,force:n}=readParams(e);if(!t)throw new Error("setAtom requires a `label`");const s=jotaiStateStore.getAtom(t);let i;try{i=s?.getValue()}catch{i=void 0}let u=a,l=[],c=[];const d=!0===r||void 0!==o;if(void 0!==o){if(void 0===a)return{ok:!1,error:`Refused: sent \`path\` without \`value\`. Say what to set "${o}" to.`};const e=patchFromPath(i,o,a);if(!e.ok)return{ok:!1,error:`Refused: ${e.error}`};u=e.patch}if(!n&&d&&void 0!==i){const e=expandKeyedArrays(i,u);if(e.error)return{ok:!1,error:`Refused: ${e.error}`};u=e.patch,l=e.notes,c=unshapedLists(i).filter(e=>touchesList(u,e));const t=typedEditViolations(i,u,{noun:"atom"});if(t.length){const e=suggestedPatch(u,t);return{ok:!1,error:refusalText(t,"atom",e),violations:t,...e?{suggestedValue:e}:{}}}}return jotaiStateStore.setAtomValue(t,d?deepMergeInto(i,u):u),{ok:!0,...void 0!==o?{path:o}:{},...l.length?{keyed:l}:{},...c.length?{unchecked:c.map(e=>`\`${e}\` was empty, so nothing knows what its items look like and the shape of what you added was NOT checked.`)}:{}}}}});
|
|
@@ -1,317 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Jotai state store — captures and stores Jotai atom changes
|
|
5
|
-
*
|
|
6
|
-
* Mirrors the architecture of zustandStateStore.ts from @buoy-gg/zustand
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { ChangeLog, entityColor, formatValuePreview as sharedFormatValuePreview, shallowDiffSummary } from "@buoy-gg/shared-ui";
|
|
10
|
-
// ============================================
|
|
11
|
-
// Atom Color Palette
|
|
12
|
-
// ============================================
|
|
13
|
-
|
|
14
|
-
const ATOM_COLORS = {
|
|
15
|
-
count: "#10B981",
|
|
16
|
-
// emerald
|
|
17
|
-
auth: "#8B5CF6",
|
|
18
|
-
// purple
|
|
19
|
-
user: "#3B82F6",
|
|
20
|
-
// blue
|
|
21
|
-
cart: "#EC4899",
|
|
22
|
-
// pink
|
|
23
|
-
app: "#6366F1",
|
|
24
|
-
// indigo
|
|
25
|
-
ui: "#F59E0B",
|
|
26
|
-
// amber
|
|
27
|
-
settings: "#14B8A6",
|
|
28
|
-
// teal
|
|
29
|
-
theme: "#06B6D4",
|
|
30
|
-
// cyan
|
|
31
|
-
nav: "#F97316",
|
|
32
|
-
// orange
|
|
33
|
-
form: "#EF4444",
|
|
34
|
-
// red
|
|
35
|
-
modal: "#A855F7",
|
|
36
|
-
// violet
|
|
37
|
-
filter: "#84CC16" // lime
|
|
38
|
-
};
|
|
39
|
-
function getAtomColor(label) {
|
|
40
|
-
return entityColor(label, ATOM_COLORS);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ============================================
|
|
44
|
-
// Helper Functions
|
|
45
|
-
// ============================================
|
|
46
|
-
|
|
47
|
-
function formatValuePreview(value, maxLength = 40) {
|
|
48
|
-
return sharedFormatValuePreview(value, {
|
|
49
|
-
maxLength,
|
|
50
|
-
functionLabel: "(fn)"
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
const getValueDiffSummary = shallowDiffSummary;
|
|
54
|
-
|
|
55
|
-
// ============================================
|
|
56
|
-
// Jotai State Store
|
|
57
|
-
// ============================================
|
|
58
|
-
|
|
59
|
-
class JotaiStateStore extends ChangeLog {
|
|
60
|
-
atoms = new Map();
|
|
61
|
-
atomListeners = new Set();
|
|
62
|
-
|
|
63
|
-
// ---- Change Tracking ----
|
|
64
|
-
|
|
65
|
-
addAtomChange(params) {
|
|
66
|
-
if (!this.canRecord()) return;
|
|
67
|
-
const {
|
|
68
|
-
atomLabel,
|
|
69
|
-
prevValue,
|
|
70
|
-
nextValue,
|
|
71
|
-
category = "write"
|
|
72
|
-
} = params;
|
|
73
|
-
const hasValueChange = prevValue !== nextValue;
|
|
74
|
-
const {
|
|
75
|
-
summary,
|
|
76
|
-
changedKeys,
|
|
77
|
-
changedCount
|
|
78
|
-
} = getValueDiffSummary(prevValue, nextValue);
|
|
79
|
-
const valuePreview = formatValuePreview(nextValue);
|
|
80
|
-
const change = {
|
|
81
|
-
id: this.nextId(),
|
|
82
|
-
atomLabel,
|
|
83
|
-
timestamp: Date.now(),
|
|
84
|
-
prevValue,
|
|
85
|
-
nextValue,
|
|
86
|
-
hasValueChange,
|
|
87
|
-
category,
|
|
88
|
-
changedKeys,
|
|
89
|
-
changedKeysCount: changedCount,
|
|
90
|
-
diffSummary: summary,
|
|
91
|
-
valuePreview,
|
|
92
|
-
isSlowUpdate: false
|
|
93
|
-
};
|
|
94
|
-
this.push(change);
|
|
95
|
-
const atomInfo = this.atoms.get(atomLabel);
|
|
96
|
-
if (atomInfo) {
|
|
97
|
-
atomInfo.changeCount++;
|
|
98
|
-
}
|
|
99
|
-
this.notifyListeners();
|
|
100
|
-
this.notifyAtomListeners();
|
|
101
|
-
}
|
|
102
|
-
getAtomChanges() {
|
|
103
|
-
return this.getAll();
|
|
104
|
-
}
|
|
105
|
-
getAtomChangeById(id) {
|
|
106
|
-
return this.getById(id);
|
|
107
|
-
}
|
|
108
|
-
clearAtomChanges() {
|
|
109
|
-
for (const atom of this.atoms.values()) {
|
|
110
|
-
atom.changeCount = 0;
|
|
111
|
-
}
|
|
112
|
-
this.clear();
|
|
113
|
-
this.notifyAtomListeners();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ---- Remote Mirror Mode ----
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Serializable snapshot of the tracked atoms (registry metadata plus each
|
|
120
|
-
* atom's current value). Used by the sync adapter on the device side —
|
|
121
|
-
* the live `getValue` handles can't go over the wire.
|
|
122
|
-
*/
|
|
123
|
-
getAtomSnapshots() {
|
|
124
|
-
return Array.from(this.atoms.values()).map(atom => {
|
|
125
|
-
let currentValue;
|
|
126
|
-
try {
|
|
127
|
-
currentValue = atom.getValue();
|
|
128
|
-
} catch {
|
|
129
|
-
currentValue = undefined;
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
label: atom.label,
|
|
133
|
-
changeCount: atom.changeCount,
|
|
134
|
-
color: atom.color,
|
|
135
|
-
currentValue,
|
|
136
|
-
writeKind: atom.writeKind
|
|
137
|
-
};
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Replace the entire mirror contents from a synced device snapshot. The
|
|
143
|
-
* rebuilt registry entries get stub `getValue` handles serving the
|
|
144
|
-
* snapshotted value. Respects the enabled flag so the UI's pause button
|
|
145
|
-
* freezes the mirror.
|
|
146
|
-
*/
|
|
147
|
-
replaceFromSnapshot(changes, atomSnapshots) {
|
|
148
|
-
if (!this.getEnabled()) return;
|
|
149
|
-
|
|
150
|
-
// Preserve values a remote mirror already hydrated — wire-form snapshots
|
|
151
|
-
// always carry markers, and a later snapshot must not blank the detail pane.
|
|
152
|
-
const hydrated = new Map();
|
|
153
|
-
for (const existing of this.items) {
|
|
154
|
-
const prev = existing.prevValue;
|
|
155
|
-
if (!prev || prev.__buoyValueOnDevice !== true) {
|
|
156
|
-
hydrated.set(existing.id, existing);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const merged = changes.slice(0, this.max).map(change => {
|
|
160
|
-
const kept = hydrated.get(change.id);
|
|
161
|
-
const incoming = change.prevValue;
|
|
162
|
-
return kept && incoming?.__buoyValueOnDevice === true ? {
|
|
163
|
-
...change,
|
|
164
|
-
prevValue: kept.prevValue,
|
|
165
|
-
nextValue: kept.nextValue
|
|
166
|
-
} : change;
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Preserve currentValue a remote mirror already hydrated — oversized
|
|
170
|
-
// atoms arrive as VALUE_ON_DEVICE markers on every snapshot.
|
|
171
|
-
const hydratedAtoms = new Map();
|
|
172
|
-
for (const [label, atom] of this.atoms) {
|
|
173
|
-
try {
|
|
174
|
-
const value = atom.getValue();
|
|
175
|
-
const marker = value;
|
|
176
|
-
if (!marker || marker.__buoyValueOnDevice !== true) {
|
|
177
|
-
hydratedAtoms.set(label, value);
|
|
178
|
-
}
|
|
179
|
-
} catch {
|
|
180
|
-
// ignore
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
this.atoms = new Map(atomSnapshots.map(snapshot => {
|
|
184
|
-
const incoming = snapshot.currentValue;
|
|
185
|
-
const kept = hydratedAtoms.get(snapshot.label);
|
|
186
|
-
const currentValue = kept !== undefined && incoming?.__buoyValueOnDevice === true ? kept : snapshot.currentValue;
|
|
187
|
-
return [snapshot.label, {
|
|
188
|
-
label: snapshot.label,
|
|
189
|
-
changeCount: snapshot.changeCount,
|
|
190
|
-
color: snapshot.color,
|
|
191
|
-
getValue: () => currentValue,
|
|
192
|
-
// No setValue: the mirror has no wire back to the device from
|
|
193
|
-
// this entry (yet) — the detail screen renders read-only there.
|
|
194
|
-
writeKind: snapshot.writeKind
|
|
195
|
-
}];
|
|
196
|
-
}));
|
|
197
|
-
this.setItems(merged);
|
|
198
|
-
this.notifyAtomListeners();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Hydrate one mirrored change with real values fetched on demand from the
|
|
203
|
-
* device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
|
|
204
|
-
*/
|
|
205
|
-
hydrateChange(id, patch) {
|
|
206
|
-
this.hydrateById(id, patch);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Hydrate one mirrored atom's currentValue fetched on demand from the
|
|
211
|
-
* device via the adapter's `getAtomValue`. Remote-mirror (desktop) only.
|
|
212
|
-
*/
|
|
213
|
-
hydrateAtomValue(label, currentValue) {
|
|
214
|
-
const atom = this.atoms.get(label);
|
|
215
|
-
if (!atom) return;
|
|
216
|
-
this.atoms.set(label, {
|
|
217
|
-
...atom,
|
|
218
|
-
getValue: () => currentValue
|
|
219
|
-
});
|
|
220
|
-
this.notifyAtomListeners();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ---- Atom Registry ----
|
|
224
|
-
|
|
225
|
-
registerAtom(label, getValue, setValue, writeKind) {
|
|
226
|
-
if (this.atoms.has(label)) return;
|
|
227
|
-
const atomInfo = {
|
|
228
|
-
label,
|
|
229
|
-
changeCount: 0,
|
|
230
|
-
color: getAtomColor(label),
|
|
231
|
-
getValue,
|
|
232
|
-
setValue,
|
|
233
|
-
writeKind
|
|
234
|
-
};
|
|
235
|
-
this.atoms.set(label, atomInfo);
|
|
236
|
-
this.notifyAtomListeners();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Write a value to a registered atom (remote write from the dashboard/MCP).
|
|
241
|
-
* Throws if the atom is unknown or read-only (derived atoms have no setter).
|
|
242
|
-
*/
|
|
243
|
-
setAtomValue(label, value) {
|
|
244
|
-
const atom = this.atoms.get(label);
|
|
245
|
-
if (!atom) {
|
|
246
|
-
throw new Error(`Atom "${label}" is not registered`);
|
|
247
|
-
}
|
|
248
|
-
if (!atom.setValue) {
|
|
249
|
-
throw new Error(`Atom "${label}" is read-only (derived) — cannot set`);
|
|
250
|
-
}
|
|
251
|
-
atom.setValue(value);
|
|
252
|
-
}
|
|
253
|
-
unregisterAtom(label) {
|
|
254
|
-
this.atoms.delete(label);
|
|
255
|
-
this.notifyAtomListeners();
|
|
256
|
-
}
|
|
257
|
-
getAtoms() {
|
|
258
|
-
return Array.from(this.atoms.values());
|
|
259
|
-
}
|
|
260
|
-
getAtom(label) {
|
|
261
|
-
return this.atoms.get(label);
|
|
262
|
-
}
|
|
263
|
-
getAtomColor(label) {
|
|
264
|
-
return this.atoms.get(label)?.color ?? getAtomColor(label);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// ---- Filtering ----
|
|
268
|
-
|
|
269
|
-
filterAtomChanges(filter) {
|
|
270
|
-
let filtered = this.getAll();
|
|
271
|
-
if (filter.searchText) {
|
|
272
|
-
const search = filter.searchText.toLowerCase();
|
|
273
|
-
filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
|
|
274
|
-
}
|
|
275
|
-
if (filter.atomLabels && filter.atomLabels.length > 0) {
|
|
276
|
-
filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
|
|
277
|
-
}
|
|
278
|
-
if (filter.onlyWithChanges) {
|
|
279
|
-
filtered = filtered.filter(c => c.hasValueChange);
|
|
280
|
-
}
|
|
281
|
-
return filtered;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ---- Stats ----
|
|
285
|
-
|
|
286
|
-
getStats() {
|
|
287
|
-
const total = this.items.length;
|
|
288
|
-
const withChanges = this.items.filter(c => c.hasValueChange).length;
|
|
289
|
-
return {
|
|
290
|
-
totalChanges: total,
|
|
291
|
-
changesWithValueChange: withChanges,
|
|
292
|
-
changesWithoutValueChange: total - withChanges,
|
|
293
|
-
atomCount: this.atoms.size
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
getUniqueAtomLabels() {
|
|
297
|
-
return Array.from(this.atoms.keys()).sort();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ---- Enable / Disable ----
|
|
301
|
-
|
|
302
|
-
setMaxChanges(max) {
|
|
303
|
-
this.setMax(max);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ---- Subscriptions ----
|
|
307
|
-
|
|
308
|
-
subscribeToAtoms(listener) {
|
|
309
|
-
this.atomListeners.add(listener);
|
|
310
|
-
return () => this.atomListeners.delete(listener);
|
|
311
|
-
}
|
|
312
|
-
notifyAtomListeners() {
|
|
313
|
-
const atoms = this.getAtoms();
|
|
314
|
-
this.atomListeners.forEach(listener => listener(atoms));
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
export const jotaiStateStore = new JotaiStateStore();
|
|
1
|
+
"use strict";import{ChangeLog,entityColor,formatValuePreview as sharedFormatValuePreview,shallowDiffSummary}from"@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(e){return entityColor(e,ATOM_COLORS)}function formatValuePreview(e,t=40){return sharedFormatValuePreview(e,{maxLength:t,functionLabel:"(fn)"})}const getValueDiffSummary=shallowDiffSummary;class JotaiStateStore extends ChangeLog{atoms=new Map;atomListeners=new Set;addAtomChange(e){if(!this.canRecord())return;const{atomLabel:t,prevValue:a,nextValue:o,category:s="write"}=e,r=a!==o,{summary:n,changedKeys:i,changedCount:l}=getValueDiffSummary(a,o),u=formatValuePreview(o),h={id:this.nextId(),atomLabel:t,timestamp:Date.now(),prevValue:a,nextValue:o,hasValueChange:r,category:s,changedKeys:i,changedKeysCount:l,diffSummary:n,valuePreview:u,isSlowUpdate:!1};this.push(h);const m=this.atoms.get(t);m&&m.changeCount++,this.notifyListeners(),this.notifyAtomListeners()}getAtomChanges(){return this.getAll()}getAtomChangeById(e){return this.getById(e)}clearAtomChanges(){for(const e of this.atoms.values())e.changeCount=0;this.clear(),this.notifyAtomListeners()}getAtomSnapshots(){return Array.from(this.atoms.values()).map(e=>{let t;try{t=e.getValue()}catch{t=void 0}return{label:e.label,changeCount:e.changeCount,color:e.color,currentValue:t,writeKind:e.writeKind}})}replaceFromSnapshot(e,t){if(!this.getEnabled())return;const a=new Map;for(const e of this.items){const t=e.prevValue;t&&!0===t.__buoyValueOnDevice||a.set(e.id,e)}const o=e.slice(0,this.max).map(e=>{const t=a.get(e.id),o=e.prevValue;return t&&!0===o?.__buoyValueOnDevice?{...e,prevValue:t.prevValue,nextValue:t.nextValue}:e}),s=new Map;for(const[e,t]of this.atoms)try{const a=t.getValue();a&&!0===a.__buoyValueOnDevice||s.set(e,a)}catch{}this.atoms=new Map(t.map(e=>{const t=e.currentValue,a=s.get(e.label),o=void 0!==a&&!0===t?.__buoyValueOnDevice?a:e.currentValue;return[e.label,{label:e.label,changeCount:e.changeCount,color:e.color,getValue:()=>o,writeKind:e.writeKind}]})),this.setItems(o),this.notifyAtomListeners()}hydrateChange(e,t){this.hydrateById(e,t)}hydrateAtomValue(e,t){const a=this.atoms.get(e);a&&(this.atoms.set(e,{...a,getValue:()=>t}),this.notifyAtomListeners())}registerAtom(e,t,a,o){if(this.atoms.has(e))return;const s={label:e,changeCount:0,color:getAtomColor(e),getValue:t,setValue:a,writeKind:o};this.atoms.set(e,s),this.notifyAtomListeners()}setAtomValue(e,t){const a=this.atoms.get(e);if(!a)throw new Error(`Atom "${e}" is not registered`);if(!a.setValue)throw new Error(`Atom "${e}" is read-only (derived) — cannot set`);a.setValue(t)}unregisterAtom(e){this.atoms.delete(e),this.notifyAtomListeners()}getAtoms(){return Array.from(this.atoms.values())}getAtom(e){return this.atoms.get(e)}getAtomColor(e){return this.atoms.get(e)?.color??getAtomColor(e)}filterAtomChanges(e){let t=this.getAll();if(e.searchText){const a=e.searchText.toLowerCase();t=t.filter(e=>e.atomLabel.toLowerCase().includes(a)||e.valuePreview.toLowerCase().includes(a)||e.changedKeys.some(e=>e.toLowerCase().includes(a)))}return e.atomLabels&&e.atomLabels.length>0&&(t=t.filter(t=>e.atomLabels.includes(t.atomLabel))),e.onlyWithChanges&&(t=t.filter(e=>e.hasValueChange)),t}getStats(){const e=this.items.length,t=this.items.filter(e=>e.hasValueChange).length;return{totalChanges:e,changesWithValueChange:t,changesWithoutValueChange:e-t,atomCount:this.atoms.size}}getUniqueAtomLabels(){return Array.from(this.atoms.keys()).sort()}setMaxChanges(e){this.setMax(e)}subscribeToAtoms(e){return this.atomListeners.add(e),()=>this.atomListeners.delete(e)}notifyAtomListeners(){const e=this.getAtoms();this.atomListeners.forEach(t=>t(e))}}export const jotaiStateStore=new JotaiStateStore;
|