totally-tubular 0.3.1 → 0.3.3

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 @@
1
+ export { Tubular } from './totally-tubular.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { Tubular } from './totally-tubular.js';
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from './react.mjs';
1
+ import { useCallback, useEffect, useState } from 'react';
2
2
  /**
3
3
  * A React hook that binds to a {@link Tubular} instance and a specific key path
4
4
  * within its state.
@@ -25,34 +25,30 @@ import { useCallback, useEffect, useState } from './react.mjs';
25
25
  * return <p>Hello, {name}!</p>;
26
26
  * }
27
27
  * ```
28
- */ export function useTubular(tubular, key) {
29
- /** state */ const [val, setVal] = useState(()=>tubular.read(key));
30
- /** callbacks */ const handleUpdateVal = useCallback((newValOrCallback)=>{
28
+ */
29
+ export function useTubular(tubular, key) {
30
+ /** state */
31
+ const [val, setVal] = useState(() => tubular.read(key));
32
+ /** callbacks */
33
+ const handleUpdateVal = useCallback((newValOrCallback) => {
31
34
  if (typeof newValOrCallback === 'function') {
32
35
  tubular.update(key, newValOrCallback);
33
- } else {
34
- tubular.update(key, ()=>newValOrCallback);
35
36
  }
36
- }, [
37
- tubular,
38
- key
39
- ]);
40
- /** effects */ useEffect(()=>{
37
+ else {
38
+ tubular.update(key, () => newValOrCallback);
39
+ }
40
+ }, [tubular, key]);
41
+ /** effects */
42
+ useEffect(() => {
41
43
  // Sync in case the value changed between render and effect registration
42
44
  setVal(tubular.read(key));
43
- const observeCb = (newVal)=>{
45
+ const observeCb = (newVal) => {
44
46
  setVal(newVal);
45
47
  };
46
48
  tubular.observe(key, observeCb);
47
- return ()=>{
49
+ return () => {
48
50
  tubular.unobserve(key, observeCb);
49
51
  };
50
- }, [
51
- tubular,
52
- key
53
- ]);
54
- return [
55
- val,
56
- handleUpdateVal
57
- ];
52
+ }, [tubular, key]);
53
+ return [val, handleUpdateVal];
58
54
  }
@@ -3,10 +3,11 @@
3
3
  * set of keys and values.
4
4
  * if you want to support nested or "deep" state updates and observations,
5
5
  * you should construct your state to be a series of Tubular instances
6
- */ export class Tubular {
6
+ */
7
+ export class Tubular {
7
8
  observationCallbacks = new Map();
8
9
  state;
9
- constructor(initialState){
10
+ constructor(initialState) {
10
11
  this.state = initialState;
11
12
  }
12
13
  doSomethingToPath(readPath, doSomething) {
@@ -16,13 +17,16 @@
16
17
  }
17
18
  const splitPath = readPath.split('.');
18
19
  const len = splitPath.length;
19
- for(let i = 0; i < len; i++){
20
+ for (let i = 0; i < len; i++) {
20
21
  const p = splitPath[i] || '';
21
22
  if (i === len - 1) {
22
- if (thing) thing[p] = doSomething(thing[p]);
23
- } else thing = thing?.[p];
23
+ if (thing)
24
+ thing[p] = doSomething(thing[p]);
25
+ }
26
+ else
27
+ thing = thing?.[p];
24
28
  }
25
- for (const p of splitPath){
29
+ for (const p of splitPath) {
26
30
  thing = thing?.[p];
27
31
  }
28
32
  }
@@ -32,43 +36,45 @@
32
36
  return this.getPortionOfStateByPath(thing[readPath]);
33
37
  }
34
38
  const splitPath = readPath.split('.');
35
- for (const p of splitPath){
39
+ for (const p of splitPath) {
36
40
  thing = thing?.[p];
37
41
  }
38
42
  return thing ?? null;
39
43
  }
40
44
  /**
41
- * observe changes to some part of state that was updated
42
- */ observe(propPath, observeCallback) {
45
+ * observe changes to some part of state that was updated
46
+ */
47
+ observe(propPath, observeCallback) {
43
48
  const prevForPath = this.observationCallbacks.get(propPath) ?? [];
44
- this.observationCallbacks.set(propPath, [
45
- ...prevForPath,
46
- observeCallback
47
- ]);
49
+ this.observationCallbacks.set(propPath, [...prevForPath, observeCallback]);
48
50
  }
49
51
  /**
50
- * reads a portion of your state
51
- */ read(readPath) {
52
+ * reads a portion of your state
53
+ */
54
+ read(readPath) {
52
55
  return this.getPortionOfStateByPath(readPath);
53
56
  }
54
57
  /**
55
- * disconnects an observer callback from totally tubular
56
- */ unobserve(propPath, observeCallback) {
58
+ * disconnects an observer callback from totally tubular
59
+ */
60
+ unobserve(propPath, observeCallback) {
57
61
  const observers = this.observationCallbacks.get(propPath);
58
- if (!observers?.length) return;
59
- this.observationCallbacks.set(propPath, observers.filter((ob)=>ob !== observeCallback));
62
+ if (!observers?.length)
63
+ return;
64
+ this.observationCallbacks.set(propPath, observers.filter((ob) => ob !== observeCallback));
60
65
  }
61
66
  /**
62
- * updates a portion of your state
63
- */ update(updatePath, valUpdater) {
67
+ * updates a portion of your state
68
+ */
69
+ update(updatePath, valUpdater) {
64
70
  const prevVal = this.read(updatePath);
65
71
  let newVal = null;
66
- this.doSomethingToPath(updatePath, (oldVal)=>{
72
+ this.doSomethingToPath(updatePath, (oldVal) => {
67
73
  const n = valUpdater(oldVal);
68
74
  newVal = n;
69
75
  return n;
70
76
  });
71
- for (const cb of this.observationCallbacks.get(updatePath) ?? []){
77
+ for (const cb of this.observationCallbacks.get(updatePath) ?? []) {
72
78
  cb(newVal, prevVal, updatePath);
73
79
  }
74
80
  }
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
- "main": "./dist/totally-tubular.js",
7
- "types": "./dist/totally-tubular.d.ts",
8
- "typings": "./dist/totally-tubular.d.ts",
9
- "version": "0.3.1",
6
+ "version": "0.3.3",
10
7
  "keywords": [
11
8
  "state",
12
9
  "management",
@@ -30,59 +27,49 @@
30
27
  "dist/"
31
28
  ],
32
29
  "scripts": {
33
- "build": "ts-duality --clean",
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
- "@better-builds/ts-duality": "^1.3.2",
41
- "@biomejs/biome": "^2.4.6",
42
- "@commitlint/cli": "^20.4.3",
43
- "@commitlint/config-conventional": "^20.4.3",
44
- "@happy-dom/global-registrator": "^20.8.3",
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",
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": ">=18",
49
- "happy-dom": "^20.8.3",
44
+ "@types/react": "^19.2.14",
45
+ "happy-dom": "^20.9.0",
50
46
  "react": ">=18",
51
- "typescript": "^5"
47
+ "typescript": "^5.9.3"
52
48
  },
53
- "optionalDependencies": {
49
+ "peerDependencies": {
54
50
  "react": ">=18"
55
51
  },
52
+ "peerDependenciesMeta": {
53
+ "react": {
54
+ "optional": true
55
+ }
56
+ },
56
57
  "exports": {
58
+ ".": {
59
+ "types": "./dist/index.d.ts",
60
+ "default": "./dist/index.js"
61
+ },
57
62
  "./react": {
58
- "require": {
59
- "types": "./dist/cjs/react.d.ts",
60
- "default": "./dist/cjs/react.cjs"
61
- },
62
- "import": {
63
- "types": "./dist/esm/react.d.ts",
64
- "default": "./dist/esm/react.mjs"
65
- }
63
+ "types": "./dist/react.d.ts",
64
+ "default": "./dist/react.js"
66
65
  },
67
66
  "./totally-tubular": {
68
- "require": {
69
- "types": "./dist/cjs/totally-tubular.d.ts",
70
- "default": "./dist/cjs/totally-tubular.cjs"
71
- },
72
- "import": {
73
- "types": "./dist/esm/totally-tubular.d.ts",
74
- "default": "./dist/esm/totally-tubular.mjs"
75
- }
67
+ "types": "./dist/totally-tubular.d.ts",
68
+ "default": "./dist/totally-tubular.js"
76
69
  },
77
70
  "./types": {
78
- "require": {
79
- "types": "./dist/cjs/types.d.ts",
80
- "default": "./dist/cjs/types.cjs"
81
- },
82
- "import": {
83
- "types": "./dist/esm/types.d.ts",
84
- "default": "./dist/esm/types.mjs"
85
- }
71
+ "types": "./dist/types.d.ts",
72
+ "default": "./dist/types.js"
86
73
  },
87
74
  "./package.json": "./package.json"
88
75
  }
@@ -1 +0,0 @@
1
- { "type": "commonjs" }
@@ -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,4 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
@@ -1 +0,0 @@
1
- { "type": "module" }
@@ -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];
@@ -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
- }
@@ -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 {};
@@ -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