@buoy-gg/zustand 7.0.15 → 7.0.17

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.
@@ -93,6 +93,7 @@ Object.defineProperty(exports, "zustandToolPreset", {
93
93
  return _preset.zustandToolPreset;
94
94
  }
95
95
  });
96
+ require("./zustand/utils/snapshotProvider");
96
97
  var _preset = require("./preset");
97
98
  var _buoyZustandMiddleware = require("./zustand/utils/buoyZustandMiddleware");
98
99
  var _useZustandStateChanges = require("./zustand/hooks/useZustandStateChanges");
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+
3
+ var _sharedUi = require("@buoy-gg/shared-ui");
4
+ var _zustandStateStore = require("./zustandStateStore");
5
+ /**
6
+ * Time Machine snapshot provider for Zustand.
7
+ *
8
+ * Registered into @buoy-gg/shared-ui's stateSnapshotRegistry at module init;
9
+ * consumed by @buoy-gg/time-machine. Neither package imports the other.
10
+ *
11
+ * The Zustand-specific hazard: stores conventionally keep their action
12
+ * functions IN state (`increment: () => ...`). Functions don't survive
13
+ * serialization, and a full `setState(snapshot, true)` replace would wipe
14
+ * them, breaking every `useStore(s => s.increment)` call site. So capture
15
+ * strips functions (recording where they were), and restore re-grafts the
16
+ * LIVE store's functions onto the snapshot before replacing.
17
+ */
18
+
19
+ const MAX_DEPTH = 8;
20
+ function isPlainObjectLike(v) {
21
+ return typeof v === "object" && v !== null && !Array.isArray(v);
22
+ }
23
+
24
+ /** Clone `value` without functions, recording their paths. */
25
+ function stripFunctions(value, path, fnPaths, seen, depth) {
26
+ if (typeof value === "function") {
27
+ fnPaths.push(path);
28
+ return undefined;
29
+ }
30
+ if (typeof value !== "object" || value === null || depth >= MAX_DEPTH) {
31
+ return value;
32
+ }
33
+ if (seen.has(value)) return undefined; // cycle — drop the back-reference
34
+ seen.add(value);
35
+ if (Array.isArray(value)) {
36
+ return value.map((item, i) => typeof item === "function" ? (fnPaths.push(`${path}[${i}]`), null) : stripFunctions(item, `${path}[${i}]`, fnPaths, seen, depth + 1));
37
+ }
38
+ if (!isPlainObjectLike(value)) return value; // Date/Map/Set/etc. — superjson handles
39
+ const out = {};
40
+ for (const [k, v] of Object.entries(value)) {
41
+ if (typeof v === "function") {
42
+ fnPaths.push(path ? `${path}.${k}` : k);
43
+ continue;
44
+ }
45
+ out[k] = stripFunctions(v, path ? `${path}.${k}` : k, fnPaths, seen, depth + 1);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ /**
51
+ * Copy every function-valued property from the LIVE state onto the restored
52
+ * snapshot (creating containers as needed), so store actions keep working
53
+ * after a full replace.
54
+ */
55
+ function graftFunctions(target, live, seen, depth) {
56
+ if (!isPlainObjectLike(live) || !isPlainObjectLike(target)) return;
57
+ if (depth >= MAX_DEPTH || seen.has(live)) return;
58
+ seen.add(live);
59
+ for (const [k, v] of Object.entries(live)) {
60
+ if (typeof v === "function") {
61
+ target[k] = v;
62
+ } else if (isPlainObjectLike(v)) {
63
+ if (!isPlainObjectLike(target[k])) {
64
+ // Branch missing from the snapshot — only recreate it if it holds
65
+ // functions somewhere; otherwise the snapshot's absence wins.
66
+ if (!containsFunction(v, new WeakSet(), depth + 1)) continue;
67
+ target[k] = {};
68
+ }
69
+ graftFunctions(target[k], v, seen, depth + 1);
70
+ }
71
+ }
72
+ }
73
+ function containsFunction(value, seen, depth) {
74
+ if (depth >= MAX_DEPTH || seen.has(value)) return false;
75
+ seen.add(value);
76
+ for (const v of Object.values(value)) {
77
+ if (typeof v === "function") return true;
78
+ if (isPlainObjectLike(v) && containsFunction(v, seen, depth + 1)) return true;
79
+ }
80
+ return false;
81
+ }
82
+ function status() {
83
+ const count = _zustandStateStore.zustandStateStore.getStores().length;
84
+ if (count === 0) {
85
+ return {
86
+ canCapture: false,
87
+ canRestore: false,
88
+ detail: "No Zustand stores registered — pass `zustandStores` to FloatingDevTools or add the buoyDevTools middleware"
89
+ };
90
+ }
91
+ return {
92
+ canCapture: true,
93
+ canRestore: true
94
+ };
95
+ }
96
+ async function capture() {
97
+ const warnings = [];
98
+ const stores = {};
99
+ for (const info of _zustandStateStore.zustandStateStore.getStores()) {
100
+ const fnPaths = [];
101
+ let state;
102
+ try {
103
+ state = stripFunctions(info.api.getState(), "", fnPaths, new WeakSet(), 0);
104
+ } catch (e) {
105
+ warnings.push(`store "${info.name}": capture failed (${String(e)})`);
106
+ continue;
107
+ }
108
+ stores[info.name] = {
109
+ state,
110
+ fnPaths
111
+ };
112
+ }
113
+ return {
114
+ data: {
115
+ stores
116
+ },
117
+ warnings
118
+ };
119
+ }
120
+ async function restore(data) {
121
+ const snapshot = data;
122
+ const result = {
123
+ ok: true,
124
+ applied: 0,
125
+ skipped: [],
126
+ warnings: []
127
+ };
128
+ if (!snapshot?.stores) {
129
+ return {
130
+ ok: false,
131
+ applied: 0,
132
+ skipped: [],
133
+ warnings: ["empty zustand snapshot"]
134
+ };
135
+ }
136
+ for (const [name, envelope] of Object.entries(snapshot.stores)) {
137
+ const info = _zustandStateStore.zustandStateStore.getStore(name);
138
+ if (!info) {
139
+ result.skipped.push({
140
+ id: name,
141
+ reason: "store not registered (renamed, or created after snapshot?)"
142
+ });
143
+ continue;
144
+ }
145
+ try {
146
+ const next = envelope.state;
147
+ graftFunctions(next, info.api.getState(), new WeakSet(), 0);
148
+ info.api.setState(next, true);
149
+ if (info.api.getState() === next) {
150
+ result.applied += 1;
151
+ } else {
152
+ result.warnings.push(`store "${name}": setState did not take effect`);
153
+ result.ok = false;
154
+ }
155
+ } catch (e) {
156
+ result.skipped.push({
157
+ id: name,
158
+ reason: String(e)
159
+ });
160
+ result.ok = false;
161
+ }
162
+ }
163
+ return result;
164
+ }
165
+ (0, _sharedUi.registerStateSnapshotProvider)({
166
+ id: "zustand",
167
+ label: "Zustand",
168
+ status,
169
+ capture,
170
+ restore
171
+ });
@@ -37,6 +37,9 @@
37
37
  * ```
38
38
  */
39
39
 
40
+ // Side-effect: registers the Time Machine snapshot provider
41
+ import "./zustand/utils/snapshotProvider";
42
+
40
43
  // =============================================================================
41
44
  // PRESET (Primary entry point for FloatingDevTools)
42
45
  // =============================================================================
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Time Machine snapshot provider for Zustand.
5
+ *
6
+ * Registered into @buoy-gg/shared-ui's stateSnapshotRegistry at module init;
7
+ * consumed by @buoy-gg/time-machine. Neither package imports the other.
8
+ *
9
+ * The Zustand-specific hazard: stores conventionally keep their action
10
+ * functions IN state (`increment: () => ...`). Functions don't survive
11
+ * serialization, and a full `setState(snapshot, true)` replace would wipe
12
+ * them, breaking every `useStore(s => s.increment)` call site. So capture
13
+ * strips functions (recording where they were), and restore re-grafts the
14
+ * LIVE store's functions onto the snapshot before replacing.
15
+ */
16
+ import { registerStateSnapshotProvider } from "@buoy-gg/shared-ui";
17
+ import { zustandStateStore } from "./zustandStateStore";
18
+ const MAX_DEPTH = 8;
19
+ function isPlainObjectLike(v) {
20
+ return typeof v === "object" && v !== null && !Array.isArray(v);
21
+ }
22
+
23
+ /** Clone `value` without functions, recording their paths. */
24
+ function stripFunctions(value, path, fnPaths, seen, depth) {
25
+ if (typeof value === "function") {
26
+ fnPaths.push(path);
27
+ return undefined;
28
+ }
29
+ if (typeof value !== "object" || value === null || depth >= MAX_DEPTH) {
30
+ return value;
31
+ }
32
+ if (seen.has(value)) return undefined; // cycle — drop the back-reference
33
+ seen.add(value);
34
+ if (Array.isArray(value)) {
35
+ return value.map((item, i) => typeof item === "function" ? (fnPaths.push(`${path}[${i}]`), null) : stripFunctions(item, `${path}[${i}]`, fnPaths, seen, depth + 1));
36
+ }
37
+ if (!isPlainObjectLike(value)) return value; // Date/Map/Set/etc. — superjson handles
38
+ const out = {};
39
+ for (const [k, v] of Object.entries(value)) {
40
+ if (typeof v === "function") {
41
+ fnPaths.push(path ? `${path}.${k}` : k);
42
+ continue;
43
+ }
44
+ out[k] = stripFunctions(v, path ? `${path}.${k}` : k, fnPaths, seen, depth + 1);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * Copy every function-valued property from the LIVE state onto the restored
51
+ * snapshot (creating containers as needed), so store actions keep working
52
+ * after a full replace.
53
+ */
54
+ function graftFunctions(target, live, seen, depth) {
55
+ if (!isPlainObjectLike(live) || !isPlainObjectLike(target)) return;
56
+ if (depth >= MAX_DEPTH || seen.has(live)) return;
57
+ seen.add(live);
58
+ for (const [k, v] of Object.entries(live)) {
59
+ if (typeof v === "function") {
60
+ target[k] = v;
61
+ } else if (isPlainObjectLike(v)) {
62
+ if (!isPlainObjectLike(target[k])) {
63
+ // Branch missing from the snapshot — only recreate it if it holds
64
+ // functions somewhere; otherwise the snapshot's absence wins.
65
+ if (!containsFunction(v, new WeakSet(), depth + 1)) continue;
66
+ target[k] = {};
67
+ }
68
+ graftFunctions(target[k], v, seen, depth + 1);
69
+ }
70
+ }
71
+ }
72
+ function containsFunction(value, seen, depth) {
73
+ if (depth >= MAX_DEPTH || seen.has(value)) return false;
74
+ seen.add(value);
75
+ for (const v of Object.values(value)) {
76
+ if (typeof v === "function") return true;
77
+ if (isPlainObjectLike(v) && containsFunction(v, seen, depth + 1)) return true;
78
+ }
79
+ return false;
80
+ }
81
+ function status() {
82
+ const count = zustandStateStore.getStores().length;
83
+ if (count === 0) {
84
+ return {
85
+ canCapture: false,
86
+ canRestore: false,
87
+ detail: "No Zustand stores registered — pass `zustandStores` to FloatingDevTools or add the buoyDevTools middleware"
88
+ };
89
+ }
90
+ return {
91
+ canCapture: true,
92
+ canRestore: true
93
+ };
94
+ }
95
+ async function capture() {
96
+ const warnings = [];
97
+ const stores = {};
98
+ for (const info of zustandStateStore.getStores()) {
99
+ const fnPaths = [];
100
+ let state;
101
+ try {
102
+ state = stripFunctions(info.api.getState(), "", fnPaths, new WeakSet(), 0);
103
+ } catch (e) {
104
+ warnings.push(`store "${info.name}": capture failed (${String(e)})`);
105
+ continue;
106
+ }
107
+ stores[info.name] = {
108
+ state,
109
+ fnPaths
110
+ };
111
+ }
112
+ return {
113
+ data: {
114
+ stores
115
+ },
116
+ warnings
117
+ };
118
+ }
119
+ async function restore(data) {
120
+ const snapshot = data;
121
+ const result = {
122
+ ok: true,
123
+ applied: 0,
124
+ skipped: [],
125
+ warnings: []
126
+ };
127
+ if (!snapshot?.stores) {
128
+ return {
129
+ ok: false,
130
+ applied: 0,
131
+ skipped: [],
132
+ warnings: ["empty zustand snapshot"]
133
+ };
134
+ }
135
+ for (const [name, envelope] of Object.entries(snapshot.stores)) {
136
+ const info = zustandStateStore.getStore(name);
137
+ if (!info) {
138
+ result.skipped.push({
139
+ id: name,
140
+ reason: "store not registered (renamed, or created after snapshot?)"
141
+ });
142
+ continue;
143
+ }
144
+ try {
145
+ const next = envelope.state;
146
+ graftFunctions(next, info.api.getState(), new WeakSet(), 0);
147
+ info.api.setState(next, true);
148
+ if (info.api.getState() === next) {
149
+ result.applied += 1;
150
+ } else {
151
+ result.warnings.push(`store "${name}": setState did not take effect`);
152
+ result.ok = false;
153
+ }
154
+ } catch (e) {
155
+ result.skipped.push({
156
+ id: name,
157
+ reason: String(e)
158
+ });
159
+ result.ok = false;
160
+ }
161
+ }
162
+ return result;
163
+ }
164
+ registerStateSnapshotProvider({
165
+ id: "zustand",
166
+ label: "Zustand",
167
+ status,
168
+ capture,
169
+ restore
170
+ });
@@ -34,6 +34,7 @@
34
34
  * );
35
35
  * ```
36
36
  */
37
+ import "./zustand/utils/snapshotProvider";
37
38
  export { zustandToolPreset, createZustandTool } from "./preset";
38
39
  export { watchStores } from "./zustand/utils/buoyZustandMiddleware";
39
40
  export { buoyDevTools } from "./zustand/utils/buoyZustandMiddleware";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAKH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKhE,OAAO,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AAKpE,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAKrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAK5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,YAAY,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AAO3F,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EACL,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAKnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAKvE,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,iBAAiB,CAAC;AAKzB,gBAAgB;AAChB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,kCAAkC,CAAC;AAK1C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKhE,OAAO,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AAKpE,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAKrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAK5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,YAAY,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AAO3F,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EACL,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAKnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAKvE,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,iBAAiB,CAAC;AAKzB,gBAAgB;AAChB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=snapshotProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshotProvider.d.ts","sourceRoot":"","sources":["../../../../src/zustand/utils/snapshotProvider.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/zustand",
3
- "version": "7.0.15",
3
+ "version": "7.0.17",
4
4
  "description": "Zustand devtools inside your React Native app — state diffs, jump-to-state & one-tap store reset, no middleware. Part of Buoy devtools.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -24,10 +24,13 @@
24
24
  "files": [
25
25
  "lib"
26
26
  ],
27
- "sideEffects": false,
27
+ "sideEffects": [
28
+ "**/snapshotProvider.js",
29
+ "**/snapshotProvider.ts"
30
+ ],
28
31
  "dependencies": {
29
- "@buoy-gg/shared-ui": "^7.0.15",
30
- "@buoy-gg/license": "^7.0.15"
32
+ "@buoy-gg/shared-ui": "^7.0.17",
33
+ "@buoy-gg/license": "^7.0.17"
31
34
  },
32
35
  "peerDependencies": {
33
36
  "react": "*",