totally-tubular 0.1.2 → 0.3.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
@@ -1,24 +1,14 @@
1
1
  # totally-tubular
2
2
 
3
- A completely framework-agnostic, JavaScript / TypeScript state management and update broadcasting library with zero dependencies, solid performance and a micro file size.
3
+ A framework-agnostic JavaScript / TypeScript state management library with zero dependencies, a tiny file size, and solid performance.
4
4
 
5
- **Woahhhh**
5
+ ## Why another state management library?
6
6
 
7
- ## But why is there _another_ state management library? I already use XYZ library.
7
+ State management has a tendency to get complicated fast. This library is an experiment in keeping things simple:
8
8
 
9
- Great! I'm glad you found something that works for you!
10
- This library (`totally-tubular`) was an experiment that manifested out of feeling
11
- like state management is overcomplicated these days and often riddled with "gotchas"
12
- and easy-to-oopsie performance faux pas.
13
-
14
- Additionally, our shipped JavaScript bundles should be getting **smaller**, not **bigger**,
15
- but it's all-too-easy, especially when crafting enterprise software, to keep shoving more libraries
16
- into your bundle with questionable benefits, when there often exists a much simpler, closer-to-vanilla
17
- way to do what you need to do.
18
-
19
- As a final point, your state management system should be fully-portable, so if you swap out
20
- your rendering layer, be it React, Svelte, Angular, or something else, the meat-and-potatoes of
21
- your state management shouldn't need to be fully rewritten.
9
+ - **Portable.** Your store is plain TypeScript. If you swap React for Svelte, or add a Node.js background worker, the store itself doesn't change.
10
+ - **Small.** No framework coupling, no middleware pipeline, no magic — just a class that holds state, lets you read it, update it, and watch it for changes.
11
+ - **Honest.** It mutates your state in place for performance. If you want immutability, return new objects from your updater functions.
22
12
 
23
13
  ## Install
24
14
 
@@ -26,12 +16,25 @@ your state management shouldn't need to be fully rewritten.
26
16
  npm i totally-tubular --save
27
17
  ```
28
18
 
19
+ ## Core concepts
20
+
21
+ A `Tubular` instance wraps an object and gives you four things:
22
+
23
+ | Method | What it does |
24
+ |---|---|
25
+ | `read(path)` | Returns the current value at a dot-separated path |
26
+ | `update(path, fn)` | Calls `fn` with the current value, stores the return value, then notifies all observers |
27
+ | `observe(path, fn)` | Registers a callback that fires whenever `path` is updated |
28
+ | `unobserve(path, fn)` | Removes a previously registered callback |
29
+
30
+ 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
+
29
32
  ## Quick start
30
33
 
31
34
  ```typescript
32
35
  import { Tubular } from "totally-tubular";
33
36
 
34
- interface MyStateShape {
37
+ interface AppState {
35
38
  isActive: boolean;
36
39
  count: number;
37
40
  meta: {
@@ -40,107 +43,89 @@ interface MyStateShape {
40
43
  name: string;
41
44
  }
42
45
 
43
- const initialState: MyStateShape = {
46
+ const store = new Tubular<AppState>({
44
47
  isActive: false,
45
48
  count: 123,
46
- meta: {
47
- title: "Director",
48
- },
49
+ meta: { title: "Director" },
49
50
  name: "Test User",
50
- };
51
-
52
- const t = new Tubular(initialState);
51
+ });
53
52
 
54
- // listen to any of the keys / values in your state object for changes
55
- // and do whatever you need with the values
56
- t.observe("meta.title", (newVal, oldVal, propPath) =>
57
- console.info(propPath, "was updated from", oldVal, "to", newVal),
53
+ // Watch a value for changes
54
+ store.observe("meta.title", (newVal, oldVal) =>
55
+ console.info("title changed from", oldVal, "to", newVal),
58
56
  );
59
57
 
60
- console.info(t.read("meta.title")); // will print 'Director'
58
+ console.info(store.read("meta.title")); // "Director"
59
+
60
+ // Update a value — all observers are notified immediately after
61
+ store.update("meta.title", (prev) => `Supreme ${prev}`);
61
62
 
62
- // update any of the keys / values in your state object.
63
- // all observers will be notified after the value has been set
64
- t.update("meta.title", (prev) => `Supreme ${prev}`);
65
- console.info(t.read("meta.title")); // will print the updated 'Supreme Director' value
63
+ console.info(store.read("meta.title")); // "Supreme Director"
66
64
  ```
67
65
 
68
- ### React example
66
+ ## React
69
67
 
70
- Since React is one of the most popular UI libraries, here is how you might mimick the vanilla example, above, but in a React application.
68
+ Import `useTubular` from `totally-tubular/react`. It works exactly like React's
69
+ built-in `useState` — you get back a `[value, setter]` tuple — except the value
70
+ comes from your store and every component watching the same path re-renders when
71
+ it changes.
71
72
 
72
73
  ```tsx
73
74
  import { Tubular } from "totally-tubular";
74
- import { useEffect, useState } from "react";
75
+ import { useTubular } from "totally-tubular/react";
75
76
 
76
- interface MyState {
77
- permissions: {
78
- entitlements: string[];
79
- };
80
- time: number;
81
- username: string;
77
+ interface AppState {
78
+ user: { name: string };
79
+ count: number;
82
80
  }
83
81
 
84
- const initialState: MyState = {
85
- permissions: { entitlements: [] },
86
- username: "Test User",
87
- time: Date.now(),
88
- };
89
-
90
- const t = new Tubular<MyState>(initialState);
82
+ // Create the store once, outside your components
83
+ const store = new Tubular<AppState>({
84
+ user: { name: "Alice" },
85
+ count: 0,
86
+ });
91
87
 
92
- const getTime = (timestamp: number) => {
93
- const d = new Date();
94
- d.setTime(timestamp);
95
- return d.toLocaleTimeString();
96
- };
97
-
98
- export default function App() {
99
- /** state */
100
- const [username, setUsername] = useState(initialState.username);
101
- const [time, setTime] = useState(initialState.time);
102
-
103
- /** effects */
104
- useEffect(() => {
105
- t.observe("username", (v) => setUsername(v));
106
- t.observe("time", (t) => setTime(t));
107
- }, []);
108
-
109
- useEffect(() => {
110
- const interval = setInterval(() => t.update("time", (oldVal) => oldVal + 1000), 1000);
111
-
112
- return () => {
113
- clearInterval(interval);
114
- };
115
- }, []);
88
+ function Counter() {
89
+ const [count, setCount] = useTubular(store, "count");
116
90
 
117
91
  return (
118
- <div>
119
- <strong>User: {username}</strong>
120
- <div>The time: {getTime(time)}</div>
121
- </div>
92
+ <button onClick={() => setCount((n) => (n ?? 0) + 1)}>
93
+ Clicked {count} times
94
+ </button>
122
95
  );
123
96
  }
97
+
98
+ function Greeting() {
99
+ // Read-only — just destructure the first element
100
+ const [name] = useTubular(store, "user.name");
101
+ return <p>Hello, {name}!</p>;
102
+ }
124
103
  ```
125
104
 
126
- ## Things you should know
105
+ The setter accepts either a plain value or an updater function:
106
+
107
+ ```ts
108
+ // Plain value
109
+ setCount(10);
110
+
111
+ // Updater function (receives the current value)
112
+ setCount((prev) => (prev ?? 0) + 1);
113
+ ```
127
114
 
128
- - This library _does not_ support observing arbitrary indices in an array, or objects inside of an array.
129
- This is intentional.
130
- If you want to be notified of changes to an array or you want to update some item in an array, you should use the path to the array in your state object, then perform reads or transformations on that.
131
- - This library **mutates your originally-provided `initialState`**.
132
- This is for performance reasons, as mutating objects doesn't require cloning and iterating on all object properties, saving on computation time.
133
- If you want to ensure immutability, you should make your calls to `.update()` return new objects, array, etc.
115
+ `useTubular` automatically unsubscribes when the component unmounts, so you
116
+ don't need to manage cleanup yourself.
134
117
 
135
- ## (Extra) Synthetic Performance Benchmarks
118
+ ## Things you should know
136
119
 
137
- ### Machine specs
120
+ - **Arrays and nested objects are observed at their path, not their contents.**
121
+ `observe("items", fn)` fires when `items` itself is replaced. To add an item
122
+ to an array, use `update("items", prev => [...prev, newItem])`.
123
+ - **Arbitrary array indices are not observable.** You cannot do `observe("items.0", fn)`. If you need per-item reactivity, store each item in its own `Tubular` instance.
124
+ - **State is mutated in place.** `totally-tubular` does not clone your state on every update. This keeps things fast, but means the object you pass to `new Tubular(initialState)` will be modified directly. If that matters to you, pass a deep clone as the initial state.
138
125
 
139
- #### Apple Machine
126
+ ## Performance benchmarks
140
127
 
141
- CPU: M3 Max
142
- RAM: 36GB
143
- OS: MacOS Sequoia 15.5
128
+ ### Apple M3 Max · 36 GB RAM · macOS Sequoia 15.5
144
129
 
145
130
  ```
146
131
  -----shallow state object-----
@@ -189,11 +174,7 @@ OS: MacOS Sequoia 15.5
189
174
  1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 1.1015419579999999s
190
175
  ```
191
176
 
192
- #### Windows Machine
193
-
194
- CPU: Intel i9-13900HX
195
- RAM: 32GB
196
- OS: Windows 11
177
+ ### Intel i9-13900HX · 32 GB RAM · Windows 11
197
178
 
198
179
  ```
199
180
  -----shallow state object-----
@@ -0,0 +1 @@
1
+ { "type": "commonjs" }
@@ -0,0 +1,42 @@
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");
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
+ }
@@ -0,0 +1,30 @@
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];
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _buntest = require("bun:test");
6
+ const _react = require("@testing-library/react");
7
+ const _totallytubular = require("./totally-tubular.cjs");
8
+ const _totallytubularreact = require("./totally-tubular-react.cjs");
9
+ const makeInitialState = ()=>({
10
+ animals: [],
11
+ drink: {
12
+ beer: true,
13
+ kind: 'guiness'
14
+ },
15
+ food: {
16
+ pasta: false,
17
+ pizza: true
18
+ }
19
+ });
20
+ (0, _buntest.describe)('useTubular', ()=>{
21
+ (0, _buntest.it)('should read an initial value from a Tubular instance', ()=>{
22
+ const initial = makeInitialState();
23
+ initial.drink.kind = 'Guiness 0.0%';
24
+ const t = new _totallytubular.Tubular(initial);
25
+ const { result } = (0, _react.renderHook)(()=>(0, _totallytubularreact.useTubular)(t, 'drink.kind'));
26
+ (0, _buntest.expect)(result.current[0]).toBe('Guiness 0.0%');
27
+ });
28
+ (0, _buntest.it)('should update a top-level value and reflect the change', ()=>{
29
+ const t = new _totallytubular.Tubular(makeInitialState());
30
+ const animals = [
31
+ 'cats',
32
+ 'dogs'
33
+ ];
34
+ const { result, rerender } = (0, _react.renderHook)(()=>(0, _totallytubularreact.useTubular)(t, 'animals'));
35
+ (0, _buntest.expect)(result.current[0]).toEqual([]);
36
+ (0, _react.act)(()=>{
37
+ result.current[1](animals);
38
+ });
39
+ rerender();
40
+ (0, _buntest.expect)(result.current[0]).toEqual(animals);
41
+ });
42
+ (0, _buntest.it)('should update a nested value using a callback updater', ()=>{
43
+ const t = new _totallytubular.Tubular(makeInitialState());
44
+ const { result, rerender } = (0, _react.renderHook)(()=>(0, _totallytubularreact.useTubular)(t, 'food.pasta'));
45
+ (0, _buntest.expect)(result.current[0]).toBeFalse();
46
+ (0, _react.act)(()=>{
47
+ result.current[1]((prev)=>!prev);
48
+ });
49
+ rerender();
50
+ (0, _buntest.expect)(result.current[0]).toBeTrue();
51
+ });
52
+ (0, _buntest.it)('should notify all hooks observing the same key when one updates', ()=>{
53
+ const t = new _totallytubular.Tubular(makeInitialState());
54
+ const { result: resultA, rerender: rerenderA } = (0, _react.renderHook)(()=>(0, _totallytubularreact.useTubular)(t, 'drink.beer'));
55
+ const { result: resultB, rerender: rerenderB } = (0, _react.renderHook)(()=>(0, _totallytubularreact.useTubular)(t, 'drink.beer'));
56
+ (0, _buntest.expect)(resultA.current[0]).toBeTrue();
57
+ (0, _buntest.expect)(resultB.current[0]).toBeTrue();
58
+ (0, _react.act)(()=>{
59
+ resultA.current[1](false);
60
+ });
61
+ rerenderA();
62
+ rerenderB();
63
+ (0, _buntest.expect)(resultA.current[0]).toBeFalse();
64
+ (0, _buntest.expect)(resultB.current[0]).toBeFalse();
65
+ });
66
+ (0, _buntest.it)('should unobserve when the hook unmounts', ()=>{
67
+ const t = new _totallytubular.Tubular(makeInitialState());
68
+ const { result, unmount } = (0, _react.renderHook)(()=>(0, _totallytubularreact.useTubular)(t, 'drink.kind'));
69
+ (0, _buntest.expect)(result.current[0]).toBe('guiness');
70
+ unmount();
71
+ // Update after unmount — the store still works, but the unmounted hook
72
+ // should no longer be observing so no "update on unmounted component" error occurs.
73
+ (0, _react.act)(()=>{
74
+ t.update('drink.kind', ()=>'stella');
75
+ });
76
+ // Verify the store update took effect via a fresh hook instance.
77
+ const { result: freshResult } = (0, _react.renderHook)(()=>(0, _totallytubularreact.useTubular)(t, 'drink.kind'));
78
+ (0, _buntest.expect)(freshResult.current[0]).toBe('stella');
79
+ });
80
+ });
@@ -0,0 +1,80 @@
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 +1,4 @@
1
- import type { AllObjectKeys, PropType } from './types.js';
1
+ import type { AllObjectKeys, ObservationCallback, PropType } from './types.js';
2
2
  /**
3
3
  * A totally tubular instance that tracks a top-level
4
4
  * set of keys and values.
@@ -14,17 +14,17 @@ export declare class Tubular<T extends object> {
14
14
  /**
15
15
  * observe changes to some part of state that was updated
16
16
  */
17
- observe<K extends AllObjectKeys<T>, V = PropType<T, K & string>>(propPath: K, observeCallback: (newVal: V, oldVal: V, propPath: K) => void): void;
17
+ observe<K extends string>(propPath: K & AllObjectKeys<T>, observeCallback: ObservationCallback<T>): void;
18
18
  /**
19
19
  * reads a portion of your state
20
20
  */
21
- read<K extends AllObjectKeys<T>>(readPath: K): PropType<T, K & string> | null;
21
+ read<K extends string>(readPath: K & AllObjectKeys<T>): PropType<T, K> | null;
22
22
  /**
23
23
  * disconnects an observer callback from totally tubular
24
24
  */
25
- unobserve<K extends AllObjectKeys<T>, V = PropType<T, K & string>>(propPath: K, observeCallback: (newVal: V, oldVal: V) => void): void;
25
+ unobserve<K extends string>(propPath: K & AllObjectKeys<T>, observeCallback: ObservationCallback<T>): void;
26
26
  /**
27
27
  * updates a portion of your state
28
28
  */
29
- update<K extends AllObjectKeys<T>>(updatePath: K, valUpdater: (oldVal: PropType<T, K & string>) => PropType<T, K & string>): void;
29
+ update<K extends string>(updatePath: K & AllObjectKeys<T>, valUpdater: (oldVal: PropType<T, K>) => PropType<T, K>): void;
30
30
  }
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _buntest = require("bun:test");
6
+ const _totallytubular = require("./totally-tubular.cjs");
7
+ const makeInitialState = ()=>({
8
+ animals: [],
9
+ drink: {
10
+ beer: true,
11
+ kind: 'guiness'
12
+ },
13
+ food: {
14
+ pasta: false,
15
+ pizza: true
16
+ }
17
+ });
18
+ (0, _buntest.describe)('totally-tubular', ()=>{
19
+ (0, _buntest.test)('should update one single property', ()=>{
20
+ const t = new _totallytubular.Tubular(makeInitialState());
21
+ const newVal = 'old speckled hen';
22
+ t.update('drink.kind', ()=>newVal);
23
+ (0, _buntest.expect)(t.read('drink.kind')).toBe(newVal);
24
+ });
25
+ (0, _buntest.test)('should ensure update is called multiple times', ()=>{
26
+ const t = new _totallytubular.Tubular(makeInitialState());
27
+ const initialBeer = t.read('drink.kind');
28
+ const v1 = 'ginger beer';
29
+ const v2 = 'corona';
30
+ const v3 = "London's Pride";
31
+ const ob = _buntest.vi.fn();
32
+ t.observe('drink.kind', ob);
33
+ t.update('drink.kind', ()=>v1);
34
+ t.update('drink.kind', ()=>v2);
35
+ t.update('drink.kind', ()=>v3);
36
+ (0, _buntest.expect)(ob).toHaveBeenNthCalledWith(1, v1, initialBeer, 'drink.kind');
37
+ (0, _buntest.expect)(ob).toHaveBeenNthCalledWith(2, v2, v1, 'drink.kind');
38
+ (0, _buntest.expect)(ob).toHaveBeenNthCalledWith(3, v3, v2, 'drink.kind');
39
+ });
40
+ (0, _buntest.test)('should update an array', ()=>{
41
+ const t = new _totallytubular.Tubular(makeInitialState());
42
+ t.update('animals', (prev)=>[
43
+ ...prev,
44
+ 'birds'
45
+ ]);
46
+ t.update('animals', (prev)=>[
47
+ ...prev,
48
+ 'cats'
49
+ ]);
50
+ t.update('animals', (prev)=>[
51
+ ...prev,
52
+ 'dogs'
53
+ ]);
54
+ (0, _buntest.expect)(t.read('animals')).toStrictEqual([
55
+ 'birds',
56
+ 'cats',
57
+ 'dogs'
58
+ ]);
59
+ });
60
+ (0, _buntest.test)('should update one item in an array', ()=>{
61
+ const t = new _totallytubular.Tubular({
62
+ ...makeInitialState(),
63
+ animals: [
64
+ 'beagle',
65
+ 'corgie',
66
+ 'dachshund'
67
+ ]
68
+ });
69
+ t.update('animals', (prev)=>prev.toSpliced(1, 1, 'yorkie'));
70
+ (0, _buntest.expect)(t.read('animals')).toStrictEqual([
71
+ 'beagle',
72
+ 'yorkie',
73
+ 'dachshund'
74
+ ]);
75
+ });
76
+ (0, _buntest.test)('should observe and then disconnect', ()=>{
77
+ const t = new _totallytubular.Tubular(makeInitialState());
78
+ const foodOb = _buntest.vi.fn();
79
+ const pizzaOb = _buntest.vi.fn();
80
+ t.observe('food.pasta', foodOb);
81
+ t.observe('food.pizza', pizzaOb);
82
+ t.update('food.pasta', (prev)=>!prev);
83
+ t.update('food.pizza', (prev)=>!prev);
84
+ t.unobserve('food.pasta', foodOb);
85
+ t.update('food.pasta', (prev)=>!prev);
86
+ t.update('food.pizza', (prev)=>!prev);
87
+ (0, _buntest.expect)(t.read('food.pasta')).toBeFalsy();
88
+ (0, _buntest.expect)(t.read('food.pizza')).toBeTruthy();
89
+ (0, _buntest.expect)(foodOb).toHaveBeenCalledTimes(1);
90
+ (0, _buntest.expect)(pizzaOb).toHaveBeenCalledTimes(2);
91
+ });
92
+ (0, _buntest.test)('should observe an array and object', ()=>{
93
+ const t = new _totallytubular.Tubular(makeInitialState());
94
+ const animalsOb = _buntest.vi.fn();
95
+ const drinkOb = _buntest.vi.fn();
96
+ t.observe('animals', animalsOb);
97
+ t.observe('drink', drinkOb);
98
+ t.update('animals', (prev)=>[
99
+ ...prev,
100
+ 'dogs'
101
+ ]);
102
+ t.update('animals', (prev)=>[
103
+ ...prev,
104
+ 'more dogs'
105
+ ]);
106
+ t.update('drink', (prev)=>({
107
+ ...prev,
108
+ beer: false,
109
+ kind: 'water'
110
+ }));
111
+ t.update('drink', (prev)=>({
112
+ ...prev,
113
+ beer: true,
114
+ kind: 'is not beer'
115
+ }));
116
+ (0, _buntest.expect)(animalsOb).toHaveBeenNthCalledWith(1, [
117
+ 'dogs'
118
+ ], [], 'animals');
119
+ (0, _buntest.expect)(animalsOb).toHaveBeenNthCalledWith(2, [
120
+ 'dogs',
121
+ 'more dogs'
122
+ ], [
123
+ 'dogs'
124
+ ], 'animals');
125
+ (0, _buntest.expect)(drinkOb).toHaveBeenNthCalledWith(1, {
126
+ beer: false,
127
+ kind: 'water'
128
+ }, {
129
+ beer: true,
130
+ kind: 'guiness'
131
+ }, 'drink');
132
+ (0, _buntest.expect)(drinkOb).toHaveBeenNthCalledWith(2, {
133
+ beer: true,
134
+ kind: 'is not beer'
135
+ }, {
136
+ beer: false,
137
+ kind: 'water'
138
+ }, 'drink');
139
+ });
140
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });