totally-tubular 0.3.3 → 0.4.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.
package/README.md CHANGED
@@ -18,7 +18,7 @@ npm i totally-tubular --save
18
18
 
19
19
  ## Core concepts
20
20
 
21
- A `Tubular` instance wraps an object and gives you four things:
21
+ A `Tubular` instance wraps an object and gives you five things:
22
22
 
23
23
  | Method | What it does |
24
24
  |---|---|
@@ -26,6 +26,7 @@ A `Tubular` instance wraps an object and gives you four things:
26
26
  | `update(path, fn)` | Calls `fn` with the current value, stores the return value, then notifies all observers |
27
27
  | `observe(path, fn)` | Registers a callback that fires whenever `path` is updated |
28
28
  | `unobserve(path, fn)` | Removes a previously registered callback |
29
+ | `reset()` | Restores every key to its initial value and notifies all observers of each change |
29
30
 
30
31
  Paths are fully type-checked. TypeScript knows every valid dot-path into your state shape, so you get autocomplete and a compile error if you mistype a key.
31
32
 
@@ -63,6 +64,30 @@ store.update("meta.title", (prev) => `Supreme ${prev}`);
63
64
  console.info(store.read("meta.title")); // "Supreme Director"
64
65
  ```
65
66
 
67
+ ## Resetting state
68
+
69
+ Call `reset()` to restore your entire state back to the value you passed to the constructor. Every updated key is restored and every registered observer fires with the new (initial) value.
70
+
71
+ ```typescript
72
+ const store = new Tubular({
73
+ name: "Alice",
74
+ count: 0,
75
+ settings: { theme: "light" },
76
+ });
77
+
78
+ store.update("name", () => "Bob");
79
+ store.update("count", (prev) => prev + 1);
80
+ store.update("settings.theme", () => "dark");
81
+
82
+ store.observe("name", (newVal, oldVal) => {
83
+ console.log(`name: ${oldVal} -> ${newVal}`);
84
+ });
85
+
86
+ store.reset();
87
+ // Logs: name: Bob -> Alice
88
+ // Reads back: "Alice", 0, { theme: "light" }
89
+ ```
90
+
66
91
  ## React
67
92
 
68
93
  Import `useTubular` from `totally-tubular/react`. It works exactly like React's
@@ -115,6 +140,24 @@ setCount((prev) => (prev ?? 0) + 1);
115
140
  `useTubular` automatically unsubscribes when the component unmounts, so you
116
141
  don't need to manage cleanup yourself.
117
142
 
143
+ ### Resetting from React
144
+
145
+ Import `useTubularReset` from `totally-tubular/react`. It returns a stable
146
+ callback that restores the store to its initial state. Every `useTubular` hook
147
+ watching the store re-renders with the restored value.
148
+
149
+ ```tsx
150
+ import { useTubularReset } from "totally-tubular/react";
151
+
152
+ function ResetButton() {
153
+ const handleReset = useTubularReset(store);
154
+ return <button onClick={handleReset}>Reset</button>;
155
+ }
156
+ ```
157
+
158
+ The callback identity never changes, so it's safe to pass directly as an event
159
+ handler or as a dependency to `useEffect`.
160
+
118
161
  ## Things you should know
119
162
 
120
163
  - **Arrays and nested objects are observed at their path, not their contents.**
@@ -0,0 +1,3 @@
1
+ export declare function isArray(thing: unknown): thing is unknown[];
2
+ export declare function isObject(thing: unknown): thing is object;
3
+ export declare function isNullOrUndefined(thing: unknown): thing is null | undefined;
package/dist/object.js ADDED
@@ -0,0 +1,9 @@
1
+ export function isArray(thing) {
2
+ return Array.isArray(thing);
3
+ }
4
+ export function isObject(thing) {
5
+ return typeof thing === 'object';
6
+ }
7
+ export function isNullOrUndefined(thing) {
8
+ return thing === null || thing === undefined || typeof thing === 'undefined';
9
+ }
package/dist/react.d.ts CHANGED
@@ -28,3 +28,26 @@ import type { AllObjectKeys, PropType } from './types.js';
28
28
  * ```
29
29
  */
30
30
  export declare function useTubular<T extends object, K extends string>(tubular: Tubular<T>, key: K & AllObjectKeys<T>): readonly [PropType<T, K> | null, (newValOrCallback: PropType<T, K> | ((prevVal: PropType<T, K> | null) => PropType<T, K>)) => void];
31
+ /**
32
+ * A React hook that returns a stable callback to reset a {@link Tubular} store
33
+ * back to its initial state.
34
+ *
35
+ * Every key in the store is restored to the value originally passed to the
36
+ * constructor, and every registered observer (including {@link useTubular}
37
+ * hooks) is notified of each change.
38
+ *
39
+ * The returned callback is stable across re-renders.
40
+ * It never changes its identity, so it is safe to pass as a dependency to `useEffect` or as a prop
41
+ * to memoized components.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * const store = new Tubular({ name: 'Alice', count: 0 });
46
+ *
47
+ * function ResetButton() {
48
+ * const handleReset = useTubularReset(store);
49
+ * return <button onClick={handleReset}>Reset</button>;
50
+ * }
51
+ * ```
52
+ */
53
+ export declare function useTubularReset<T extends object>(tubular: Tubular<T>): () => void;
package/dist/react.js CHANGED
@@ -52,3 +52,28 @@ export function useTubular(tubular, key) {
52
52
  }, [tubular, key]);
53
53
  return [val, handleUpdateVal];
54
54
  }
55
+ /**
56
+ * A React hook that returns a stable callback to reset a {@link Tubular} store
57
+ * back to its initial state.
58
+ *
59
+ * Every key in the store is restored to the value originally passed to the
60
+ * constructor, and every registered observer (including {@link useTubular}
61
+ * hooks) is notified of each change.
62
+ *
63
+ * The returned callback is stable across re-renders.
64
+ * It never changes its identity, so it is safe to pass as a dependency to `useEffect` or as a prop
65
+ * to memoized components.
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * const store = new Tubular({ name: 'Alice', count: 0 });
70
+ *
71
+ * function ResetButton() {
72
+ * const handleReset = useTubularReset(store);
73
+ * return <button onClick={handleReset}>Reset</button>;
74
+ * }
75
+ * ```
76
+ */
77
+ export function useTubularReset(tubular) {
78
+ return useCallback(() => tubular.reset(), [tubular]);
79
+ }
@@ -8,6 +8,7 @@ import type { AllObjectKeys, ObservationCallback, PropType } from './types.js';
8
8
  export declare class Tubular<T extends object> {
9
9
  private observationCallbacks;
10
10
  private state;
11
+ private initialStateClone;
11
12
  constructor(initialState: T);
12
13
  private doSomethingToPath;
13
14
  private getPortionOfStateByPath;
@@ -19,6 +20,12 @@ export declare class Tubular<T extends object> {
19
20
  * reads a portion of your state
20
21
  */
21
22
  read<K extends string>(readPath: K & AllObjectKeys<T>): PropType<T, K> | null;
23
+ private resetImpl;
24
+ /**
25
+ * resets the state back to its initial value
26
+ * (the value you provided to the constructor)
27
+ */
28
+ reset(): void;
22
29
  /**
23
30
  * disconnects an observer callback from totally tubular
24
31
  */
@@ -1,3 +1,4 @@
1
+ import { isArray, isNullOrUndefined, isObject } from './object.js';
1
2
  /**
2
3
  * A totally tubular instance that tracks a top-level
3
4
  * set of keys and values.
@@ -7,14 +8,13 @@
7
8
  export class Tubular {
8
9
  observationCallbacks = new Map();
9
10
  state;
11
+ initialStateClone;
10
12
  constructor(initialState) {
11
13
  this.state = initialState;
14
+ this.initialStateClone = globalThis.structuredClone(this.state);
12
15
  }
13
16
  doSomethingToPath(readPath, doSomething) {
14
17
  let thing = this.state;
15
- if (typeof readPath !== 'string') {
16
- return this.getPortionOfStateByPath(thing[readPath]);
17
- }
18
18
  const splitPath = readPath.split('.');
19
19
  const len = splitPath.length;
20
20
  for (let i = 0; i < len; i++) {
@@ -26,15 +26,9 @@ export class Tubular {
26
26
  else
27
27
  thing = thing?.[p];
28
28
  }
29
- for (const p of splitPath) {
30
- thing = thing?.[p];
31
- }
32
29
  }
33
30
  getPortionOfStateByPath(readPath) {
34
31
  let thing = this.state;
35
- if (typeof readPath !== 'string') {
36
- return this.getPortionOfStateByPath(thing[readPath]);
37
- }
38
32
  const splitPath = readPath.split('.');
39
33
  for (const p of splitPath) {
40
34
  thing = thing?.[p];
@@ -54,6 +48,32 @@ export class Tubular {
54
48
  read(readPath) {
55
49
  return this.getPortionOfStateByPath(readPath);
56
50
  }
51
+ resetImpl(parentKey) {
52
+ let sourceObj = this.initialStateClone;
53
+ if (parentKey) {
54
+ for (const p of parentKey.split('.')) {
55
+ sourceObj = sourceObj?.[p];
56
+ }
57
+ if (isNullOrUndefined(sourceObj) || !isObject(sourceObj))
58
+ return;
59
+ }
60
+ for (const [key, val] of Object.entries(sourceObj)) {
61
+ const childKey = parentKey ? `${parentKey}.${key}` : key;
62
+ if (!isNullOrUndefined(val) && isObject(val) && !isArray(val)) {
63
+ this.resetImpl(childKey);
64
+ }
65
+ else {
66
+ this.update(childKey, () => val);
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * resets the state back to its initial value
72
+ * (the value you provided to the constructor)
73
+ */
74
+ reset() {
75
+ this.resetImpl();
76
+ }
57
77
  /**
58
78
  * disconnects an observer callback from totally tubular
59
79
  */
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "A completely framework-agnostic, JavaScript / TypeScript state management and update broadcasting library wit zero dependencies, solid performance and a micro file size.",
4
4
  "name": "totally-tubular",
5
5
  "type": "module",
6
- "version": "0.3.3",
6
+ "version": "0.4.0",
7
7
  "keywords": [
8
8
  "state",
9
9
  "management",
@@ -34,16 +34,18 @@
34
34
  },
35
35
  "devDependencies": {
36
36
  "@better-builds/lets-version": "^1.6.0",
37
- "@biomejs/biome": "^2.4.11",
38
- "@commitlint/cli": "^20.5.0",
39
- "@commitlint/config-conventional": "^20.5.0",
40
- "@happy-dom/global-registrator": "^20.9.0",
37
+ "@biomejs/biome": "^2.4.16",
38
+ "@commitlint/cli": "^20.5.3",
39
+ "@commitlint/config-conventional": "^20.5.3",
40
+ "@happy-dom/global-registrator": "^20.10.2",
41
41
  "@testing-library/react": "^16.3.2",
42
42
  "@testing-library/user-event": "^14.6.1",
43
43
  "@types/bun": "latest",
44
- "@types/react": "^19.2.14",
45
- "happy-dom": "^20.9.0",
46
- "react": ">=18",
44
+ "@types/react": "^19.2.17",
45
+ "@types/react-dom": "^19.2.3",
46
+ "happy-dom": "^20.10.2",
47
+ "react-dom": "^19.2.7",
48
+ "react": "^19.2.7",
47
49
  "typescript": "^5.9.3"
48
50
  },
49
51
  "peerDependencies": {