@buoy-gg/jotai 7.0.32 → 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.
Files changed (35) hide show
  1. package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +20 -41
  2. package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +7 -52
  3. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +8 -87
  4. package/lib/commonjs/jotai/components/JotaiEventFilterView.js +21 -299
  5. package/lib/commonjs/jotai/components/JotaiModal.js +82 -124
  6. package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +33 -49
  7. package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +19 -66
  8. package/lib/commonjs/jotai/utils/jotaiStateStore.js +23 -188
  9. package/lib/commonjs/jotai/utils/snapshotProvider.js +7 -25
  10. package/lib/commonjs/preset.js +14 -22
  11. package/lib/module/jotai/components/JotaiAtomBrowser.js +21 -42
  12. package/lib/module/jotai/components/JotaiAtomChangeItem.js +9 -54
  13. package/lib/module/jotai/components/JotaiAtomDetailContent.js +9 -88
  14. package/lib/module/jotai/components/JotaiEventFilterView.js +24 -302
  15. package/lib/module/jotai/components/JotaiModal.js +84 -126
  16. package/lib/module/jotai/hooks/useJotaiAtomChanges.js +34 -50
  17. package/lib/module/jotai/sync/jotaiSyncAdapter.js +20 -67
  18. package/lib/module/jotai/utils/jotaiStateStore.js +23 -188
  19. package/lib/module/jotai/utils/snapshotProvider.js +8 -26
  20. package/lib/module/preset.js +13 -20
  21. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -1
  22. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -1
  23. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
  24. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts +2 -2
  25. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -1
  26. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
  27. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts +2 -1
  28. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +1 -1
  29. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +54 -68
  30. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
  31. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +2 -25
  32. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
  33. package/lib/typescript/preset.d.ts +2 -8
  34. package/lib/typescript/preset.d.ts.map +1 -1
  35. package/package.json +3 -3
@@ -9,8 +9,8 @@
9
9
  */
10
10
 
11
11
  import { useState, useMemo, useRef, useCallback, useEffect } from "react";
12
- import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity } from "react-native";
13
- import { JsModal, ModalHeader, TabSelector, macOSColors, night, Search, Filter, Power, X, Box, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, windowDragProps } from "@buoy-gg/shared-ui";
12
+ import { View, Text, StyleSheet, FlatList, TouchableOpacity } from "react-native";
13
+ import { JsModal, ModalHeader, TabSelector, macOSColors, night, Search, Filter, Power, ToolEmptyState, resolveListEmptyState, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, windowDragProps, EVENT_LIST_PRESET } from "@buoy-gg/shared-ui";
14
14
  import { useIsPro } from "@buoy-gg/license";
15
15
  import { useJotaiAtomChanges } from "../hooks/useJotaiAtomChanges";
16
16
  import { JotaiAtomChangeItem } from "./JotaiAtomChangeItem";
@@ -21,33 +21,32 @@ import { JotaiEventFilterView } from "./JotaiEventFilterView";
21
21
  import { JOTAI_TEST_ID, buildChangeRowTestIDs } from "../utils/testIDs";
22
22
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
23
23
  /**
24
- * Cell clipping, off in e2e bundles only.
25
- *
26
- * `removeClippedSubviews` unmounts the views of rows scrolled out of the
27
- * window, which is what makes a long change list cheap — and which makes
28
- * Detox report a perfectly rendered row as un-hittable. The env var is
29
- * inlined at bundle time, so a bundler that does NOT inline it leaves the
30
- * production behaviour exactly as it was.
24
+ * The change log's "nothing here". Everything but the nouns is the shared
25
+ * precedence — see `ToolEmptyState` in shared-ui for why it lives there.
31
26
  */
32
- const CLIP_OFFSCREEN_CELLS = !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E);
33
27
  function EventsEmptyState({
34
- isEnabled
28
+ isEnabled,
29
+ searchText,
30
+ totalChanges
35
31
  }) {
36
- return /*#__PURE__*/_jsxs(View, {
37
- style: styles.emptyState,
38
- testID: `${JOTAI_TEST_ID}-empty`,
39
- children: [/*#__PURE__*/_jsx(Box, {
40
- size: 32,
41
- color: night.color.textTertiary
42
- }), /*#__PURE__*/_jsx(Text, {
43
- style: styles.emptyTitle,
44
- testID: `${JOTAI_TEST_ID}-empty-title`,
45
- children: "No atom changes"
46
- }), /*#__PURE__*/_jsx(Text, {
47
- style: styles.emptyText,
48
- testID: `${JOTAI_TEST_ID}-empty-text`,
49
- children: isEnabled ? "Atom changes will appear here as values update." : "Enable capture to start recording atom changes"
50
- })]
32
+ const content = resolveListEmptyState({
33
+ subject: "changes",
34
+ search: searchText,
35
+ total: totalChanges,
36
+ capturing: isEnabled,
37
+ waiting: {
38
+ otto: "waiting",
39
+ title: "No atom changes",
40
+ message: "Atom changes will appear here as values update."
41
+ },
42
+ paused: {
43
+ title: "No atom changes",
44
+ message: "Enable capture to start recording atom changes"
45
+ }
46
+ });
47
+ return /*#__PURE__*/_jsx(ToolEmptyState, {
48
+ ...content,
49
+ testID: `${JOTAI_TEST_ID}-empty`
51
50
  });
52
51
  }
53
52
  export function JotaiModal({
@@ -74,7 +73,9 @@ export function JotaiModal({
74
73
  const [showFilters, setShowFilters] = useState(false);
75
74
  const [ignoredPatterns, setIgnoredPatterns] = useState(new Set());
76
75
  const {
76
+ atomChanges,
77
77
  filteredChanges,
78
+ stats,
78
79
  filter,
79
80
  setFilter,
80
81
  atoms,
@@ -97,6 +98,15 @@ export function JotaiModal({
97
98
 
98
99
  // Pro is Infinity here, so slice() returns everything and the locked count
99
100
  // floors at 0 — no tier branching needed at the call site.
101
+ /**
102
+ * How many changes this atom has captured, before the search — the
103
+ * denominator the scoped empty state needs so "clear the search" is only
104
+ * offered when clearing it would actually show something.
105
+ */
106
+ const scopedTotalForAtom = useMemo(() => {
107
+ if (!selectedHistoryAtom) return 0;
108
+ return atomChanges.filter(c => c.atomLabel === selectedHistoryAtom).length;
109
+ }, [atomChanges, selectedHistoryAtom]);
100
110
  const visibleChanges = useMemo(() => displayedChanges.slice(0, changeLimit), [displayedChanges, changeLimit]);
101
111
  const lockedChangeCount = useMemo(() => Math.max(0, displayedChanges.length - changeLimit), [displayedChanges.length, changeLimit]);
102
112
  const hasLockedChanges = lockedChangeCount > 0;
@@ -332,49 +342,24 @@ export function JotaiModal({
332
342
  onBack: onBack
333
343
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
334
344
  title: "",
335
- children: isSearchActive ? /*#__PURE__*/_jsxs(View, {
336
- style: styles.headerSearchContainer,
337
- testID: `${JOTAI_TEST_ID}-search`,
338
- children: [/*#__PURE__*/_jsx(Search, {
339
- size: 14,
340
- color: night.color.textSecondary
341
- }), /*#__PURE__*/_jsx(TextInput, {
342
- ref: searchInputRef,
343
- testID: `${JOTAI_TEST_ID}-search-field`,
344
- style: styles.headerSearchInput,
345
- placeholder: activeTab === "atoms" ? "Search atoms..." : "Search events...",
346
- placeholderTextColor: night.color.placeholder,
347
- value: searchText,
348
- onChangeText: handleSearch,
349
- onSubmitEditing: () => searchInputRef.current?.blur()
350
- // Stay open while the query is still narrowing the list.
351
- //
352
- // Collapsing on blur hid the reason rows were missing: the
353
- // header went back to its TABS — whose counts are of the
354
- // WHOLE capture — while the list stayed filtered, so the tool
355
- // showed "Atoms (8)" above a single row and offered nothing to
356
- // explain the gap. The only way back was to reopen a field the
357
- // user had no reason to think was still armed. Same shape, and
358
- // the same fix, as the Redux, Events and Zustand tools.
359
- ,
360
- onBlur: () => {
361
- if (searchText.length === 0) setIsSearchActive(false);
362
- },
363
- accessibilityLabel: "Search Jotai atoms and events",
364
- autoCapitalize: "none",
365
- autoCorrect: false,
366
- returnKeyType: "search"
367
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
368
- onPress: closeSearch,
369
- style: styles.headerSearchClear,
370
- testID: `${JOTAI_TEST_ID}-search-clear`,
371
- accessibilityRole: "button",
372
- accessibilityLabel: "Close search",
373
- children: /*#__PURE__*/_jsx(X, {
374
- size: 14,
375
- color: night.color.textSecondary
376
- })
377
- })]
345
+ children: isSearchActive ? /*#__PURE__*/_jsx(ModalHeader.Search, {
346
+ value: searchText,
347
+ onChangeText: handleSearch,
348
+ onCancel: closeSearch,
349
+ placeholder: activeTab === "atoms" ? "Search atoms..." : "Search events...",
350
+ accessibilityLabel: "Search Jotai atoms and events",
351
+ iconSize: 14,
352
+ testID: `${JOTAI_TEST_ID}-search-field`,
353
+ containerTestID: `${JOTAI_TEST_ID}-search`,
354
+ clearTestID: `${JOTAI_TEST_ID}-search-clear`
355
+ // Stay open while the query is still narrowing the list: the
356
+ // header would otherwise go back to its TABS — whose counts are
357
+ // of the WHOLE capture — above a filtered list, with nothing to
358
+ // explain the gap.
359
+ ,
360
+ onBlur: () => {
361
+ if (searchText.length === 0) setIsSearchActive(false);
362
+ }
378
363
  }) : /*#__PURE__*/_jsx(TabSelector, {
379
364
  tabs: [{
380
365
  key: "atoms",
@@ -464,24 +449,25 @@ export function JotaiModal({
464
449
  keyExtractor: keyExtractor,
465
450
  contentContainerStyle: styles.listContent,
466
451
  showsVerticalScrollIndicator: true,
467
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
468
- initialNumToRender: 12,
469
- maxToRenderPerBatch: 8,
470
- windowSize: 5,
471
- updateCellsBatchingPeriod: 50
472
- }) : /*#__PURE__*/_jsxs(View, {
473
- style: styles.emptyState,
452
+ ...EVENT_LIST_PRESET
453
+ }) : /*#__PURE__*/_jsx(ToolEmptyState, {
474
454
  testID: `${JOTAI_TEST_ID}-history-empty`,
475
- children: [/*#__PURE__*/_jsx(Box, {
476
- size: 32,
477
- color: night.color.textTertiary
478
- }), /*#__PURE__*/_jsx(Text, {
479
- style: styles.emptyTitle,
480
- children: "No history yet"
481
- }), /*#__PURE__*/_jsxs(Text, {
482
- style: styles.emptyText,
483
- children: ["Changes for ", selectedHistoryAtom, " will appear here."]
484
- })]
455
+ ...resolveListEmptyState({
456
+ subject: "changes",
457
+ search: searchText,
458
+ // Scoped to one atom, so the count is this atom's, not the log's.
459
+ total: searchText.trim() ? scopedTotalForAtom : 0,
460
+ capturing: isEnabled,
461
+ waiting: {
462
+ otto: "waiting",
463
+ title: "No history yet",
464
+ message: `Changes for ${selectedHistoryAtom} will appear here.`
465
+ },
466
+ paused: {
467
+ title: "No history yet",
468
+ message: `Capture is off, so nothing is being recorded for ${selectedHistoryAtom}.`
469
+ }
470
+ })
485
471
  });
486
472
  }
487
473
 
@@ -497,19 +483,14 @@ export function JotaiModal({
497
483
  /*#__PURE__*/
498
484
  // The atom fell out of the registry mid-look (a remote mirror
499
485
  // rebuilding, a dev reload). Say so rather than rendering nothing.
500
- _jsxs(View, {
501
- style: styles.emptyState,
502
- testID: `${JOTAI_TEST_ID}-atom-detail-gone`,
503
- children: [/*#__PURE__*/_jsx(Box, {
504
- size: 32,
505
- color: night.color.textTertiary
506
- }), /*#__PURE__*/_jsx(Text, {
507
- style: styles.emptyTitle,
508
- children: "Atom no longer registered"
509
- }), /*#__PURE__*/_jsxs(Text, {
510
- style: styles.emptyText,
511
- children: [selectedAtomLabel, " is not in the registry any more."]
512
- })]
486
+ _jsx(ToolEmptyState, {
487
+ testID: `${JOTAI_TEST_ID}-atom-detail-gone`
488
+ // ghost = "stale or detached: an unmounted component, an old
489
+ // snapshot" — exactly what the atom you are looking at has become.
490
+ ,
491
+ otto: "ghost",
492
+ title: "Atom no longer registered",
493
+ message: `${selectedAtomLabel} is not in the registry any more.`
513
494
  });
514
495
  }
515
496
  if (activeTab === "atoms") {
@@ -557,13 +538,11 @@ export function JotaiModal({
557
538
  keyExtractor: keyExtractor,
558
539
  contentContainerStyle: styles.listContent,
559
540
  showsVerticalScrollIndicator: true,
560
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
561
- initialNumToRender: 12,
562
- maxToRenderPerBatch: 8,
563
- windowSize: 5,
564
- updateCellsBatchingPeriod: 50
541
+ ...EVENT_LIST_PRESET
565
542
  }) : /*#__PURE__*/_jsx(EventsEmptyState, {
566
- isEnabled: isEnabled
543
+ isEnabled: isEnabled,
544
+ searchText: searchText,
545
+ totalChanges: stats.totalChanges
567
546
  })]
568
547
  });
569
548
  };
@@ -673,27 +652,6 @@ const styles = StyleSheet.create({
673
652
  // Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
674
653
  backgroundColor: "transparent"
675
654
  },
676
- headerSearchContainer: {
677
- flexDirection: "row",
678
- alignItems: "center",
679
- backgroundColor: night.color.surfaceElevated,
680
- borderRadius: night.radius.row,
681
- borderWidth: 1,
682
- borderColor: night.color.border,
683
- paddingHorizontal: 12,
684
- paddingVertical: 5
685
- },
686
- headerSearchInput: {
687
- flex: 1,
688
- color: night.color.text,
689
- fontSize: 13,
690
- marginLeft: 6,
691
- paddingVertical: 2
692
- },
693
- headerSearchClear: {
694
- marginLeft: 6,
695
- padding: 4
696
- },
697
655
  headerActionButton: {
698
656
  width: 32,
699
657
  height: 32,
@@ -3,76 +3,60 @@
3
3
  /**
4
4
  * Hook for consuming Jotai atom changes from the store
5
5
  *
6
- * Mirrors useZustandStateChanges.ts from @buoy-gg/zustand
6
+ * The subscribe / filter / pause / clear plumbing is shared `useChangeLog`;
7
+ * this file owns the jotai-specific filter, stats and atom registry.
7
8
  */
8
9
 
9
- import { useState, useEffect, useMemo, useCallback } from "react";
10
+ import { useCallback, useMemo } from "react";
11
+ import { useChangeLog, useSubscribedValue } from "@buoy-gg/shared-ui";
10
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);
11
29
  export function useJotaiAtomChanges() {
12
- const [atomChanges, setAtomChanges] = useState(() => jotaiStateStore.getAtomChanges());
13
- const [atoms, setAtoms] = useState(() => jotaiStateStore.getAtoms());
14
- const [filter, setFilter] = useState({});
15
- const [isEnabled, setIsEnabled] = useState(() => jotaiStateStore.getEnabled());
16
- useEffect(() => {
17
- const unsubChanges = jotaiStateStore.subscribe(newChanges => {
18
- setAtomChanges(newChanges);
19
- });
20
- const unsubAtoms = jotaiStateStore.subscribeToAtoms(newAtoms => {
21
- setAtoms(newAtoms);
22
- });
23
- return () => {
24
- unsubChanges();
25
- unsubAtoms();
26
- };
27
- }, []);
28
- const filteredChanges = useMemo(() => {
29
- let filtered = atomChanges;
30
- if (filter.searchText) {
31
- const search = filter.searchText.toLowerCase();
32
- filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
33
- }
34
- if (filter.atomLabels && filter.atomLabels.length > 0) {
35
- filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
36
- }
37
- if (filter.onlyWithChanges) {
38
- filtered = filtered.filter(c => c.hasValueChange);
39
- }
40
- return filtered;
41
- }, [atomChanges, filter]);
30
+ const log = useChangeLog(jotaiStateStore, {
31
+ filterItems: filterChanges
32
+ });
33
+ const atoms = useSubscribedValue(readAtoms, subscribeToAtoms);
42
34
  const stats = useMemo(() => {
43
- const total = atomChanges.length;
44
- const withChanges = atomChanges.filter(c => c.hasValueChange).length;
35
+ const total = log.items.length;
36
+ const withChanges = log.items.filter(c => c.hasValueChange).length;
45
37
  return {
46
38
  totalChanges: total,
47
39
  changesWithValueChange: withChanges,
48
40
  changesWithoutValueChange: total - withChanges,
49
41
  atomCount: atoms.length
50
42
  };
51
- }, [atomChanges, atoms]);
43
+ }, [log.items, atoms]);
52
44
  const atomLabels = useMemo(() => {
53
45
  return jotaiStateStore.getUniqueAtomLabels();
54
46
  }, [atoms]);
55
- const clearChanges = useCallback(() => {
56
- jotaiStateStore.clearAtomChanges();
57
- }, []);
58
- const toggleCapture = useCallback(() => {
59
- const newEnabled = !isEnabled;
60
- jotaiStateStore.setEnabled(newEnabled);
61
- setIsEnabled(newEnabled);
62
- }, [isEnabled]);
63
47
  const getChangeById = useCallback(id => {
64
48
  return jotaiStateStore.getAtomChangeById(id);
65
49
  }, []);
66
50
  return {
67
- atomChanges,
68
- filteredChanges,
69
- filter,
70
- setFilter,
51
+ atomChanges: log.items,
52
+ filteredChanges: log.filteredItems,
53
+ filter: log.filter,
54
+ setFilter: log.setFilter,
71
55
  stats,
72
56
  atoms,
73
- clearChanges,
74
- isEnabled,
75
- toggleCapture,
57
+ clearChanges: log.clear,
58
+ isEnabled: log.isEnabled,
59
+ toggleCapture: log.toggleCapture,
76
60
  atomLabels,
77
61
  getChangeById
78
62
  };
@@ -1,43 +1,28 @@
1
1
  "use strict";
2
2
 
3
3
  import { jotaiStateStore } from "../utils/jotaiStateStore";
4
- import { isOverWireBudget } from "@buoy-gg/shared-ui";
4
+ import { WIRE_DETAIL_LIMIT_BYTES, createSyncAdapter, capDetail, detailByIdAction, memoByIdentity, onDeviceMarker, readParams, toWire } from "@buoy-gg/shared-ui";
5
5
 
6
6
  /**
7
7
  * Sentinel used in WIRE-FORM snapshots in place of raw prevValue/nextValue.
8
8
  * Same hazard as redux/zustand: up to 200 changes × 2 values on every
9
9
  * snapshot. Dashboards fetch values via `getChangeDetail`.
10
10
  */
11
- export const VALUE_ON_DEVICE = Object.freeze({
12
- __buoyValueOnDevice: true,
13
- note: "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue."
14
- });
15
- const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
16
- const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
11
+ export const VALUE_ON_DEVICE = onDeviceMarker("Value", "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue.");
17
12
  /**
18
13
  * Per-change wire cache — the store prepends and never mutates a recorded
19
14
  * change, so identity is a correct, self-invalidating key.
20
15
  */
21
- const changeCache = new WeakMap();
22
- function toWireChange(change) {
23
- const cached = changeCache.get(change);
24
- if (cached) return cached;
25
- const wire = {
26
- ...change,
27
- prevValue: VALUE_ON_DEVICE,
28
- nextValue: VALUE_ON_DEVICE
29
- };
30
- changeCache.set(change, wire);
31
- return wire;
32
- }
33
- function toWireValue(value) {
34
- return isOverWireBudget(value, MAX_WIRE_PAYLOAD_BYTES) ? VALUE_ON_DEVICE : value;
35
- }
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);
36
22
 
37
23
  /**
38
24
  * Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
39
- * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
40
- * this package doesn't need a dependency on it).
25
+ * `useExternalSync`.
41
26
  *
42
27
  * Atom changes are captured by watchAtoms(), independent of whether a
43
28
  * dashboard is watching — subscribing here only streams what the store
@@ -49,7 +34,7 @@ function toWireValue(value) {
49
34
  * Dashboards fetch values via `getChangeDetail`. Oversized / unserializable
50
35
  * `atoms[].currentValue` uses the same marker and is fetched via `getAtomValue`.
51
36
  */
52
- export const jotaiSyncAdapter = {
37
+ export const jotaiSyncAdapter = createSyncAdapter({
53
38
  version: 2,
54
39
  getSnapshot: () => ({
55
40
  changes: jotaiStateStore.getAtomChanges().map(toWireChange),
@@ -58,14 +43,7 @@ export const jotaiSyncAdapter = {
58
43
  currentValue: toWireValue(atom.currentValue)
59
44
  }))
60
45
  }),
61
- subscribe: onChange => {
62
- const unsubscribeChanges = jotaiStateStore.subscribe(onChange);
63
- const unsubscribeAtoms = jotaiStateStore.subscribeToAtoms(onChange);
64
- return () => {
65
- unsubscribeChanges();
66
- unsubscribeAtoms();
67
- };
68
- },
46
+ subscribe: [onChange => jotaiStateStore.subscribe(onChange), onChange => jotaiStateStore.subscribeToAtoms(onChange)],
69
47
  actions: {
70
48
  clearEvents: () => {
71
49
  jotaiStateStore.clearAtomChanges();
@@ -75,31 +53,10 @@ export const jotaiSyncAdapter = {
75
53
  * The per-snapshot stream stays value-free; this is the explicit,
76
54
  * size-guarded channel for the detail pane.
77
55
  */
78
- getChangeDetail: params => {
79
- const {
80
- id
81
- } = params ?? {};
82
- if (!id) return {
83
- found: false,
84
- reason: "missing id"
85
- };
86
- const change = jotaiStateStore.getAtomChangeById(id);
87
- if (!change) return {
88
- found: false,
89
- reason: "unknown id"
90
- };
91
- const cappedNote = {
92
- __buoyTruncated: true,
93
- note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
94
- };
95
- const cap = value => isOverWireBudget(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
96
- return {
97
- found: true,
98
- id,
99
- prevValue: cap(change.prevValue),
100
- nextValue: cap(change.nextValue)
101
- };
102
- },
56
+ getChangeDetail: detailByIdAction(id => jotaiStateStore.getAtomChangeById(id), (change, cap) => ({
57
+ prevValue: cap(change.prevValue),
58
+ nextValue: cap(change.nextValue)
59
+ })),
103
60
  /**
104
61
  * On-demand current value for one atom. Wire-form snapshots replace
105
62
  * oversized / unserializable `currentValue` with VALUE_ON_DEVICE.
@@ -107,7 +64,7 @@ export const jotaiSyncAdapter = {
107
64
  getAtomValue: params => {
108
65
  const {
109
66
  label
110
- } = params ?? {};
67
+ } = readParams(params);
111
68
  if (!label) return {
112
69
  found: false,
113
70
  reason: "missing label"
@@ -123,10 +80,6 @@ export const jotaiSyncAdapter = {
123
80
  } catch {
124
81
  currentValue = undefined;
125
82
  }
126
- const cappedNote = {
127
- __buoyTruncated: true,
128
- note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
129
- };
130
83
  try {
131
84
  JSON.stringify(currentValue);
132
85
  } catch {
@@ -139,7 +92,7 @@ export const jotaiSyncAdapter = {
139
92
  return {
140
93
  found: true,
141
94
  label,
142
- currentValue: isOverWireBudget(currentValue, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : currentValue
95
+ currentValue: capDetail(currentValue, WIRE_DETAIL_LIMIT_BYTES)
143
96
  };
144
97
  },
145
98
  /**
@@ -152,7 +105,7 @@ export const jotaiSyncAdapter = {
152
105
  const {
153
106
  includeValues = false,
154
107
  limit
155
- } = params ?? {};
108
+ } = readParams(params);
156
109
  const all = jotaiStateStore.getAtoms();
157
110
  const capped = typeof limit === "number" && limit > 0 ? all.slice(0, limit) : all;
158
111
  const atoms = capped.map(a => {
@@ -185,9 +138,9 @@ export const jotaiSyncAdapter = {
185
138
  const {
186
139
  label,
187
140
  value
188
- } = params ?? {};
141
+ } = readParams(params);
189
142
  if (!label) throw new Error("setAtom requires a `label`");
190
143
  jotaiStateStore.setAtomValue(label, value);
191
144
  }
192
145
  }
193
- };
146
+ });