@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.
Files changed (32) hide show
  1. package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +7 -52
  2. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +8 -87
  3. package/lib/commonjs/jotai/components/JotaiEventFilterView.js +21 -299
  4. package/lib/commonjs/jotai/components/JotaiModal.js +20 -85
  5. package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +33 -49
  6. package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +19 -66
  7. package/lib/commonjs/jotai/utils/jotaiStateStore.js +23 -188
  8. package/lib/commonjs/jotai/utils/snapshotProvider.js +7 -25
  9. package/lib/commonjs/preset.js +14 -22
  10. package/lib/module/jotai/components/JotaiAtomChangeItem.js +9 -54
  11. package/lib/module/jotai/components/JotaiAtomDetailContent.js +9 -88
  12. package/lib/module/jotai/components/JotaiEventFilterView.js +24 -302
  13. package/lib/module/jotai/components/JotaiModal.js +22 -87
  14. package/lib/module/jotai/hooks/useJotaiAtomChanges.js +34 -50
  15. package/lib/module/jotai/sync/jotaiSyncAdapter.js +20 -67
  16. package/lib/module/jotai/utils/jotaiStateStore.js +23 -188
  17. package/lib/module/jotai/utils/snapshotProvider.js +8 -26
  18. package/lib/module/preset.js +13 -20
  19. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -1
  20. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
  21. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts +2 -2
  22. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -1
  23. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
  24. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts +2 -1
  25. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +1 -1
  26. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +54 -68
  27. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
  28. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +2 -25
  29. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
  30. package/lib/typescript/preset.d.ts +2 -8
  31. package/lib/typescript/preset.d.ts.map +1 -1
  32. 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, ToolEmptyState, resolveListEmptyState, 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";
@@ -20,17 +20,6 @@ import { JotaiAtomValueDetailContent } from "./JotaiAtomValueDetailContent";
20
20
  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
- /**
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.
31
- */
32
- const CLIP_OFFSCREEN_CELLS = !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E);
33
-
34
23
  /**
35
24
  * The change log's "nothing here". Everything but the nouns is the shared
36
25
  * precedence — see `ToolEmptyState` in shared-ui for why it lives there.
@@ -353,49 +342,24 @@ export function JotaiModal({
353
342
  onBack: onBack
354
343
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
355
344
  title: "",
356
- children: isSearchActive ? /*#__PURE__*/_jsxs(View, {
357
- style: styles.headerSearchContainer,
358
- testID: `${JOTAI_TEST_ID}-search`,
359
- children: [/*#__PURE__*/_jsx(Search, {
360
- size: 14,
361
- color: night.color.textSecondary
362
- }), /*#__PURE__*/_jsx(TextInput, {
363
- ref: searchInputRef,
364
- testID: `${JOTAI_TEST_ID}-search-field`,
365
- style: styles.headerSearchInput,
366
- placeholder: activeTab === "atoms" ? "Search atoms..." : "Search events...",
367
- placeholderTextColor: night.color.placeholder,
368
- value: searchText,
369
- onChangeText: handleSearch,
370
- onSubmitEditing: () => searchInputRef.current?.blur()
371
- // Stay open while the query is still narrowing the list.
372
- //
373
- // Collapsing on blur hid the reason rows were missing: the
374
- // header went back to its TABS — whose counts are of the
375
- // WHOLE capture — while the list stayed filtered, so the tool
376
- // showed "Atoms (8)" above a single row and offered nothing to
377
- // explain the gap. The only way back was to reopen a field the
378
- // user had no reason to think was still armed. Same shape, and
379
- // the same fix, as the Redux, Events and Zustand tools.
380
- ,
381
- onBlur: () => {
382
- if (searchText.length === 0) setIsSearchActive(false);
383
- },
384
- accessibilityLabel: "Search Jotai atoms and events",
385
- autoCapitalize: "none",
386
- autoCorrect: false,
387
- returnKeyType: "search"
388
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
389
- onPress: closeSearch,
390
- style: styles.headerSearchClear,
391
- testID: `${JOTAI_TEST_ID}-search-clear`,
392
- accessibilityRole: "button",
393
- accessibilityLabel: "Close search",
394
- children: /*#__PURE__*/_jsx(X, {
395
- size: 14,
396
- color: night.color.textSecondary
397
- })
398
- })]
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
+ }
399
363
  }) : /*#__PURE__*/_jsx(TabSelector, {
400
364
  tabs: [{
401
365
  key: "atoms",
@@ -485,11 +449,7 @@ export function JotaiModal({
485
449
  keyExtractor: keyExtractor,
486
450
  contentContainerStyle: styles.listContent,
487
451
  showsVerticalScrollIndicator: true,
488
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
489
- initialNumToRender: 12,
490
- maxToRenderPerBatch: 8,
491
- windowSize: 5,
492
- updateCellsBatchingPeriod: 50
452
+ ...EVENT_LIST_PRESET
493
453
  }) : /*#__PURE__*/_jsx(ToolEmptyState, {
494
454
  testID: `${JOTAI_TEST_ID}-history-empty`,
495
455
  ...resolveListEmptyState({
@@ -578,11 +538,7 @@ export function JotaiModal({
578
538
  keyExtractor: keyExtractor,
579
539
  contentContainerStyle: styles.listContent,
580
540
  showsVerticalScrollIndicator: true,
581
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
582
- initialNumToRender: 12,
583
- maxToRenderPerBatch: 8,
584
- windowSize: 5,
585
- updateCellsBatchingPeriod: 50
541
+ ...EVENT_LIST_PRESET
586
542
  }) : /*#__PURE__*/_jsx(EventsEmptyState, {
587
543
  isEnabled: isEnabled,
588
544
  searchText: searchText,
@@ -696,27 +652,6 @@ const styles = StyleSheet.create({
696
652
  // Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
697
653
  backgroundColor: "transparent"
698
654
  },
699
- headerSearchContainer: {
700
- flexDirection: "row",
701
- alignItems: "center",
702
- backgroundColor: night.color.surfaceElevated,
703
- borderRadius: night.radius.row,
704
- borderWidth: 1,
705
- borderColor: night.color.border,
706
- paddingHorizontal: 12,
707
- paddingVertical: 5
708
- },
709
- headerSearchInput: {
710
- flex: 1,
711
- color: night.color.text,
712
- fontSize: 13,
713
- marginLeft: 6,
714
- paddingVertical: 2
715
- },
716
- headerSearchClear: {
717
- marginLeft: 6,
718
- padding: 4
719
- },
720
655
  headerActionButton: {
721
656
  width: 32,
722
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
+ });