juststore 1.1.1 → 1.2.0

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.
@@ -0,0 +1,644 @@
1
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore, } from "react";
2
+ import rfcIsEqual from "react-fast-compare";
3
+ import { KVStore } from "./kv_store";
4
+ import { getExternalKeyOrder, getStableKeys, setExternalKeyOrder, } from "./stable_keys";
5
+ export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, isRecord, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, setNestedValue, subscribe, testReset, updateSnapshot, useCompute, useDebounce, useObject, };
6
+ const inMemStorage = new Map();
7
+ const listeners = new Map();
8
+ const descendantListenerKeysByPrefix = new Map();
9
+ const virtualRevisions = new Map();
10
+ const store = new KVStore({
11
+ inMemStorage,
12
+ memoryOnly: false,
13
+ });
14
+ const memoryStore = new KVStore({
15
+ inMemStorage,
16
+ memoryOnly: true,
17
+ });
18
+ function testReset() {
19
+ store.reset();
20
+ memoryStore.reset();
21
+ }
22
+ function isVirtualKey(key) {
23
+ return key.endsWith(".__juststore_keys") || key === "__juststore_keys";
24
+ }
25
+ // check if the value is a class instance
26
+ function isClass(value) {
27
+ if (value === null || value === undefined)
28
+ return false;
29
+ if (typeof value !== "object")
30
+ return false;
31
+ const proto = Object.getPrototypeOf(value);
32
+ if (!proto || proto === Object.prototype || proto === Array.prototype)
33
+ return false;
34
+ const descriptors = Object.getOwnPropertyDescriptors(proto);
35
+ for (const key in descriptors) {
36
+ if (descriptors[key]?.get)
37
+ return true;
38
+ }
39
+ return false;
40
+ }
41
+ function isRecord(value) {
42
+ if (value === null || value === undefined)
43
+ return false;
44
+ if (typeof value !== "object")
45
+ return false;
46
+ return !Array.isArray(value) && !isClass(value);
47
+ }
48
+ /** Compare two values for equality
49
+ * @description
50
+ * - react-fast-compare for non-class instances
51
+ * - reference equality for class instances
52
+ * @param a - The first value to compare
53
+ * @param b - The second value to compare
54
+ * @returns True if the values are equal, false otherwise
55
+ */
56
+ function isEqual(a, b) {
57
+ if (a === b)
58
+ return true;
59
+ if (isClass(a) || isClass(b))
60
+ return a === b;
61
+ return rfcIsEqual(a, b);
62
+ }
63
+ /**
64
+ * Extracts the root namespace from a full key.
65
+ *
66
+ * @param key - Full key string
67
+ * @returns Namespace
68
+ * @example
69
+ * getNamespace('app.user.name') // 'app'
70
+ */
71
+ function getNamespace(key) {
72
+ const index = key.indexOf(".");
73
+ if (index === -1)
74
+ return key;
75
+ return key.slice(0, index);
76
+ }
77
+ /**
78
+ * Joins a namespace and path into a full key string.
79
+ *
80
+ * @param namespace - The store namespace (root key)
81
+ * @param path - Optional dot-separated path within the namespace
82
+ * @returns Combined key string (e.g., "app.user.name")
83
+ */
84
+ function joinPath(namespace, path) {
85
+ if (!path)
86
+ return namespace;
87
+ return `${namespace}.${path}`;
88
+ }
89
+ function joinChildKey(parent, child) {
90
+ return parent ? `${parent}.${child}` : child;
91
+ }
92
+ function getKeyPrefixes(key) {
93
+ const dot = key.indexOf(".");
94
+ if (dot === -1)
95
+ return [];
96
+ const [first, ...parts] = key.split(".");
97
+ if (parts.length === 0)
98
+ return [];
99
+ const prefixes = [];
100
+ let current = first;
101
+ for (let i = 0; i < parts.length - 1; i++) {
102
+ current += `.${parts[i]}`;
103
+ prefixes.push(current);
104
+ }
105
+ prefixes.unshift(first);
106
+ return prefixes;
107
+ }
108
+ /** Snapshot getter used by React's useSyncExternalStore. */
109
+ function getSnapshot(key, memoryOnly) {
110
+ if (isVirtualKey(key)) {
111
+ return virtualRevisions.get(key) ?? 0;
112
+ }
113
+ if (memoryOnly) {
114
+ return memoryStore.get(key);
115
+ }
116
+ else {
117
+ return store.get(key);
118
+ }
119
+ }
120
+ /** Updates the snapshot of a key. */
121
+ function updateSnapshot(key, value, memoryOnly) {
122
+ if (memoryOnly) {
123
+ memoryStore.set(key, value);
124
+ }
125
+ else {
126
+ store.set(key, value);
127
+ }
128
+ }
129
+ // Path traversal utilities
130
+ /** Get a nested value from an object/array using a dot-separated path. */
131
+ function getNestedValue(obj, path) {
132
+ if (!path)
133
+ return obj;
134
+ const segments = path.split(".");
135
+ let current = obj;
136
+ // Array indices must be explicit non-negative integers.
137
+ // IMPORTANT: treat empty string ("") as a *key*, not index 0.
138
+ // (Number('') === 0 would otherwise turn paths like `foo.bar.` into `foo.bar.0`.)
139
+ const parseArrayIndex = (segment) => {
140
+ if (!/^(0|[1-9]\d*)$/.test(segment))
141
+ return null;
142
+ return Number(segment);
143
+ };
144
+ for (const segment of segments) {
145
+ if (current === null || current === undefined)
146
+ return undefined;
147
+ if (typeof current !== "object")
148
+ return undefined;
149
+ if (Array.isArray(current)) {
150
+ const index = parseArrayIndex(segment);
151
+ if (index === null)
152
+ return undefined;
153
+ current = current[index];
154
+ }
155
+ else {
156
+ current = current[segment];
157
+ }
158
+ }
159
+ return current;
160
+ }
161
+ /**
162
+ * Immutably sets or deletes a nested value using a dot-separated path.
163
+ *
164
+ * Creates intermediate objects or arrays as needed based on whether the next
165
+ * path segment is numeric. When value is undefined, the key is deleted from
166
+ * objects or the index is spliced from arrays.
167
+ *
168
+ * @param obj - The root object to update
169
+ * @param path - Dot-separated path to the target location
170
+ * @param value - The value to set, or undefined to delete
171
+ * @returns A new root object with the change applied
172
+ */
173
+ function setNestedValue(obj, path, value) {
174
+ if (!path)
175
+ return value;
176
+ const segments = path.split(".");
177
+ if (obj !== null && obj !== undefined && typeof obj !== "object") {
178
+ return obj;
179
+ }
180
+ // Array indices must be explicit non-negative integers.
181
+ // IMPORTANT: treat empty string ("") as a *key*, not index 0.
182
+ const parseArrayIndex = (segment) => {
183
+ if (!/^(0|[1-9]\d*)$/.test(segment))
184
+ return null;
185
+ return Number(segment);
186
+ };
187
+ const result = obj === null || obj === undefined
188
+ ? {}
189
+ : Array.isArray(obj)
190
+ ? [...obj]
191
+ : (() => {
192
+ const existing = obj;
193
+ const next = { ...existing };
194
+ const order = getExternalKeyOrder(existing);
195
+ if (order)
196
+ setExternalKeyOrder(next, order);
197
+ return next;
198
+ })();
199
+ let current = result;
200
+ for (let i = 0; i < segments.length - 1; i++) {
201
+ const segment = segments[i];
202
+ const nextSegment = segments[i + 1];
203
+ const isNextIndex = parseArrayIndex(nextSegment) !== null;
204
+ if (Array.isArray(current)) {
205
+ const index = parseArrayIndex(segment);
206
+ if (index === null)
207
+ break;
208
+ const existing = current[index];
209
+ let next;
210
+ if (existing === null || existing === undefined) {
211
+ next = isNextIndex ? [] : {};
212
+ }
213
+ else if (typeof existing !== "object") {
214
+ next = isNextIndex ? [] : {};
215
+ }
216
+ else if (Array.isArray(existing)) {
217
+ next = [...existing];
218
+ }
219
+ else {
220
+ next = { ...existing };
221
+ }
222
+ current[index] = next;
223
+ current = next;
224
+ }
225
+ else if (typeof current === "object" && current !== null) {
226
+ const currentObj = current;
227
+ const existing = currentObj[segment];
228
+ let next;
229
+ if (existing === null || existing === undefined) {
230
+ next = isNextIndex ? [] : {};
231
+ }
232
+ else if (typeof existing !== "object") {
233
+ next = isNextIndex ? [] : {};
234
+ }
235
+ else if (Array.isArray(existing)) {
236
+ next = [...existing];
237
+ }
238
+ else {
239
+ const existingObj = existing;
240
+ next = { ...existingObj };
241
+ const order = getExternalKeyOrder(existingObj);
242
+ if (order)
243
+ setExternalKeyOrder(next, order);
244
+ }
245
+ currentObj[segment] = next;
246
+ current = next;
247
+ }
248
+ }
249
+ const lastSegment = segments[segments.length - 1];
250
+ if (Array.isArray(current)) {
251
+ const index = parseArrayIndex(lastSegment);
252
+ if (index !== null) {
253
+ if (value === undefined) {
254
+ current.splice(index, 1);
255
+ }
256
+ else {
257
+ current[index] = value;
258
+ }
259
+ }
260
+ }
261
+ else if (typeof current === "object" && current !== null) {
262
+ const currentObj = current;
263
+ const hadKey = Object.hasOwn(currentObj, lastSegment);
264
+ if (value === undefined) {
265
+ delete currentObj[lastSegment];
266
+ if (hadKey) {
267
+ const order = getExternalKeyOrder(currentObj);
268
+ if (order)
269
+ setExternalKeyOrder(currentObj, order.filter((k) => k !== lastSegment));
270
+ }
271
+ }
272
+ else {
273
+ currentObj[lastSegment] = value;
274
+ if (!hadKey) {
275
+ const order = getExternalKeyOrder(currentObj);
276
+ if (order)
277
+ setExternalKeyOrder(currentObj, [...order, lastSegment]);
278
+ }
279
+ }
280
+ }
281
+ return result;
282
+ }
283
+ /**
284
+ * Notifies all relevant listeners when a value changes.
285
+ *
286
+ * Handles three types of listeners:
287
+ * 1. Exact match - listeners subscribed to the exact changed path
288
+ * 2. Root listeners - listeners on the namespace root (for full-store subscriptions)
289
+ * 3. Child listeners - listeners on nested paths that may be affected by the change
290
+ *
291
+ * Child listeners are only notified if their specific value actually changed,
292
+ * determined by deep equality comparison.
293
+ */
294
+ function notifyListeners(key, oldValue, newValue, { skipRoot = false, skipChildren = false, forceNotify = false } = {}) {
295
+ // Keep `state.xxx.keys()` in sync: any mutation under a path can change the set of
296
+ // keys for that path (or its ancestors). Keys are represented as virtual nodes at
297
+ // `${path}.__juststore_keys`, so we bump those virtual nodes here.
298
+ //
299
+ // Important: avoid recursion when *we* are notifying a virtual key.
300
+ if (!isVirtualKey(key)) {
301
+ const paths = [...getKeyPrefixes(key), key];
302
+ for (const p of paths) {
303
+ const virtualKey = joinChildKey(p, "__juststore_keys");
304
+ const listenerSet = listeners.get(virtualKey);
305
+ if (listenerSet && listenerSet.size > 0) {
306
+ // Only notify the virtual key subscribers; the current call will handle
307
+ // ancestors/children for the real key.
308
+ notifyVirtualKey(virtualKey);
309
+ }
310
+ }
311
+ }
312
+ if (skipRoot && skipChildren) {
313
+ if (!forceNotify && isEqual(oldValue, newValue)) {
314
+ return;
315
+ }
316
+ // exact match only
317
+ const listenerSet = listeners.get(key);
318
+ if (listenerSet) {
319
+ listenerSet.forEach((listener) => {
320
+ listener();
321
+ });
322
+ }
323
+ return;
324
+ }
325
+ // Exact key match
326
+ const exactSet = listeners.get(key);
327
+ if (exactSet) {
328
+ exactSet.forEach((listener) => {
329
+ listener();
330
+ });
331
+ }
332
+ // Ancestor keys match (including namespace root)
333
+ if (!skipRoot) {
334
+ const namespace = getNamespace(key);
335
+ if (namespace !== key) {
336
+ const rootSet = listeners.get(namespace);
337
+ if (rootSet) {
338
+ rootSet.forEach((listener) => {
339
+ listener();
340
+ });
341
+ }
342
+ }
343
+ // Also notify intermediate ancestors
344
+ const prefixes = getKeyPrefixes(key);
345
+ for (const prefix of prefixes) {
346
+ if (prefix === namespace)
347
+ continue; // Already handled
348
+ const prefixSet = listeners.get(prefix);
349
+ if (prefixSet) {
350
+ prefixSet.forEach((listener) => {
351
+ listener();
352
+ });
353
+ }
354
+ }
355
+ }
356
+ // Child key match - check if value actually changed
357
+ if (!skipChildren) {
358
+ const childKeys = descendantListenerKeysByPrefix.get(key);
359
+ if (childKeys) {
360
+ for (const childKey of childKeys) {
361
+ if (isVirtualKey(childKey)) {
362
+ const childPath = childKey.slice(key.length + 1);
363
+ const suffix = ".__juststore_keys";
364
+ const objectPath = childPath.endsWith(suffix)
365
+ ? childPath.slice(0, -suffix.length)
366
+ : "";
367
+ const getKeys = (root) => {
368
+ const obj = objectPath ? getNestedValue(root, objectPath) : root;
369
+ return getStableKeys(obj);
370
+ };
371
+ const oldKeys = getKeys(oldValue);
372
+ const newKeys = getKeys(newValue);
373
+ if (forceNotify || !isEqual(oldKeys, newKeys)) {
374
+ notifyVirtualKey(childKey);
375
+ }
376
+ continue;
377
+ }
378
+ const childPath = childKey.slice(key.length + 1);
379
+ const oldChildValue = getNestedValue(oldValue, childPath);
380
+ const newChildValue = getNestedValue(newValue, childPath);
381
+ if (forceNotify || !isEqual(oldChildValue, newChildValue)) {
382
+ const childSet = listeners.get(childKey);
383
+ if (childSet) {
384
+ childSet.forEach((listener) => {
385
+ listener();
386
+ });
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+ }
393
+ function notifyVirtualKey(key) {
394
+ virtualRevisions.set(key, (virtualRevisions.get(key) ?? 0) + 1);
395
+ notifyListeners(key, undefined, undefined, {
396
+ skipRoot: true,
397
+ skipChildren: true,
398
+ forceNotify: true,
399
+ });
400
+ }
401
+ /**
402
+ * Subscribes to changes for a specific key.
403
+ *
404
+ * @param key - The full key path to subscribe to
405
+ * @param listener - Callback invoked when the value changes
406
+ * @returns An unsubscribe function to remove the listener
407
+ */
408
+ function subscribe(key, listener) {
409
+ if (!listeners.has(key)) {
410
+ listeners.set(key, new Set());
411
+ }
412
+ listeners.get(key)?.add(listener);
413
+ const prefixes = getKeyPrefixes(key);
414
+ for (const prefix of prefixes) {
415
+ if (!descendantListenerKeysByPrefix.has(prefix)) {
416
+ descendantListenerKeysByPrefix.set(prefix, new Set());
417
+ }
418
+ descendantListenerKeysByPrefix.get(prefix)?.add(key);
419
+ }
420
+ return () => {
421
+ const keyListeners = listeners.get(key);
422
+ if (keyListeners) {
423
+ keyListeners.delete(listener);
424
+ if (keyListeners.size === 0) {
425
+ listeners.delete(key);
426
+ for (const prefix of prefixes) {
427
+ const prefixKeys = descendantListenerKeysByPrefix.get(prefix);
428
+ if (prefixKeys) {
429
+ prefixKeys.delete(key);
430
+ if (prefixKeys.size === 0) {
431
+ descendantListenerKeysByPrefix.delete(prefix);
432
+ }
433
+ }
434
+ }
435
+ }
436
+ }
437
+ };
438
+ }
439
+ function useCompute(namespace, path, fn, deps, memoryOnly = false) {
440
+ const fullPath = joinPath(namespace, path);
441
+ const fnRef = useRef(fn);
442
+ fnRef.current = fn;
443
+ const cacheRef = useRef(null);
444
+ const depsRef = useRef(deps);
445
+ // Invalidate cached compute when hook inputs change.
446
+ if (!isEqual(depsRef.current, deps)) {
447
+ depsRef.current = deps;
448
+ cacheRef.current = null;
449
+ }
450
+ const pathRef = useRef(fullPath);
451
+ if (pathRef.current !== fullPath) {
452
+ pathRef.current = fullPath;
453
+ cacheRef.current = null;
454
+ }
455
+ const subscribeToPath = useCallback((onStoreChange) => subscribe(fullPath, onStoreChange), [fullPath]);
456
+ const getComputedSnapshot = useCallback(() => {
457
+ const storeValue = getSnapshot(fullPath, memoryOnly);
458
+ if (cacheRef.current &&
459
+ Object.is(cacheRef.current.storeValue, storeValue)) {
460
+ // same store value, return the same computed value
461
+ return cacheRef.current.computed;
462
+ }
463
+ const computedNext = fnRef.current(storeValue);
464
+ // Important: even if storeValue changed, we should avoid forcing a re-render
465
+ // when the computed result is logically unchanged. `useSyncExternalStore`
466
+ // uses `Object.is` on the snapshot; returning the same reference will bail out.
467
+ if (cacheRef.current && isEqual(cacheRef.current.computed, computedNext)) {
468
+ cacheRef.current.storeValue = storeValue;
469
+ return cacheRef.current.computed;
470
+ }
471
+ cacheRef.current = { storeValue, computed: computedNext };
472
+ return computedNext;
473
+ }, [fullPath, memoryOnly]);
474
+ return useSyncExternalStore(subscribeToPath, getComputedSnapshot, getComputedSnapshot);
475
+ }
476
+ /**
477
+ * Core mutation function that updates the store and notifies listeners.
478
+ *
479
+ * Handles both setting and deleting values, with optimizations to skip
480
+ * unnecessary updates when the value hasn't changed.
481
+ *
482
+ * @param key - The full key path to update
483
+ * @param value - The new value, or undefined to delete
484
+ * @param skipUpdate - When true, skips notifying listeners
485
+ * @param memoryOnly - When true, skips localStorage persistence
486
+ */
487
+ function produce(key, value, skipUpdate, memoryOnly) {
488
+ if (skipUpdate) {
489
+ updateSnapshot(key, value, memoryOnly);
490
+ return;
491
+ }
492
+ const current = getSnapshot(key, memoryOnly);
493
+ if (isEqual(current, value))
494
+ return;
495
+ updateSnapshot(key, value, memoryOnly);
496
+ // Notify listeners hierarchically with old and new values
497
+ notifyListeners(key, current, value);
498
+ }
499
+ /**
500
+ * Renames a key in an object.
501
+ *
502
+ * It trigger updates to
503
+ *
504
+ * - listeners to `path` (key is updated)
505
+ * - listeners to `path.oldKey` (deleted)
506
+ * - listeners to `path.newKey` (created)
507
+ *
508
+ * @param path - The full key path to rename
509
+ * @param oldKey - The old key to rename
510
+ * @param newKey - The new key to rename to
511
+ */
512
+ function rename(path, oldKey, newKey, memoryOnly) {
513
+ const current = getSnapshot(path, memoryOnly);
514
+ if (current === undefined ||
515
+ current === null ||
516
+ typeof current !== "object") {
517
+ // assign a new object with the new key
518
+ const next = { [newKey]: undefined };
519
+ updateSnapshot(path, next, memoryOnly);
520
+ setExternalKeyOrder(next, [newKey]);
521
+ notifyListeners(path, current, next);
522
+ return;
523
+ }
524
+ const obj = current;
525
+ if (oldKey === newKey)
526
+ return;
527
+ if (!Object.hasOwn(obj, oldKey))
528
+ return;
529
+ const keyOrder = getStableKeys(obj);
530
+ const entries = [];
531
+ for (const key of keyOrder) {
532
+ if (!Object.hasOwn(obj, key))
533
+ continue;
534
+ if (key === oldKey) {
535
+ entries.push([newKey, obj[oldKey]]);
536
+ continue;
537
+ }
538
+ entries.push([key, obj[key]]);
539
+ }
540
+ const newObject = Object.fromEntries(entries);
541
+ updateSnapshot(path, newObject, memoryOnly);
542
+ setExternalKeyOrder(newObject, Array.from(new Set(entries.map(([k]) => k))));
543
+ notifyListeners(path, current, newObject);
544
+ }
545
+ /**
546
+ * React hook that subscribes to and reads a value at a path.
547
+ *
548
+ * Uses useSyncExternalStore for tear-free reads and automatic re-rendering
549
+ * when the subscribed value changes.
550
+ *
551
+ * @param key - The namespace or full key
552
+ * @param path - Optional path within the namespace
553
+ * @param memoryOnly - When true, skips localStorage persistence
554
+ * @returns The current value at the path, or undefined if not set
555
+ */
556
+ function useObject(key, path, memoryOnly) {
557
+ const fullKey = joinPath(key, path);
558
+ const value = useSyncExternalStore((listener) => subscribe(fullKey, listener), () => getSnapshot(fullKey, memoryOnly), () => getSnapshot(fullKey, memoryOnly));
559
+ return value;
560
+ }
561
+ /**
562
+ * React hook that subscribes to a value with debounced updates.
563
+ *
564
+ * The returned value only updates after the specified delay has passed
565
+ * since the last change, useful for expensive operations like search.
566
+ *
567
+ * @param key - The namespace or full key
568
+ * @param path - Path within the namespace
569
+ * @param delay - Debounce delay in milliseconds
570
+ * @param memoryOnly - When true, skips localStorage persistence
571
+ * @returns The debounced value at the path
572
+ */
573
+ function useDebounce(key, path, delay, memoryOnly) {
574
+ const fullKey = joinPath(key, path);
575
+ const currentValue = useSyncExternalStore((listener) => subscribe(fullKey, listener), () => getSnapshot(fullKey, memoryOnly), () => getSnapshot(fullKey, memoryOnly));
576
+ const [debouncedValue, setDebouncedValue] = useState(currentValue);
577
+ const timeoutRef = useRef(undefined);
578
+ useEffect(() => {
579
+ if (timeoutRef.current) {
580
+ clearTimeout(timeoutRef.current);
581
+ }
582
+ timeoutRef.current = setTimeout(() => {
583
+ if (!isEqual(debouncedValue, currentValue)) {
584
+ setDebouncedValue(currentValue);
585
+ }
586
+ }, delay);
587
+ return () => {
588
+ if (timeoutRef.current) {
589
+ clearTimeout(timeoutRef.current);
590
+ }
591
+ };
592
+ }, [currentValue, delay, debouncedValue]);
593
+ return debouncedValue;
594
+ }
595
+ /**
596
+ * Sets a value at a specific path within a namespace.
597
+ *
598
+ * @param key - The namespace
599
+ * @param path - Path within the namespace
600
+ * @param value - The value to set, or undefined to delete
601
+ * @param skipUpdate - When true, skips notifying listeners
602
+ * @param memoryOnly - When true, skips localStorage persistence
603
+ */
604
+ function setLeaf(key, path, value, skipUpdate = false, memoryOnly = false) {
605
+ const fullKey = joinPath(key, path);
606
+ produce(fullKey, value, skipUpdate, memoryOnly);
607
+ }
608
+ // BroadcastChannel for cross-tab synchronization
609
+ const broadcastChannel = typeof window !== "undefined" ? new BroadcastChannel("juststore") : null;
610
+ // Cross-tab synchronization: keep memoryStore in sync with BroadcastChannel events
611
+ if (broadcastChannel) {
612
+ store.setBroadcastChannel(broadcastChannel);
613
+ memoryStore.setBroadcastChannel(broadcastChannel);
614
+ broadcastChannel.addEventListener("message", (event) => {
615
+ const { type, key, value } = event.data;
616
+ if (!key)
617
+ return;
618
+ // Store old value before updating
619
+ const oldRootValue = memoryStore.get(key);
620
+ if (type === "delete") {
621
+ memoryStore.delete(key);
622
+ }
623
+ else if (type === "set") {
624
+ memoryStore.set(key, value);
625
+ }
626
+ // Notify all listeners that might be affected by this root key change
627
+ const newRootValue = type === "delete" ? undefined : value;
628
+ notifyListeners(key, oldRootValue, newRootValue);
629
+ });
630
+ }
631
+ // Debug helpers (dev only)
632
+ /** Development-only debug helpers exposed on window.__pc_debug in development. */
633
+ const __pc_debug = {
634
+ getStoreSize: () => store.size,
635
+ getListenerSize: () => listeners.size,
636
+ getStore: () => memoryStore,
637
+ getStoreValue: (key) => memoryStore.get(key),
638
+ getListeners: () => listeners,
639
+ };
640
+ // Expose debug in browser for quick inspection during development
641
+ if (typeof window !== "undefined" && process.env.NODE_ENV === "development") {
642
+ window.__pc_debug =
643
+ __pc_debug;
644
+ }
@@ -0,0 +1,10 @@
1
+ export { type Atom, createAtom } from "./atom";
2
+ export type * from "./form";
3
+ export { useForm } from "./form";
4
+ export { isEqual } from "./impl";
5
+ export { createMemoryStore, type MemoryStore, useMemoryStore } from "./memory";
6
+ export { createMixedState } from "./mixed_state";
7
+ export type * from "./path";
8
+ export { createStore, type Store } from "./store";
9
+ export type * from "./types";
10
+ export * from "./utils";
@@ -0,0 +1,7 @@
1
+ export { createAtom } from "./atom";
2
+ export { useForm } from "./form";
3
+ export { isEqual } from "./impl";
4
+ export { createMemoryStore, useMemoryStore } from "./memory";
5
+ export { createMixedState } from "./mixed_state";
6
+ export { createStore } from "./store";
7
+ export * from "./utils";
@@ -0,0 +1,29 @@
1
+ import { getNestedValue, setNestedValue } from "./impl";
2
+ export { getNestedValue, type KeyValueStore, KVStore, setNestedValue };
3
+ type KeyValueStore = {
4
+ getBroadcastChannel: () => BroadcastChannel | undefined;
5
+ setBroadcastChannel: (broadcastChannel: BroadcastChannel) => void;
6
+ get: (key: string) => unknown;
7
+ set: (key: string, value: unknown) => void;
8
+ delete: (key: string) => void;
9
+ reset: () => void;
10
+ readonly size: number;
11
+ };
12
+ type CreateKVStoreOptions = {
13
+ inMemStorage: Map<string, unknown>;
14
+ broadcastChannel?: BroadcastChannel;
15
+ memoryOnly: boolean;
16
+ };
17
+ declare class KVStore implements KeyValueStore {
18
+ private inMemStorage;
19
+ private broadcastChannel?;
20
+ private memoryOnly;
21
+ constructor(options: CreateKVStoreOptions);
22
+ getBroadcastChannel(): BroadcastChannel | undefined;
23
+ setBroadcastChannel(broadcastChannel: BroadcastChannel): void;
24
+ get(key: string): unknown;
25
+ set(key: string, value: unknown): void;
26
+ delete(key: string): void;
27
+ reset(): void;
28
+ get size(): number;
29
+ }