totally-tubular 0.3.2 → 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 +44 -1
- package/dist/object.d.ts +3 -0
- package/dist/object.js +9 -0
- package/dist/{cjs/react.d.ts → react.d.ts} +23 -0
- package/dist/react.js +79 -0
- package/dist/{esm/totally-tubular.d.ts → totally-tubular.d.ts} +7 -0
- package/dist/totally-tubular.js +101 -0
- package/dist/types.js +1 -0
- package/package.json +27 -49
- package/dist/cjs/index.cjs +0 -11
- package/dist/cjs/package.json +0 -1
- package/dist/cjs/react.cjs +0 -42
- package/dist/cjs/totally-tubular.cjs +0 -80
- package/dist/cjs/totally-tubular.d.ts +0 -30
- package/dist/cjs/types.cjs +0 -4
- package/dist/esm/index.mjs +0 -1
- package/dist/esm/package.json +0 -1
- package/dist/esm/react.d.ts +0 -30
- package/dist/esm/react.mjs +0 -58
- package/dist/esm/totally-tubular.mjs +0 -75
- package/dist/esm/types.d.ts +0 -53
- package/dist/esm/types.mjs +0 -11
- /package/dist/{cjs/index.d.ts → index.d.ts} +0 -0
- /package/dist/{esm/index.d.ts → index.js} +0 -0
- /package/dist/{cjs/types.d.ts → types.d.ts} +0 -0
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
|
|
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.**
|
package/dist/object.d.ts
ADDED
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
|
+
}
|
|
@@ -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
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* A React hook that binds to a {@link Tubular} instance and a specific key path
|
|
4
|
+
* within its state.
|
|
5
|
+
*
|
|
6
|
+
* Returns a `[value, setter]` tuple that works exactly like React's built-in
|
|
7
|
+
* `useState`, except reads and writes go through the Tubular store — so every
|
|
8
|
+
* component (and non-React observer) watching the same path is notified on
|
|
9
|
+
* every update.
|
|
10
|
+
*
|
|
11
|
+
* The setter accepts either a new value directly or an updater function that
|
|
12
|
+
* receives the current value and returns the next one.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
|
|
17
|
+
*
|
|
18
|
+
* function Counter() {
|
|
19
|
+
* const [count, setCount] = useTubular(store, 'count');
|
|
20
|
+
* return <button onClick={() => setCount(n => (n ?? 0) + 1)}>{count}</button>;
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* function Greeting() {
|
|
24
|
+
* const [name] = useTubular(store, 'user.name');
|
|
25
|
+
* return <p>Hello, {name}!</p>;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useTubular(tubular, key) {
|
|
30
|
+
/** state */
|
|
31
|
+
const [val, setVal] = useState(() => tubular.read(key));
|
|
32
|
+
/** callbacks */
|
|
33
|
+
const handleUpdateVal = useCallback((newValOrCallback) => {
|
|
34
|
+
if (typeof newValOrCallback === 'function') {
|
|
35
|
+
tubular.update(key, newValOrCallback);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
tubular.update(key, () => newValOrCallback);
|
|
39
|
+
}
|
|
40
|
+
}, [tubular, key]);
|
|
41
|
+
/** effects */
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
// Sync in case the value changed between render and effect registration
|
|
44
|
+
setVal(tubular.read(key));
|
|
45
|
+
const observeCb = (newVal) => {
|
|
46
|
+
setVal(newVal);
|
|
47
|
+
};
|
|
48
|
+
tubular.observe(key, observeCb);
|
|
49
|
+
return () => {
|
|
50
|
+
tubular.unobserve(key, observeCb);
|
|
51
|
+
};
|
|
52
|
+
}, [tubular, key]);
|
|
53
|
+
return [val, handleUpdateVal];
|
|
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
|
*/
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isArray, isNullOrUndefined, isObject } from './object.js';
|
|
2
|
+
/**
|
|
3
|
+
* A totally tubular instance that tracks a top-level
|
|
4
|
+
* set of keys and values.
|
|
5
|
+
* if you want to support nested or "deep" state updates and observations,
|
|
6
|
+
* you should construct your state to be a series of Tubular instances
|
|
7
|
+
*/
|
|
8
|
+
export class Tubular {
|
|
9
|
+
observationCallbacks = new Map();
|
|
10
|
+
state;
|
|
11
|
+
initialStateClone;
|
|
12
|
+
constructor(initialState) {
|
|
13
|
+
this.state = initialState;
|
|
14
|
+
this.initialStateClone = globalThis.structuredClone(this.state);
|
|
15
|
+
}
|
|
16
|
+
doSomethingToPath(readPath, doSomething) {
|
|
17
|
+
let thing = this.state;
|
|
18
|
+
const splitPath = readPath.split('.');
|
|
19
|
+
const len = splitPath.length;
|
|
20
|
+
for (let i = 0; i < len; i++) {
|
|
21
|
+
const p = splitPath[i] || '';
|
|
22
|
+
if (i === len - 1) {
|
|
23
|
+
if (thing)
|
|
24
|
+
thing[p] = doSomething(thing[p]);
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
thing = thing?.[p];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
getPortionOfStateByPath(readPath) {
|
|
31
|
+
let thing = this.state;
|
|
32
|
+
const splitPath = readPath.split('.');
|
|
33
|
+
for (const p of splitPath) {
|
|
34
|
+
thing = thing?.[p];
|
|
35
|
+
}
|
|
36
|
+
return thing ?? null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* observe changes to some part of state that was updated
|
|
40
|
+
*/
|
|
41
|
+
observe(propPath, observeCallback) {
|
|
42
|
+
const prevForPath = this.observationCallbacks.get(propPath) ?? [];
|
|
43
|
+
this.observationCallbacks.set(propPath, [...prevForPath, observeCallback]);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* reads a portion of your state
|
|
47
|
+
*/
|
|
48
|
+
read(readPath) {
|
|
49
|
+
return this.getPortionOfStateByPath(readPath);
|
|
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
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* disconnects an observer callback from totally tubular
|
|
79
|
+
*/
|
|
80
|
+
unobserve(propPath, observeCallback) {
|
|
81
|
+
const observers = this.observationCallbacks.get(propPath);
|
|
82
|
+
if (!observers?.length)
|
|
83
|
+
return;
|
|
84
|
+
this.observationCallbacks.set(propPath, observers.filter((ob) => ob !== observeCallback));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* updates a portion of your state
|
|
88
|
+
*/
|
|
89
|
+
update(updatePath, valUpdater) {
|
|
90
|
+
const prevVal = this.read(updatePath);
|
|
91
|
+
let newVal = null;
|
|
92
|
+
this.doSomethingToPath(updatePath, (oldVal) => {
|
|
93
|
+
const n = valUpdater(oldVal);
|
|
94
|
+
newVal = n;
|
|
95
|
+
return n;
|
|
96
|
+
});
|
|
97
|
+
for (const cb of this.observationCallbacks.get(updatePath) ?? []) {
|
|
98
|
+
cb(newVal, prevVal, updatePath);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -3,10 +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
|
-
"
|
|
7
|
-
"types": "./dist/cjs/index.d.ts",
|
|
8
|
-
"typings": "./dist/totally-tubular.d.ts",
|
|
9
|
-
"version": "0.3.2",
|
|
6
|
+
"version": "0.4.0",
|
|
10
7
|
"keywords": [
|
|
11
8
|
"state",
|
|
12
9
|
"management",
|
|
@@ -30,71 +27,52 @@
|
|
|
30
27
|
"dist/"
|
|
31
28
|
],
|
|
32
29
|
"scripts": {
|
|
33
|
-
"build": "
|
|
30
|
+
"build": "rm -rf dist/ && tsc --project ./tsconfig.build.json",
|
|
34
31
|
"clean": "rm -rf dist/",
|
|
35
32
|
"test": "bun test",
|
|
36
33
|
"test:watch": "bun test --watch"
|
|
37
34
|
},
|
|
38
35
|
"devDependencies": {
|
|
39
36
|
"@better-builds/lets-version": "^1.6.0",
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
42
|
-
"@commitlint/
|
|
43
|
-
"@
|
|
44
|
-
"@happy-dom/global-registrator": "^20.8.3",
|
|
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",
|
|
45
41
|
"@testing-library/react": "^16.3.2",
|
|
46
42
|
"@testing-library/user-event": "^14.6.1",
|
|
47
43
|
"@types/bun": "latest",
|
|
48
|
-
"@types/react": "
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
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",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
52
50
|
},
|
|
53
|
-
"
|
|
51
|
+
"peerDependencies": {
|
|
54
52
|
"react": ">=18"
|
|
55
53
|
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"react": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
56
59
|
"exports": {
|
|
57
60
|
".": {
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
"default": "./dist/cjs/index.cjs"
|
|
61
|
-
},
|
|
62
|
-
"import": {
|
|
63
|
-
"types": "./dist/esm/index.d.ts",
|
|
64
|
-
"default": "./dist/esm/index.mjs"
|
|
65
|
-
}
|
|
61
|
+
"types": "./dist/index.d.ts",
|
|
62
|
+
"default": "./dist/index.js"
|
|
66
63
|
},
|
|
67
64
|
"./react": {
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
"default": "./dist/cjs/react.cjs"
|
|
71
|
-
},
|
|
72
|
-
"import": {
|
|
73
|
-
"types": "./dist/esm/react.d.ts",
|
|
74
|
-
"default": "./dist/esm/react.mjs"
|
|
75
|
-
}
|
|
65
|
+
"types": "./dist/react.d.ts",
|
|
66
|
+
"default": "./dist/react.js"
|
|
76
67
|
},
|
|
77
68
|
"./totally-tubular": {
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
"default": "./dist/cjs/totally-tubular.cjs"
|
|
81
|
-
},
|
|
82
|
-
"import": {
|
|
83
|
-
"types": "./dist/esm/totally-tubular.d.ts",
|
|
84
|
-
"default": "./dist/esm/totally-tubular.mjs"
|
|
85
|
-
}
|
|
69
|
+
"types": "./dist/totally-tubular.d.ts",
|
|
70
|
+
"default": "./dist/totally-tubular.js"
|
|
86
71
|
},
|
|
87
72
|
"./types": {
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
"default": "./dist/cjs/types.cjs"
|
|
91
|
-
},
|
|
92
|
-
"import": {
|
|
93
|
-
"types": "./dist/esm/types.d.ts",
|
|
94
|
-
"default": "./dist/esm/types.mjs"
|
|
95
|
-
}
|
|
73
|
+
"types": "./dist/types.d.ts",
|
|
74
|
+
"default": "./dist/types.js"
|
|
96
75
|
},
|
|
97
76
|
"./package.json": "./package.json"
|
|
98
|
-
}
|
|
99
|
-
"module": "./dist/esm/index.mjs"
|
|
77
|
+
}
|
|
100
78
|
}
|
package/dist/cjs/index.cjs
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
Object.defineProperty(exports, "Tubular", {
|
|
6
|
-
enumerable: true,
|
|
7
|
-
get: function() {
|
|
8
|
-
return _totallytubular.Tubular;
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
const _totallytubular = require("./totally-tubular.cjs");
|
package/dist/cjs/package.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "type": "commonjs" }
|
package/dist/cjs/react.cjs
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
Object.defineProperty(exports, "useTubular", {
|
|
6
|
-
enumerable: true,
|
|
7
|
-
get: function() {
|
|
8
|
-
return useTubular;
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
const _react = require("./react.cjs");
|
|
12
|
-
function useTubular(tubular, key) {
|
|
13
|
-
/** state */ const [val, setVal] = (0, _react.useState)(()=>tubular.read(key));
|
|
14
|
-
/** callbacks */ const handleUpdateVal = (0, _react.useCallback)((newValOrCallback)=>{
|
|
15
|
-
if (typeof newValOrCallback === 'function') {
|
|
16
|
-
tubular.update(key, newValOrCallback);
|
|
17
|
-
} else {
|
|
18
|
-
tubular.update(key, ()=>newValOrCallback);
|
|
19
|
-
}
|
|
20
|
-
}, [
|
|
21
|
-
tubular,
|
|
22
|
-
key
|
|
23
|
-
]);
|
|
24
|
-
/** effects */ (0, _react.useEffect)(()=>{
|
|
25
|
-
// Sync in case the value changed between render and effect registration
|
|
26
|
-
setVal(tubular.read(key));
|
|
27
|
-
const observeCb = (newVal)=>{
|
|
28
|
-
setVal(newVal);
|
|
29
|
-
};
|
|
30
|
-
tubular.observe(key, observeCb);
|
|
31
|
-
return ()=>{
|
|
32
|
-
tubular.unobserve(key, observeCb);
|
|
33
|
-
};
|
|
34
|
-
}, [
|
|
35
|
-
tubular,
|
|
36
|
-
key
|
|
37
|
-
]);
|
|
38
|
-
return [
|
|
39
|
-
val,
|
|
40
|
-
handleUpdateVal
|
|
41
|
-
];
|
|
42
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
Object.defineProperty(exports, "Tubular", {
|
|
6
|
-
enumerable: true,
|
|
7
|
-
get: function() {
|
|
8
|
-
return Tubular;
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
class Tubular {
|
|
12
|
-
observationCallbacks = new Map();
|
|
13
|
-
state;
|
|
14
|
-
constructor(initialState){
|
|
15
|
-
this.state = initialState;
|
|
16
|
-
}
|
|
17
|
-
doSomethingToPath(readPath, doSomething) {
|
|
18
|
-
let thing = this.state;
|
|
19
|
-
if (typeof readPath !== 'string') {
|
|
20
|
-
return this.getPortionOfStateByPath(thing[readPath]);
|
|
21
|
-
}
|
|
22
|
-
const splitPath = readPath.split('.');
|
|
23
|
-
const len = splitPath.length;
|
|
24
|
-
for(let i = 0; i < len; i++){
|
|
25
|
-
const p = splitPath[i] || '';
|
|
26
|
-
if (i === len - 1) {
|
|
27
|
-
if (thing) thing[p] = doSomething(thing[p]);
|
|
28
|
-
} else thing = thing?.[p];
|
|
29
|
-
}
|
|
30
|
-
for (const p of splitPath){
|
|
31
|
-
thing = thing?.[p];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
getPortionOfStateByPath(readPath) {
|
|
35
|
-
let thing = this.state;
|
|
36
|
-
if (typeof readPath !== 'string') {
|
|
37
|
-
return this.getPortionOfStateByPath(thing[readPath]);
|
|
38
|
-
}
|
|
39
|
-
const splitPath = readPath.split('.');
|
|
40
|
-
for (const p of splitPath){
|
|
41
|
-
thing = thing?.[p];
|
|
42
|
-
}
|
|
43
|
-
return thing ?? null;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* observe changes to some part of state that was updated
|
|
47
|
-
*/ observe(propPath, observeCallback) {
|
|
48
|
-
const prevForPath = this.observationCallbacks.get(propPath) ?? [];
|
|
49
|
-
this.observationCallbacks.set(propPath, [
|
|
50
|
-
...prevForPath,
|
|
51
|
-
observeCallback
|
|
52
|
-
]);
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* reads a portion of your state
|
|
56
|
-
*/ read(readPath) {
|
|
57
|
-
return this.getPortionOfStateByPath(readPath);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* disconnects an observer callback from totally tubular
|
|
61
|
-
*/ unobserve(propPath, observeCallback) {
|
|
62
|
-
const observers = this.observationCallbacks.get(propPath);
|
|
63
|
-
if (!observers?.length) return;
|
|
64
|
-
this.observationCallbacks.set(propPath, observers.filter((ob)=>ob !== observeCallback));
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* updates a portion of your state
|
|
68
|
-
*/ update(updatePath, valUpdater) {
|
|
69
|
-
const prevVal = this.read(updatePath);
|
|
70
|
-
let newVal = null;
|
|
71
|
-
this.doSomethingToPath(updatePath, (oldVal)=>{
|
|
72
|
-
const n = valUpdater(oldVal);
|
|
73
|
-
newVal = n;
|
|
74
|
-
return n;
|
|
75
|
-
});
|
|
76
|
-
for (const cb of this.observationCallbacks.get(updatePath) ?? []){
|
|
77
|
-
cb(newVal, prevVal, updatePath);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { AllObjectKeys, ObservationCallback, PropType } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* A totally tubular instance that tracks a top-level
|
|
4
|
-
* set of keys and values.
|
|
5
|
-
* if you want to support nested or "deep" state updates and observations,
|
|
6
|
-
* you should construct your state to be a series of Tubular instances
|
|
7
|
-
*/
|
|
8
|
-
export declare class Tubular<T extends object> {
|
|
9
|
-
private observationCallbacks;
|
|
10
|
-
private state;
|
|
11
|
-
constructor(initialState: T);
|
|
12
|
-
private doSomethingToPath;
|
|
13
|
-
private getPortionOfStateByPath;
|
|
14
|
-
/**
|
|
15
|
-
* observe changes to some part of state that was updated
|
|
16
|
-
*/
|
|
17
|
-
observe<K extends string>(propPath: K & AllObjectKeys<T>, observeCallback: ObservationCallback<T>): void;
|
|
18
|
-
/**
|
|
19
|
-
* reads a portion of your state
|
|
20
|
-
*/
|
|
21
|
-
read<K extends string>(readPath: K & AllObjectKeys<T>): PropType<T, K> | null;
|
|
22
|
-
/**
|
|
23
|
-
* disconnects an observer callback from totally tubular
|
|
24
|
-
*/
|
|
25
|
-
unobserve<K extends string>(propPath: K & AllObjectKeys<T>, observeCallback: ObservationCallback<T>): void;
|
|
26
|
-
/**
|
|
27
|
-
* updates a portion of your state
|
|
28
|
-
*/
|
|
29
|
-
update<K extends string>(updatePath: K & AllObjectKeys<T>, valUpdater: (oldVal: PropType<T, K>) => PropType<T, K>): void;
|
|
30
|
-
}
|
package/dist/cjs/types.cjs
DELETED
package/dist/esm/index.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { Tubular } from './totally-tubular.mjs';
|
package/dist/esm/package.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "type": "module" }
|
package/dist/esm/react.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { Tubular } from './totally-tubular.js';
|
|
2
|
-
import type { AllObjectKeys, PropType } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* A React hook that binds to a {@link Tubular} instance and a specific key path
|
|
5
|
-
* within its state.
|
|
6
|
-
*
|
|
7
|
-
* Returns a `[value, setter]` tuple that works exactly like React's built-in
|
|
8
|
-
* `useState`, except reads and writes go through the Tubular store — so every
|
|
9
|
-
* component (and non-React observer) watching the same path is notified on
|
|
10
|
-
* every update.
|
|
11
|
-
*
|
|
12
|
-
* The setter accepts either a new value directly or an updater function that
|
|
13
|
-
* receives the current value and returns the next one.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```tsx
|
|
17
|
-
* const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
|
|
18
|
-
*
|
|
19
|
-
* function Counter() {
|
|
20
|
-
* const [count, setCount] = useTubular(store, 'count');
|
|
21
|
-
* return <button onClick={() => setCount(n => (n ?? 0) + 1)}>{count}</button>;
|
|
22
|
-
* }
|
|
23
|
-
*
|
|
24
|
-
* function Greeting() {
|
|
25
|
-
* const [name] = useTubular(store, 'user.name');
|
|
26
|
-
* return <p>Hello, {name}!</p>;
|
|
27
|
-
* }
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
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];
|
package/dist/esm/react.mjs
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from './react.mjs';
|
|
2
|
-
/**
|
|
3
|
-
* A React hook that binds to a {@link Tubular} instance and a specific key path
|
|
4
|
-
* within its state.
|
|
5
|
-
*
|
|
6
|
-
* Returns a `[value, setter]` tuple that works exactly like React's built-in
|
|
7
|
-
* `useState`, except reads and writes go through the Tubular store — so every
|
|
8
|
-
* component (and non-React observer) watching the same path is notified on
|
|
9
|
-
* every update.
|
|
10
|
-
*
|
|
11
|
-
* The setter accepts either a new value directly or an updater function that
|
|
12
|
-
* receives the current value and returns the next one.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```tsx
|
|
16
|
-
* const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
|
|
17
|
-
*
|
|
18
|
-
* function Counter() {
|
|
19
|
-
* const [count, setCount] = useTubular(store, 'count');
|
|
20
|
-
* return <button onClick={() => setCount(n => (n ?? 0) + 1)}>{count}</button>;
|
|
21
|
-
* }
|
|
22
|
-
*
|
|
23
|
-
* function Greeting() {
|
|
24
|
-
* const [name] = useTubular(store, 'user.name');
|
|
25
|
-
* return <p>Hello, {name}!</p>;
|
|
26
|
-
* }
|
|
27
|
-
* ```
|
|
28
|
-
*/ export function useTubular(tubular, key) {
|
|
29
|
-
/** state */ const [val, setVal] = useState(()=>tubular.read(key));
|
|
30
|
-
/** callbacks */ const handleUpdateVal = useCallback((newValOrCallback)=>{
|
|
31
|
-
if (typeof newValOrCallback === 'function') {
|
|
32
|
-
tubular.update(key, newValOrCallback);
|
|
33
|
-
} else {
|
|
34
|
-
tubular.update(key, ()=>newValOrCallback);
|
|
35
|
-
}
|
|
36
|
-
}, [
|
|
37
|
-
tubular,
|
|
38
|
-
key
|
|
39
|
-
]);
|
|
40
|
-
/** effects */ useEffect(()=>{
|
|
41
|
-
// Sync in case the value changed between render and effect registration
|
|
42
|
-
setVal(tubular.read(key));
|
|
43
|
-
const observeCb = (newVal)=>{
|
|
44
|
-
setVal(newVal);
|
|
45
|
-
};
|
|
46
|
-
tubular.observe(key, observeCb);
|
|
47
|
-
return ()=>{
|
|
48
|
-
tubular.unobserve(key, observeCb);
|
|
49
|
-
};
|
|
50
|
-
}, [
|
|
51
|
-
tubular,
|
|
52
|
-
key
|
|
53
|
-
]);
|
|
54
|
-
return [
|
|
55
|
-
val,
|
|
56
|
-
handleUpdateVal
|
|
57
|
-
];
|
|
58
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A totally tubular instance that tracks a top-level
|
|
3
|
-
* set of keys and values.
|
|
4
|
-
* if you want to support nested or "deep" state updates and observations,
|
|
5
|
-
* you should construct your state to be a series of Tubular instances
|
|
6
|
-
*/ export class Tubular {
|
|
7
|
-
observationCallbacks = new Map();
|
|
8
|
-
state;
|
|
9
|
-
constructor(initialState){
|
|
10
|
-
this.state = initialState;
|
|
11
|
-
}
|
|
12
|
-
doSomethingToPath(readPath, doSomething) {
|
|
13
|
-
let thing = this.state;
|
|
14
|
-
if (typeof readPath !== 'string') {
|
|
15
|
-
return this.getPortionOfStateByPath(thing[readPath]);
|
|
16
|
-
}
|
|
17
|
-
const splitPath = readPath.split('.');
|
|
18
|
-
const len = splitPath.length;
|
|
19
|
-
for(let i = 0; i < len; i++){
|
|
20
|
-
const p = splitPath[i] || '';
|
|
21
|
-
if (i === len - 1) {
|
|
22
|
-
if (thing) thing[p] = doSomething(thing[p]);
|
|
23
|
-
} else thing = thing?.[p];
|
|
24
|
-
}
|
|
25
|
-
for (const p of splitPath){
|
|
26
|
-
thing = thing?.[p];
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
getPortionOfStateByPath(readPath) {
|
|
30
|
-
let thing = this.state;
|
|
31
|
-
if (typeof readPath !== 'string') {
|
|
32
|
-
return this.getPortionOfStateByPath(thing[readPath]);
|
|
33
|
-
}
|
|
34
|
-
const splitPath = readPath.split('.');
|
|
35
|
-
for (const p of splitPath){
|
|
36
|
-
thing = thing?.[p];
|
|
37
|
-
}
|
|
38
|
-
return thing ?? null;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* observe changes to some part of state that was updated
|
|
42
|
-
*/ observe(propPath, observeCallback) {
|
|
43
|
-
const prevForPath = this.observationCallbacks.get(propPath) ?? [];
|
|
44
|
-
this.observationCallbacks.set(propPath, [
|
|
45
|
-
...prevForPath,
|
|
46
|
-
observeCallback
|
|
47
|
-
]);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* reads a portion of your state
|
|
51
|
-
*/ read(readPath) {
|
|
52
|
-
return this.getPortionOfStateByPath(readPath);
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* disconnects an observer callback from totally tubular
|
|
56
|
-
*/ unobserve(propPath, observeCallback) {
|
|
57
|
-
const observers = this.observationCallbacks.get(propPath);
|
|
58
|
-
if (!observers?.length) return;
|
|
59
|
-
this.observationCallbacks.set(propPath, observers.filter((ob)=>ob !== observeCallback));
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* updates a portion of your state
|
|
63
|
-
*/ update(updatePath, valUpdater) {
|
|
64
|
-
const prevVal = this.read(updatePath);
|
|
65
|
-
let newVal = null;
|
|
66
|
-
this.doSomethingToPath(updatePath, (oldVal)=>{
|
|
67
|
-
const n = valUpdater(oldVal);
|
|
68
|
-
newVal = n;
|
|
69
|
-
return n;
|
|
70
|
-
});
|
|
71
|
-
for (const cb of this.observationCallbacks.get(updatePath) ?? []){
|
|
72
|
-
cb(newVal, prevVal, updatePath);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
package/dist/esm/types.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
export type TSPrimitve = string | number | bigint | boolean | symbol | undefined | null;
|
|
2
|
-
/**
|
|
3
|
-
* Single-check collection guard.
|
|
4
|
-
* `readonly any[]` covers both `any[]` and `ReadonlyArray<any>` since `Array<T>`
|
|
5
|
-
* extends `ReadonlyArray<T>`. One distributive check instead of four sequential ones.
|
|
6
|
-
*/
|
|
7
|
-
type IsCollection<T> = T extends readonly any[] | Map<any, any> | Set<any> ? true : false;
|
|
8
|
-
/**
|
|
9
|
-
* Produces every dot-separated key path into T as a string union.
|
|
10
|
-
*
|
|
11
|
-
* - The `Depth` tuple acts as a recursion counter: when its length reaches 5 the
|
|
12
|
-
* type resolves to `never`, which caps the union size and prevents the language
|
|
13
|
-
* server from hanging on deeply-nested or self-referential shapes.
|
|
14
|
-
* - Top-level keys are included as bare strings (`K`), so this type supersedes
|
|
15
|
-
* `keyof T & string` — there is no need for a separate union with `keyof T`.
|
|
16
|
-
* - Collections (arrays, Map, Set) are terminated at their key rather than
|
|
17
|
-
* recursed into.
|
|
18
|
-
* - Returning `never` (not `''`) from every terminal branch avoids the need for
|
|
19
|
-
* a secondary `Extract<D, string>` pass over the full union.
|
|
20
|
-
*/
|
|
21
|
-
export type DotNestedKeys<T, Depth extends 1[] = []> = Depth['length'] extends 5 ? never : T extends object ? IsCollection<T> extends true ? never : {
|
|
22
|
-
[K in keyof T & string]: IsCollection<T[K]> extends true ? K : T[K] extends object ? K | `${K}.${DotNestedKeys<T[K], [...Depth, 1]>}` : K;
|
|
23
|
-
}[keyof T & string] : never;
|
|
24
|
-
/**
|
|
25
|
-
* All valid key paths into T (top-level and dot-nested).
|
|
26
|
-
*
|
|
27
|
-
* `DotNestedKeys<T>` already emits top-level string keys alongside nested paths,
|
|
28
|
-
* so a `keyof T` union is redundant and was previously responsible for duplicate
|
|
29
|
-
* entries that the checker had to deduplicate on every instantiation.
|
|
30
|
-
*/
|
|
31
|
-
export type AllObjectKeys<T> = DotNestedKeys<T>;
|
|
32
|
-
/**
|
|
33
|
-
* Resolves the value type at a dot-separated path P within T.
|
|
34
|
-
*
|
|
35
|
-
* The intermediate `Rest extends AllObjectKeys<T[Head]>` guard that previously
|
|
36
|
-
* existed here forced a full re-evaluation of `AllObjectKeys` at every recursive
|
|
37
|
-
* level. It has been removed: if `Rest` is not a valid path the recursion
|
|
38
|
-
* naturally returns `never`, which is the correct result.
|
|
39
|
-
*/
|
|
40
|
-
export type PropType<T, P extends string> = P extends keyof T ? T[P] : P extends `${infer Head}.${infer Rest}` ? Head extends keyof T ? PropType<T[Head], Rest> : never : never;
|
|
41
|
-
/**
|
|
42
|
-
* Callback invoked by Tubular whenever an observed path changes.
|
|
43
|
-
*
|
|
44
|
-
* The previous definition used defaulted type parameters
|
|
45
|
-
* (`K = AllObjectKeys<T>`, `V = PropType<T, K & string>`). Default type
|
|
46
|
-
* parameters are eagerly evaluated by the checker at *every* reference to
|
|
47
|
-
* `ObservationCallback<T>`, causing a full `AllObjectKeys<T>` + distributed
|
|
48
|
-
* `PropType` expansion each time. Using `unknown` for the value parameters
|
|
49
|
-
* defers any narrowing to call sites that actually need it, which is a far
|
|
50
|
-
* smaller surface area.
|
|
51
|
-
*/
|
|
52
|
-
export type ObservationCallback<T> = (newVal: unknown, oldVal: unknown, propPath: AllObjectKeys<T>) => void;
|
|
53
|
-
export {};
|
package/dist/esm/types.mjs
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Callback invoked by Tubular whenever an observed path changes.
|
|
3
|
-
*
|
|
4
|
-
* The previous definition used defaulted type parameters
|
|
5
|
-
* (`K = AllObjectKeys<T>`, `V = PropType<T, K & string>`). Default type
|
|
6
|
-
* parameters are eagerly evaluated by the checker at *every* reference to
|
|
7
|
-
* `ObservationCallback<T>`, causing a full `AllObjectKeys<T>` + distributed
|
|
8
|
-
* `PropType` expansion each time. Using `unknown` for the value parameters
|
|
9
|
-
* defers any narrowing to call sites that actually need it, which is a far
|
|
10
|
-
* smaller surface area.
|
|
11
|
-
*/ export { };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|