@buoy-gg/jotai 7.0.34 → 7.0.36

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.
@@ -165,7 +165,7 @@ const styles = _reactNative.StyleSheet.create({
165
165
  absCountText: {
166
166
  fontSize: 9,
167
167
  fontWeight: "700",
168
- fontFamily: "monospace"
168
+ fontFamily: _sharedUi.monoFont
169
169
  },
170
170
  scrollContent: {
171
171
  paddingTop: 8,
@@ -194,6 +194,6 @@ const styles = _reactNative.StyleSheet.create({
194
194
  fontSize: 10,
195
195
  fontWeight: "700",
196
196
  color: _sharedUi.night.color.accent,
197
- fontFamily: "monospace"
197
+ fontFamily: _sharedUi.monoFont
198
198
  }
199
199
  });
@@ -481,7 +481,7 @@ const styles = _reactNative.StyleSheet.create({
481
481
  valueLabel: {
482
482
  fontSize: 10,
483
483
  color: _sharedUi.night.color.textSecondary,
484
- fontFamily: "monospace",
484
+ fontFamily: _sharedUi.monoFont,
485
485
  letterSpacing: 0.5,
486
486
  fontWeight: "600"
487
487
  },
@@ -498,7 +498,7 @@ const styles = _reactNative.StyleSheet.create({
498
498
  changeText: {
499
499
  fontSize: 9,
500
500
  fontWeight: "700",
501
- fontFamily: "monospace",
501
+ fontFamily: _sharedUi.monoFont,
502
502
  letterSpacing: 0.3
503
503
  },
504
504
  typeBadge: {
@@ -511,7 +511,7 @@ const styles = _reactNative.StyleSheet.create({
511
511
  fontSize: 9,
512
512
  fontWeight: "600",
513
513
  color: _sharedUi.night.color.textTertiary,
514
- fontFamily: "monospace"
514
+ fontFamily: _sharedUi.monoFont
515
515
  },
516
516
  valueBox: {
517
517
  // Inset code box — darker than the card, the night track color.
@@ -524,7 +524,7 @@ const styles = _reactNative.StyleSheet.create({
524
524
  valueText: {
525
525
  fontSize: 12,
526
526
  color: _sharedUi.night.color.text,
527
- fontFamily: "monospace",
527
+ fontFamily: _sharedUi.monoFont,
528
528
  lineHeight: 18
529
529
  },
530
530
  diffContainer: {
@@ -542,7 +542,7 @@ const styles = _reactNative.StyleSheet.create({
542
542
  changeTextSmall: {
543
543
  fontSize: 8,
544
544
  fontWeight: "700",
545
- fontFamily: "monospace"
545
+ fontFamily: _sharedUi.monoFont
546
546
  }
547
547
  });
548
548
  const infoStyles = _reactNative.StyleSheet.create({
@@ -588,7 +588,7 @@ const infoStyles = _reactNative.StyleSheet.create({
588
588
  fontWeight: "600",
589
589
  letterSpacing: 0.5,
590
590
  color: _sharedUi.night.color.accent,
591
- fontFamily: "monospace"
591
+ fontFamily: _sharedUi.monoFont
592
592
  },
593
593
  cardContent: {
594
594
  padding: 14,
@@ -613,7 +613,7 @@ const infoStyles = _reactNative.StyleSheet.create({
613
613
  marginLeft: 12
614
614
  },
615
615
  infoValueMono: {
616
- fontFamily: "monospace"
616
+ fontFamily: _sharedUi.monoFont
617
617
  },
618
618
  badgeRow: {
619
619
  flexDirection: "row",
@@ -673,7 +673,7 @@ const infoStyles = _reactNative.StyleSheet.create({
673
673
  fontSize: 11,
674
674
  fontWeight: "600",
675
675
  color: _sharedUi.night.color.accent,
676
- fontFamily: "monospace"
676
+ fontFamily: _sharedUi.monoFont
677
677
  },
678
678
  countBadge: {
679
679
  paddingHorizontal: 8,
@@ -685,7 +685,7 @@ const infoStyles = _reactNative.StyleSheet.create({
685
685
  fontSize: 9,
686
686
  fontWeight: "700",
687
687
  color: _sharedUi.night.color.accent,
688
- fontFamily: "monospace"
688
+ fontFamily: _sharedUi.monoFont
689
689
  },
690
690
  actionsRow: {
691
691
  flexDirection: "row",
@@ -704,7 +704,7 @@ const infoStyles = _reactNative.StyleSheet.create({
704
704
  actionButtonText: {
705
705
  fontSize: 10,
706
706
  fontWeight: "700",
707
- fontFamily: "monospace",
707
+ fontFamily: _sharedUi.monoFont,
708
708
  letterSpacing: 0.3
709
709
  },
710
710
  dataViewerContainer: {
@@ -364,7 +364,7 @@ const styles = _reactNative.StyleSheet.create({
364
364
  color: _sharedUi.night.color.accent,
365
365
  letterSpacing: 0.5,
366
366
  textTransform: "uppercase",
367
- fontFamily: "monospace"
367
+ fontFamily: _sharedUi.monoFont
368
368
  },
369
369
  rawButton: {
370
370
  flexDirection: "row",
@@ -411,7 +411,7 @@ const styles = _reactNative.StyleSheet.create({
411
411
  borderBottomColor: _sharedUi.night.color.border,
412
412
  letterSpacing: 0.5,
413
413
  textTransform: "uppercase",
414
- fontFamily: "monospace"
414
+ fontFamily: _sharedUi.monoFont
415
415
  },
416
416
  infoBody: {
417
417
  padding: 12,
@@ -424,13 +424,13 @@ const styles = _reactNative.StyleSheet.create({
424
424
  primitiveText: {
425
425
  color: _sharedUi.night.color.text,
426
426
  fontSize: 12,
427
- fontFamily: "monospace",
427
+ fontFamily: _sharedUi.monoFont,
428
428
  padding: 14
429
429
  },
430
430
  valueTypeText: {
431
431
  color: _sharedUi.night.color.textTertiary,
432
432
  fontSize: 10,
433
- fontFamily: "monospace",
433
+ fontFamily: _sharedUi.monoFont,
434
434
  marginLeft: 6
435
435
  },
436
436
  inlineLinkButton: {
@@ -439,6 +439,6 @@ const styles = _reactNative.StyleSheet.create({
439
439
  inlineLinkText: {
440
440
  fontSize: 10,
441
441
  fontWeight: "600",
442
- fontFamily: "monospace"
442
+ fontFamily: _sharedUi.monoFont
443
443
  }
444
444
  });
@@ -12,6 +12,41 @@ var _sharedUi = require("@buoy-gg/shared-ui");
12
12
  * snapshot. Dashboards fetch values via `getChangeDetail`.
13
13
  */
14
14
  const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE = (0, _sharedUi.onDeviceMarker)("Value", "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue.");
15
+ const isPlainObj = v => !!v && typeof v === "object" && !Array.isArray(v);
16
+
17
+ /**
18
+ * Merge a patch into an atom's value.
19
+ *
20
+ * An atom is not always an object — plenty hold a number, a string or a list —
21
+ * so a merge only means anything when BOTH sides are plain objects. Anything
22
+ * else replaces, which is what a caller asking to merge a scalar can only have
23
+ * meant. Arrays replace too: the id-addressed form has already been expanded to
24
+ * a whole array by the time this runs, so merging element-wise here would undo
25
+ * the deletion half of that expansion.
26
+ */
27
+ function deepMergeInto(base, patch) {
28
+ if (!isPlainObj(base) || !isPlainObj(patch)) return patch;
29
+ const out = {
30
+ ...base
31
+ };
32
+ for (const [k, v] of Object.entries(patch)) out[k] = k in base ? deepMergeInto(base[k], v) : v;
33
+ return out;
34
+ }
35
+
36
+ /**
37
+ * Does this patch write into the list at `path`? Only then is an
38
+ * unchecked-shape warning about that list worth saying.
39
+ */
40
+ function touchesList(patch, path) {
41
+ let cur = patch;
42
+ for (const seg of path.split(".")) {
43
+ if (!isPlainObj(cur)) return false;
44
+ if (!(seg in cur)) return false;
45
+ cur = cur[seg];
46
+ }
47
+ return Array.isArray(cur) ? cur.length > 0 : !!cur && typeof cur === "object";
48
+ }
49
+
15
50
  /**
16
51
  * Per-change wire cache — the store prepends and never mutates a recorded
17
52
  * change, so identity is a correct, self-invalidating key.
@@ -66,7 +101,8 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = (0, _sharedUi.createSyncAdap
66
101
  */
67
102
  getAtomValue: params => {
68
103
  const {
69
- label
104
+ label,
105
+ path
70
106
  } = (0, _sharedUi.readParams)(params);
71
107
  if (!label) return {
72
108
  found: false,
@@ -92,9 +128,32 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = (0, _sharedUi.createSyncAdap
92
128
  currentValue: VALUE_ON_DEVICE
93
129
  };
94
130
  }
131
+ // One slice instead of the whole atom, when asked. The reason to want it
132
+ // is the engine's 24,000-character result cap: a caller that never saw the
133
+ // field it means to edit is left inventing a shape.
134
+ if (path) {
135
+ const at = (0, _sharedUi.readPath)(currentValue, path);
136
+ if (!at.ok) return {
137
+ found: false,
138
+ reason: at.error
139
+ };
140
+ return {
141
+ found: true,
142
+ label,
143
+ path,
144
+ shape: (0, _sharedUi.shapeSummary)(at.value),
145
+ currentValue: at.value
146
+ };
147
+ }
148
+ // `shape` BEFORE `currentValue`: a result is truncated by slicing the
149
+ // encoded string, so what is serialised first is what survives the cut,
150
+ // and the sketch does not grow with the data.
95
151
  return {
96
152
  found: true,
97
153
  label,
154
+ ...((0, _sharedUi.shapeSummary)(currentValue) ? {
155
+ shape: (0, _sharedUi.shapeSummary)(currentValue)
156
+ } : {}),
98
157
  currentValue: (0, _sharedUi.capDetail)(currentValue, _sharedUi.WIRE_DETAIL_LIMIT_BYTES)
99
158
  };
100
159
  },
@@ -120,10 +179,22 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = (0, _sharedUi.createSyncAdap
120
179
  value = undefined;
121
180
  }
122
181
  }
182
+ // The compact view is what a caller reaches for precisely because it
183
+ // does not want the data; the item shape of a list is the one thing it
184
+ // cannot infer without it, and it costs a line.
185
+ let shape;
186
+ try {
187
+ shape = (0, _sharedUi.shapeSummary)(a.getValue());
188
+ } catch {
189
+ shape = undefined;
190
+ }
123
191
  return {
124
192
  label: a.label,
125
193
  changes: a.changeCount,
126
194
  writable: typeof a.setValue === "function",
195
+ ...(shape ? {
196
+ shape
197
+ } : {}),
127
198
  ...(includeValues ? {
128
199
  currentValue: value
129
200
  } : {})
@@ -136,14 +207,100 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = (0, _sharedUi.createSyncAdap
136
207
  includedValues: includeValues
137
208
  };
138
209
  },
139
- /** Write a value to a writable atom (throws for unknown/read-only atoms). */
210
+ /**
211
+ * Write to a writable atom (throws for unknown/read-only atoms).
212
+ *
213
+ * THIS USED TO BE FOUR LINES WITH NO GUARD. `setAtom` took whatever it was
214
+ * given and wrote it: a string over a number, `null` over a list the screen
215
+ * maps, a one-item array over a fifty-item one — every failure mode the
216
+ * zustand and react-query adapters refuse, jotai accepted silently. It was
217
+ * the only writable state tool with no protection at all, and it had never
218
+ * had an eval case, so nothing had ever measured it.
219
+ *
220
+ * It now takes the same three forms as `zustand.setState`, backed by the
221
+ * same shared implementations:
222
+ * { value } replace (guarded unless force)
223
+ * { value, merge: true } merge a patch, id-addressed lists allowed
224
+ * { path, value } set one value; no depth to get wrong
225
+ */
140
226
  setAtom: params => {
141
227
  const {
142
228
  label,
143
- value
229
+ value,
230
+ merge,
231
+ path,
232
+ force
144
233
  } = (0, _sharedUi.readParams)(params);
145
234
  if (!label) throw new Error("setAtom requires a `label`");
146
- _jotaiStateStore.jotaiStateStore.setAtomValue(label, value);
235
+ const atom = _jotaiStateStore.jotaiStateStore.getAtom(label);
236
+ let current;
237
+ try {
238
+ current = atom?.getValue();
239
+ } catch {
240
+ current = undefined;
241
+ }
242
+
243
+ // A replace with no guard is the old behaviour; keep it reachable via
244
+ // `force`, and keep it the ONLY way to get it.
245
+ let effective = value;
246
+ let keyedNotes = [];
247
+ let unchecked = [];
248
+ const merging = merge === true || path !== undefined;
249
+ if (path !== undefined) {
250
+ if (value === undefined) {
251
+ return {
252
+ ok: false,
253
+ error: `Refused: sent \`path\` without \`value\`. Say what to set "${path}" to.`
254
+ };
255
+ }
256
+ const built = (0, _sharedUi.patchFromPath)(current, path, value);
257
+ if (!built.ok) return {
258
+ ok: false,
259
+ error: `Refused: ${built.error}`
260
+ };
261
+ effective = built.patch;
262
+ }
263
+ if (!force && merging && current !== undefined) {
264
+ const expanded = (0, _sharedUi.expandKeyedArrays)(current, effective);
265
+ // See keyedArrays.ts: an added item whose id disagrees with the key it
266
+ // was sent under is ambiguous, and only this layer can say so.
267
+ if (expanded.error) {
268
+ return {
269
+ ok: false,
270
+ error: `Refused: ${expanded.error}`
271
+ };
272
+ }
273
+ effective = expanded.patch;
274
+ keyedNotes = expanded.notes;
275
+ unchecked = (0, _sharedUi.unshapedLists)(current).filter(p => touchesList(effective, p));
276
+ const violations = (0, _sharedUi.typedEditViolations)(current, effective, {
277
+ noun: "atom"
278
+ });
279
+ if (violations.length) {
280
+ const suggestion = (0, _sharedUi.suggestedPatch)(effective, violations);
281
+ return {
282
+ ok: false,
283
+ error: (0, _sharedUi.refusalText)(violations, "atom", suggestion),
284
+ violations,
285
+ ...(suggestion ? {
286
+ suggestedValue: suggestion
287
+ } : {})
288
+ };
289
+ }
290
+ }
291
+ _jotaiStateStore.jotaiStateStore.setAtomValue(label, merging ? deepMergeInto(current, effective) : effective);
292
+ return {
293
+ ok: true,
294
+ ...(path !== undefined ? {
295
+ path
296
+ } : {}),
297
+ ...(keyedNotes.length ? {
298
+ keyed: keyedNotes
299
+ } : {}),
300
+ ...(unchecked.length ? {
301
+ unchecked: unchecked.map(p => `\`${p}\` was empty, so nothing knows what its items look like and the shape of what you added was NOT checked.`)
302
+ } : {})
303
+ };
147
304
  }
148
305
  }
149
306
  });
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { useMemo } from "react";
13
13
  import { View, Text, StyleSheet, ScrollView } from "react-native";
14
- import { CompactRow, night, ToolEmptyState, resolveListEmptyState } from "@buoy-gg/shared-ui";
14
+ import { CompactRow, night, ToolEmptyState, resolveListEmptyState, monoFont } from "@buoy-gg/shared-ui";
15
15
  import { jotaiStateStore } from "../utils/jotaiStateStore";
16
16
  import { JOTAI_TEST_ID, atomRowTestID } from "../utils/testIDs";
17
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -161,7 +161,7 @@ const styles = StyleSheet.create({
161
161
  absCountText: {
162
162
  fontSize: 9,
163
163
  fontWeight: "700",
164
- fontFamily: "monospace"
164
+ fontFamily: monoFont
165
165
  },
166
166
  scrollContent: {
167
167
  paddingTop: 8,
@@ -190,6 +190,6 @@ const styles = StyleSheet.create({
190
190
  fontSize: 10,
191
191
  fontWeight: "700",
192
192
  color: night.color.accent,
193
- fontFamily: "monospace"
193
+ fontFamily: monoFont
194
194
  }
195
195
  });
@@ -13,7 +13,7 @@
13
13
 
14
14
  import React, { memo, useState, useCallback, useMemo } from "react";
15
15
  import { View, Text, StyleSheet, ScrollView } from "react-native";
16
- import { buoyColors, night, EventStepperFooter, formatRelativeTime, parseValue, Info, Zap, FileText, Database, GitBranch, copyToClipboard, ProUpgradeModal, DiffModeTabs, CompareBar, DetailViewToggle } from "@buoy-gg/shared-ui";
16
+ import { buoyColors, night, EventStepperFooter, formatRelativeTime, parseValue, Info, Zap, FileText, Database, GitBranch, copyToClipboard, ProUpgradeModal, DiffModeTabs, CompareBar, DetailViewToggle, monoFont } from "@buoy-gg/shared-ui";
17
17
  import { DataViewer, SplitDiffViewer, TreeDiffViewer, diffThemes } from "@buoy-gg/shared-ui/dataViewer";
18
18
  import { useIsPro } from "@buoy-gg/license";
19
19
  import { jotaiStateStore } from "../utils/jotaiStateStore";
@@ -476,7 +476,7 @@ const styles = StyleSheet.create({
476
476
  valueLabel: {
477
477
  fontSize: 10,
478
478
  color: night.color.textSecondary,
479
- fontFamily: "monospace",
479
+ fontFamily: monoFont,
480
480
  letterSpacing: 0.5,
481
481
  fontWeight: "600"
482
482
  },
@@ -493,7 +493,7 @@ const styles = StyleSheet.create({
493
493
  changeText: {
494
494
  fontSize: 9,
495
495
  fontWeight: "700",
496
- fontFamily: "monospace",
496
+ fontFamily: monoFont,
497
497
  letterSpacing: 0.3
498
498
  },
499
499
  typeBadge: {
@@ -506,7 +506,7 @@ const styles = StyleSheet.create({
506
506
  fontSize: 9,
507
507
  fontWeight: "600",
508
508
  color: night.color.textTertiary,
509
- fontFamily: "monospace"
509
+ fontFamily: monoFont
510
510
  },
511
511
  valueBox: {
512
512
  // Inset code box — darker than the card, the night track color.
@@ -519,7 +519,7 @@ const styles = StyleSheet.create({
519
519
  valueText: {
520
520
  fontSize: 12,
521
521
  color: night.color.text,
522
- fontFamily: "monospace",
522
+ fontFamily: monoFont,
523
523
  lineHeight: 18
524
524
  },
525
525
  diffContainer: {
@@ -537,7 +537,7 @@ const styles = StyleSheet.create({
537
537
  changeTextSmall: {
538
538
  fontSize: 8,
539
539
  fontWeight: "700",
540
- fontFamily: "monospace"
540
+ fontFamily: monoFont
541
541
  }
542
542
  });
543
543
  const infoStyles = StyleSheet.create({
@@ -583,7 +583,7 @@ const infoStyles = StyleSheet.create({
583
583
  fontWeight: "600",
584
584
  letterSpacing: 0.5,
585
585
  color: night.color.accent,
586
- fontFamily: "monospace"
586
+ fontFamily: monoFont
587
587
  },
588
588
  cardContent: {
589
589
  padding: 14,
@@ -608,7 +608,7 @@ const infoStyles = StyleSheet.create({
608
608
  marginLeft: 12
609
609
  },
610
610
  infoValueMono: {
611
- fontFamily: "monospace"
611
+ fontFamily: monoFont
612
612
  },
613
613
  badgeRow: {
614
614
  flexDirection: "row",
@@ -668,7 +668,7 @@ const infoStyles = StyleSheet.create({
668
668
  fontSize: 11,
669
669
  fontWeight: "600",
670
670
  color: night.color.accent,
671
- fontFamily: "monospace"
671
+ fontFamily: monoFont
672
672
  },
673
673
  countBadge: {
674
674
  paddingHorizontal: 8,
@@ -680,7 +680,7 @@ const infoStyles = StyleSheet.create({
680
680
  fontSize: 9,
681
681
  fontWeight: "700",
682
682
  color: night.color.accent,
683
- fontFamily: "monospace"
683
+ fontFamily: monoFont
684
684
  },
685
685
  actionsRow: {
686
686
  flexDirection: "row",
@@ -699,7 +699,7 @@ const infoStyles = StyleSheet.create({
699
699
  actionButtonText: {
700
700
  fontSize: 10,
701
701
  fontWeight: "700",
702
- fontFamily: "monospace",
702
+ fontFamily: monoFont,
703
703
  letterSpacing: 0.3
704
704
  },
705
705
  dataViewerContainer: {
@@ -34,7 +34,7 @@
34
34
 
35
35
  import { useCallback, useEffect, useMemo, useState } from "react";
36
36
  import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
37
- import { buoyColors, ExpandedInfoRow, FileJson, night, PillBadge, parseValue } from "@buoy-gg/shared-ui";
37
+ import { buoyColors, ExpandedInfoRow, FileJson, night, PillBadge, parseValue, monoFont } from "@buoy-gg/shared-ui";
38
38
  import { DataViewer, LiveExplorer, ValueEditModal, updateNestedDataByPath, deleteNestedDataByPath } from "@buoy-gg/shared-ui/dataViewer";
39
39
  import { jotaiStateStore } from "../utils/jotaiStateStore";
40
40
  import { JOTAI_TEST_ID, atomRowTestID } from "../utils/testIDs";
@@ -362,7 +362,7 @@ const styles = StyleSheet.create({
362
362
  color: night.color.accent,
363
363
  letterSpacing: 0.5,
364
364
  textTransform: "uppercase",
365
- fontFamily: "monospace"
365
+ fontFamily: monoFont
366
366
  },
367
367
  rawButton: {
368
368
  flexDirection: "row",
@@ -409,7 +409,7 @@ const styles = StyleSheet.create({
409
409
  borderBottomColor: night.color.border,
410
410
  letterSpacing: 0.5,
411
411
  textTransform: "uppercase",
412
- fontFamily: "monospace"
412
+ fontFamily: monoFont
413
413
  },
414
414
  infoBody: {
415
415
  padding: 12,
@@ -422,13 +422,13 @@ const styles = StyleSheet.create({
422
422
  primitiveText: {
423
423
  color: night.color.text,
424
424
  fontSize: 12,
425
- fontFamily: "monospace",
425
+ fontFamily: monoFont,
426
426
  padding: 14
427
427
  },
428
428
  valueTypeText: {
429
429
  color: night.color.textTertiary,
430
430
  fontSize: 10,
431
- fontFamily: "monospace",
431
+ fontFamily: monoFont,
432
432
  marginLeft: 6
433
433
  },
434
434
  inlineLinkButton: {
@@ -437,6 +437,6 @@ const styles = StyleSheet.create({
437
437
  inlineLinkText: {
438
438
  fontSize: 10,
439
439
  fontWeight: "600",
440
- fontFamily: "monospace"
440
+ fontFamily: monoFont
441
441
  }
442
442
  });
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import { jotaiStateStore } from "../utils/jotaiStateStore";
4
- import { WIRE_DETAIL_LIMIT_BYTES, createSyncAdapter, capDetail, detailByIdAction, memoByIdentity, onDeviceMarker, readParams, toWire } from "@buoy-gg/shared-ui";
4
+ 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";
5
5
 
6
6
  /**
7
7
  * Sentinel used in WIRE-FORM snapshots in place of raw prevValue/nextValue.
@@ -9,6 +9,41 @@ import { WIRE_DETAIL_LIMIT_BYTES, createSyncAdapter, capDetail, detailByIdAction
9
9
  * snapshot. Dashboards fetch values via `getChangeDetail`.
10
10
  */
11
11
  export const VALUE_ON_DEVICE = onDeviceMarker("Value", "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue.");
12
+ const isPlainObj = v => !!v && typeof v === "object" && !Array.isArray(v);
13
+
14
+ /**
15
+ * Merge a patch into an atom's value.
16
+ *
17
+ * An atom is not always an object — plenty hold a number, a string or a list —
18
+ * so a merge only means anything when BOTH sides are plain objects. Anything
19
+ * else replaces, which is what a caller asking to merge a scalar can only have
20
+ * meant. Arrays replace too: the id-addressed form has already been expanded to
21
+ * a whole array by the time this runs, so merging element-wise here would undo
22
+ * the deletion half of that expansion.
23
+ */
24
+ function deepMergeInto(base, patch) {
25
+ if (!isPlainObj(base) || !isPlainObj(patch)) return patch;
26
+ const out = {
27
+ ...base
28
+ };
29
+ for (const [k, v] of Object.entries(patch)) out[k] = k in base ? deepMergeInto(base[k], v) : v;
30
+ return out;
31
+ }
32
+
33
+ /**
34
+ * Does this patch write into the list at `path`? Only then is an
35
+ * unchecked-shape warning about that list worth saying.
36
+ */
37
+ function touchesList(patch, path) {
38
+ let cur = patch;
39
+ for (const seg of path.split(".")) {
40
+ if (!isPlainObj(cur)) return false;
41
+ if (!(seg in cur)) return false;
42
+ cur = cur[seg];
43
+ }
44
+ return Array.isArray(cur) ? cur.length > 0 : !!cur && typeof cur === "object";
45
+ }
46
+
12
47
  /**
13
48
  * Per-change wire cache — the store prepends and never mutates a recorded
14
49
  * change, so identity is a correct, self-invalidating key.
@@ -63,7 +98,8 @@ export const jotaiSyncAdapter = createSyncAdapter({
63
98
  */
64
99
  getAtomValue: params => {
65
100
  const {
66
- label
101
+ label,
102
+ path
67
103
  } = readParams(params);
68
104
  if (!label) return {
69
105
  found: false,
@@ -89,9 +125,32 @@ export const jotaiSyncAdapter = createSyncAdapter({
89
125
  currentValue: VALUE_ON_DEVICE
90
126
  };
91
127
  }
128
+ // One slice instead of the whole atom, when asked. The reason to want it
129
+ // is the engine's 24,000-character result cap: a caller that never saw the
130
+ // field it means to edit is left inventing a shape.
131
+ if (path) {
132
+ const at = readPath(currentValue, path);
133
+ if (!at.ok) return {
134
+ found: false,
135
+ reason: at.error
136
+ };
137
+ return {
138
+ found: true,
139
+ label,
140
+ path,
141
+ shape: shapeSummary(at.value),
142
+ currentValue: at.value
143
+ };
144
+ }
145
+ // `shape` BEFORE `currentValue`: a result is truncated by slicing the
146
+ // encoded string, so what is serialised first is what survives the cut,
147
+ // and the sketch does not grow with the data.
92
148
  return {
93
149
  found: true,
94
150
  label,
151
+ ...(shapeSummary(currentValue) ? {
152
+ shape: shapeSummary(currentValue)
153
+ } : {}),
95
154
  currentValue: capDetail(currentValue, WIRE_DETAIL_LIMIT_BYTES)
96
155
  };
97
156
  },
@@ -117,10 +176,22 @@ export const jotaiSyncAdapter = createSyncAdapter({
117
176
  value = undefined;
118
177
  }
119
178
  }
179
+ // The compact view is what a caller reaches for precisely because it
180
+ // does not want the data; the item shape of a list is the one thing it
181
+ // cannot infer without it, and it costs a line.
182
+ let shape;
183
+ try {
184
+ shape = shapeSummary(a.getValue());
185
+ } catch {
186
+ shape = undefined;
187
+ }
120
188
  return {
121
189
  label: a.label,
122
190
  changes: a.changeCount,
123
191
  writable: typeof a.setValue === "function",
192
+ ...(shape ? {
193
+ shape
194
+ } : {}),
124
195
  ...(includeValues ? {
125
196
  currentValue: value
126
197
  } : {})
@@ -133,14 +204,100 @@ export const jotaiSyncAdapter = createSyncAdapter({
133
204
  includedValues: includeValues
134
205
  };
135
206
  },
136
- /** Write a value to a writable atom (throws for unknown/read-only atoms). */
207
+ /**
208
+ * Write to a writable atom (throws for unknown/read-only atoms).
209
+ *
210
+ * THIS USED TO BE FOUR LINES WITH NO GUARD. `setAtom` took whatever it was
211
+ * given and wrote it: a string over a number, `null` over a list the screen
212
+ * maps, a one-item array over a fifty-item one — every failure mode the
213
+ * zustand and react-query adapters refuse, jotai accepted silently. It was
214
+ * the only writable state tool with no protection at all, and it had never
215
+ * had an eval case, so nothing had ever measured it.
216
+ *
217
+ * It now takes the same three forms as `zustand.setState`, backed by the
218
+ * same shared implementations:
219
+ * { value } replace (guarded unless force)
220
+ * { value, merge: true } merge a patch, id-addressed lists allowed
221
+ * { path, value } set one value; no depth to get wrong
222
+ */
137
223
  setAtom: params => {
138
224
  const {
139
225
  label,
140
- value
226
+ value,
227
+ merge,
228
+ path,
229
+ force
141
230
  } = readParams(params);
142
231
  if (!label) throw new Error("setAtom requires a `label`");
143
- jotaiStateStore.setAtomValue(label, value);
232
+ const atom = jotaiStateStore.getAtom(label);
233
+ let current;
234
+ try {
235
+ current = atom?.getValue();
236
+ } catch {
237
+ current = undefined;
238
+ }
239
+
240
+ // A replace with no guard is the old behaviour; keep it reachable via
241
+ // `force`, and keep it the ONLY way to get it.
242
+ let effective = value;
243
+ let keyedNotes = [];
244
+ let unchecked = [];
245
+ const merging = merge === true || path !== undefined;
246
+ if (path !== undefined) {
247
+ if (value === undefined) {
248
+ return {
249
+ ok: false,
250
+ error: `Refused: sent \`path\` without \`value\`. Say what to set "${path}" to.`
251
+ };
252
+ }
253
+ const built = patchFromPath(current, path, value);
254
+ if (!built.ok) return {
255
+ ok: false,
256
+ error: `Refused: ${built.error}`
257
+ };
258
+ effective = built.patch;
259
+ }
260
+ if (!force && merging && current !== undefined) {
261
+ const expanded = expandKeyedArrays(current, effective);
262
+ // See keyedArrays.ts: an added item whose id disagrees with the key it
263
+ // was sent under is ambiguous, and only this layer can say so.
264
+ if (expanded.error) {
265
+ return {
266
+ ok: false,
267
+ error: `Refused: ${expanded.error}`
268
+ };
269
+ }
270
+ effective = expanded.patch;
271
+ keyedNotes = expanded.notes;
272
+ unchecked = unshapedLists(current).filter(p => touchesList(effective, p));
273
+ const violations = typedEditViolations(current, effective, {
274
+ noun: "atom"
275
+ });
276
+ if (violations.length) {
277
+ const suggestion = suggestedPatch(effective, violations);
278
+ return {
279
+ ok: false,
280
+ error: refusalText(violations, "atom", suggestion),
281
+ violations,
282
+ ...(suggestion ? {
283
+ suggestedValue: suggestion
284
+ } : {})
285
+ };
286
+ }
287
+ }
288
+ jotaiStateStore.setAtomValue(label, merging ? deepMergeInto(current, effective) : effective);
289
+ return {
290
+ ok: true,
291
+ ...(path !== undefined ? {
292
+ path
293
+ } : {}),
294
+ ...(keyedNotes.length ? {
295
+ keyed: keyedNotes
296
+ } : {}),
297
+ ...(unchecked.length ? {
298
+ unchecked: unchecked.map(p => `\`${p}\` was empty, so nothing knows what its items look like and the shape of what you added was NOT checked.`)
299
+ } : {})
300
+ };
144
301
  }
145
302
  }
146
303
  });
@@ -1 +1 @@
1
- {"version":3,"file":"JotaiAtomBrowser.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomBrowser.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAI9C,UAAU,qBAAqB;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAuDD,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,UAAU,GACX,EAAE,qBAAqB,+BA4FvB"}
1
+ {"version":3,"file":"JotaiAtomBrowser.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomBrowser.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAI9C,UAAU,qBAAqB;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAuDD,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,UAAU,GACX,EAAE,qBAAqB,+BA4FvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"JotaiAtomDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AA4BpE,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAwOlB,UAAU,2BAA2B;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,EACb,qBAA6B,EAC7B,WAAsB,GACvB,EAAE,2BAA2B,qBAkL7B;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,4BAcA"}
1
+ {"version":3,"file":"JotaiAtomDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AA6BpE,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAwOlB,UAAU,2BAA2B;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,EACb,qBAA6B,EAC7B,WAAsB,GACvB,EAAE,2BAA2B,qBAkL7B;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,4BAcA"}
@@ -1 +1 @@
1
- {"version":3,"file":"JotaiAtomValueDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomValueDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AA0BH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAoE9C,UAAU,gCAAgC;IACxC,IAAI,EAAE,aAAa,CAAC;IACpB,kEAAkE;IAClE,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,wBAAgB,2BAA2B,CAAC,EAC1C,IAAI,EACJ,aAAa,GACd,EAAE,gCAAgC,+BA6OlC"}
1
+ {"version":3,"file":"JotaiAtomValueDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomValueDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AA2BH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAoE9C,UAAU,gCAAgC;IACxC,IAAI,EAAE,aAAa,CAAC;IACpB,kEAAkE;IAClE,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,wBAAgB,2BAA2B,CAAC,EAC1C,IAAI,EACJ,aAAa,GACd,EAAE,gCAAgC,+BA6OlC"}
@@ -51,12 +51,29 @@ export declare const jotaiSyncAdapter: import("@buoy-gg/shared-ui").SyncAdapter<
51
51
  found: false;
52
52
  reason: string;
53
53
  label?: undefined;
54
+ path?: undefined;
55
+ shape?: undefined;
54
56
  currentValue?: undefined;
55
57
  } | {
56
58
  found: true;
57
59
  label: string;
60
+ path: string;
61
+ shape: {
62
+ shape: string;
63
+ lists?: import("@buoy-gg/shared-ui").ListShape[];
64
+ } | undefined;
58
65
  currentValue: unknown;
59
66
  reason?: undefined;
67
+ } | {
68
+ currentValue: unknown;
69
+ shape?: {
70
+ shape: string;
71
+ lists?: import("@buoy-gg/shared-ui").ListShape[];
72
+ } | undefined;
73
+ found: true;
74
+ label: string;
75
+ reason?: undefined;
76
+ path?: undefined;
60
77
  };
61
78
  /**
62
79
  * Compact current-value reader for a remote driver (MCP/LLM): registered
@@ -67,6 +84,10 @@ export declare const jotaiSyncAdapter: import("@buoy-gg/shared-ui").SyncAdapter<
67
84
  listAtoms: (params?: unknown) => {
68
85
  atoms: {
69
86
  currentValue?: unknown;
87
+ shape?: {
88
+ shape: string;
89
+ lists?: import("@buoy-gg/shared-ui").ListShape[];
90
+ } | undefined;
70
91
  label: string;
71
92
  changes: number;
72
93
  writable: boolean;
@@ -75,7 +96,36 @@ export declare const jotaiSyncAdapter: import("@buoy-gg/shared-ui").SyncAdapter<
75
96
  returned: number;
76
97
  includedValues: boolean;
77
98
  };
78
- /** Write a value to a writable atom (throws for unknown/read-only atoms). */
79
- setAtom: (params?: unknown) => void;
99
+ /**
100
+ * Write to a writable atom (throws for unknown/read-only atoms).
101
+ *
102
+ * THIS USED TO BE FOUR LINES WITH NO GUARD. `setAtom` took whatever it was
103
+ * given and wrote it: a string over a number, `null` over a list the screen
104
+ * maps, a one-item array over a fifty-item one — every failure mode the
105
+ * zustand and react-query adapters refuse, jotai accepted silently. It was
106
+ * the only writable state tool with no protection at all, and it had never
107
+ * had an eval case, so nothing had ever measured it.
108
+ *
109
+ * It now takes the same three forms as `zustand.setState`, backed by the
110
+ * same shared implementations:
111
+ * { value } replace (guarded unless force)
112
+ * { value, merge: true } merge a patch, id-addressed lists allowed
113
+ * { path, value } set one value; no depth to get wrong
114
+ */
115
+ setAtom: (params?: unknown) => {
116
+ ok: false;
117
+ error: string;
118
+ } | {
119
+ suggestedValue?: Record<string, unknown> | undefined;
120
+ ok: false;
121
+ error: string;
122
+ violations: import("@buoy-gg/shared-ui").EditViolation[];
123
+ } | {
124
+ unchecked?: string[] | undefined;
125
+ keyed?: string[] | undefined;
126
+ path?: string | undefined;
127
+ ok: true;
128
+ error?: undefined;
129
+ };
80
130
  }>;
81
131
  //# sourceMappingURL=jotaiSyncAdapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jotaiSyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/jotai/sync/jotaiSyncAdapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAahD;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;;EAG3B,CAAC;AA4BF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;IAiBzB;;;;OAIG;;;;;IAQH;;;OAGG;4BACqB,OAAO;;;;;;;;;;;IA0B/B;;;;;OAKG;yBACkB,OAAO;;;;;;;;;;;IA4B5B,6EAA6E;uBAC1D,OAAO;EAMF,CAAC"}
1
+ {"version":3,"file":"jotaiSyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/jotai/sync/jotaiSyncAdapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAqBhD;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;;EAG3B,CAAC;AAmFF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;IAiBzB;;;;OAIG;;;;;IAQH;;;OAGG;4BACqB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAsC/B;;;;;OAKG;yBACkB,OAAO;;;;;;;;;;;;;;;IAsC5B;;;;;;;;;;;;;;;OAeG;uBACgB,OAAO;;;;;;;;;;;;;;;EAiEF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/jotai",
3
- "version": "7.0.34",
3
+ "version": "7.0.36",
4
4
  "description": "Jotai devtools inside your React Native app — every atom write shows prev → next, with per-atom history & live values. Part of Buoy devtools.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -29,8 +29,8 @@
29
29
  "**/snapshotProvider.ts"
30
30
  ],
31
31
  "dependencies": {
32
- "@buoy-gg/shared-ui": "^7.0.34",
33
- "@buoy-gg/license": "^7.0.34"
32
+ "@buoy-gg/license": "^7.0.36",
33
+ "@buoy-gg/shared-ui": "^7.0.36"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "jotai": ">=2.0.0",