stunk 0.3.0 → 0.7.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,86 @@
1
+ import { Chunk } from "../core/core";
2
+
3
+ export interface ChunkWithHistory<T> extends Chunk<T> {
4
+ undo: () => void;
5
+ redo: () => void;
6
+ canUndo: () => boolean;
7
+ canRedo: () => boolean;
8
+ getHistory: () => T[];
9
+ clearHistory: () => void;
10
+ }
11
+
12
+ export function withHistory<T>(
13
+ baseChunk: Chunk<T>,
14
+ options: { maxHistory?: number } = {}
15
+ ): ChunkWithHistory<T> {
16
+ const { maxHistory = 100 } = options;
17
+ const history: T[] = [baseChunk.get()];
18
+ let currentIndex = 0;
19
+ let isHistoryAction = false;
20
+
21
+ const historyChunk: ChunkWithHistory<T> = {
22
+ ...baseChunk,
23
+
24
+ set: (newValue: T) => {
25
+ if (isHistoryAction) {
26
+ baseChunk.set(newValue);
27
+ return;
28
+ }
29
+
30
+ // Remove any future history when setting a new value
31
+ history.splice(currentIndex + 1);
32
+ history.push(newValue);
33
+
34
+ // Limit history size
35
+ if (history.length > maxHistory) {
36
+ console.warn("History limit reached. Removing oldest entries.");
37
+ const removeCount = history.length - maxHistory;
38
+ history.splice(0, removeCount);
39
+ currentIndex = Math.max(0, currentIndex - removeCount);
40
+ }
41
+
42
+ currentIndex = history.length - 1;
43
+ baseChunk.set(newValue);
44
+ },
45
+
46
+ undo: () => {
47
+ if (!historyChunk.canUndo()) return;
48
+
49
+ isHistoryAction = true;
50
+ currentIndex--;
51
+ historyChunk.set(history[currentIndex]);
52
+ isHistoryAction = false;
53
+ },
54
+
55
+ redo: () => {
56
+ if (!historyChunk.canRedo()) return;
57
+
58
+ isHistoryAction = true;
59
+ currentIndex++;
60
+ historyChunk.set(history[currentIndex]);
61
+ isHistoryAction = false;
62
+ },
63
+
64
+ canUndo: () => currentIndex > 0,
65
+
66
+ canRedo: () => currentIndex < history.length - 1,
67
+
68
+ getHistory: () => [...history],
69
+
70
+ clearHistory: () => {
71
+ const currentValue = baseChunk.get();
72
+ history.length = 0;
73
+ history.push(currentValue);
74
+ currentIndex = 0;
75
+ },
76
+
77
+ // Override destroy to clean up history
78
+ destroy: () => {
79
+ history.length = 0;
80
+ baseChunk.destroy();
81
+ }
82
+ }
83
+
84
+ return historyChunk;
85
+
86
+ }
@@ -0,0 +1,3 @@
1
+ export { logger } from "./logger";
2
+ export { nonNegativeValidator } from "./validator";
3
+ export { withHistory } from "./history";
@@ -0,0 +1,6 @@
1
+ import { Middleware } from "../core/core";
2
+
3
+ export const logger: Middleware<any> = (value, next) => {
4
+ console.log("Setting value:", value);
5
+ next(value);
6
+ };
@@ -0,0 +1,8 @@
1
+ import { Middleware } from "../core/core";
2
+
3
+ export const nonNegativeValidator: Middleware<number> = (value, next) => {
4
+ if (value < 0) {
5
+ throw new Error("Value must be non-negative!");
6
+ }
7
+ next(value); // If validation passes, proceed with the update
8
+ };
@@ -0,0 +1,108 @@
1
+ import { chunk, batch } from '../src/core/core';
2
+
3
+
4
+ describe('Chunk batch updates', () => {
5
+ it('should batch multiple updates into a single notification', () => {
6
+ const countChunk = chunk(0);
7
+ const callback = jest.fn();
8
+
9
+ countChunk.subscribe(callback);
10
+ callback.mockClear(); // Clear initial subscription call
11
+
12
+ batch(() => {
13
+ countChunk.set(1);
14
+ countChunk.set(2);
15
+ countChunk.set(3); // Should only notify once
16
+ });
17
+
18
+ expect(callback).toHaveBeenCalledTimes(1);
19
+ expect(callback).toHaveBeenLastCalledWith(3);
20
+ });
21
+
22
+ it('should handle nested batch calls', () => {
23
+ const countChunk = chunk(0);
24
+ const callback = jest.fn();
25
+
26
+ countChunk.subscribe(callback);
27
+ callback.mockClear();
28
+
29
+ batch(() => {
30
+ countChunk.set(1); // Should not notify yet
31
+ batch(() => {
32
+ countChunk.set(2);
33
+ countChunk.set(3);
34
+ });
35
+ countChunk.set(4);
36
+ });
37
+
38
+ expect(callback).toHaveBeenCalledTimes(1);
39
+ expect(callback).toHaveBeenLastCalledWith(4);
40
+ });
41
+
42
+ it('should handle errors in batch without breaking state', () => {
43
+ const countChunk = chunk(0);
44
+ const callback = jest.fn();
45
+
46
+ countChunk.subscribe(callback);
47
+ callback.mockClear();
48
+
49
+ expect(() => {
50
+ batch(() => {
51
+ countChunk.set(1);
52
+ throw new Error('Test error');
53
+ // countChunk.set(2);
54
+ });
55
+ }).toThrow('Test error');
56
+
57
+ expect(callback).toHaveBeenCalledTimes(1);
58
+ expect(callback).toHaveBeenLastCalledWith(1);
59
+ expect(countChunk.get()).toBe(1);
60
+ });
61
+
62
+ it('should work with multiple chunks in the same batch', () => {
63
+ const chunk1 = chunk(0);
64
+ const chunk2 = chunk(0);
65
+ const callback1 = jest.fn();
66
+ const callback2 = jest.fn();
67
+
68
+ chunk1.subscribe(callback1);
69
+ chunk2.subscribe(callback2);
70
+ callback1.mockClear();
71
+ callback2.mockClear();
72
+
73
+ batch(() => {
74
+ chunk1.set(1);
75
+ chunk2.set(1);
76
+ chunk1.set(2);
77
+ chunk2.set(2);
78
+ });
79
+
80
+ expect(callback1).toHaveBeenCalledTimes(1);
81
+ expect(callback2).toHaveBeenCalledTimes(1);
82
+ expect(callback1).toHaveBeenLastCalledWith(2);
83
+ expect(callback2).toHaveBeenLastCalledWith(2);
84
+ });
85
+
86
+ it('should handle derived chunks in batch updates', () => {
87
+ const sourceChunk = chunk(0);
88
+ const derivedChunk = sourceChunk.derive(x => x * 2);
89
+ const sourceCallback = jest.fn();
90
+ const derivedCallback = jest.fn();
91
+
92
+ sourceChunk.subscribe(sourceCallback);
93
+ derivedChunk.subscribe(derivedCallback);
94
+ sourceCallback.mockClear();
95
+ derivedCallback.mockClear();
96
+
97
+ batch(() => {
98
+ sourceChunk.set(1);
99
+ sourceChunk.set(2);
100
+ sourceChunk.set(3);
101
+ });
102
+
103
+ expect(sourceCallback).toHaveBeenCalledTimes(1);
104
+ expect(derivedCallback).toHaveBeenCalledTimes(1);
105
+ expect(sourceCallback).toHaveBeenLastCalledWith(3);
106
+ expect(derivedCallback).toHaveBeenLastCalledWith(6);
107
+ });
108
+ });
@@ -1,35 +1,187 @@
1
- // tests/chunk.test.ts
2
-
3
- import { createChunk } from "../src/core";
1
+ import { chunk } from "../src/core/core";
4
2
 
5
3
  test("Chunk should get and set values correctly", () => {
6
- const chunk = createChunk<number>(0);
7
- expect(chunk.get()).toBe(0);
8
- chunk.set(10);
9
- expect(chunk.get()).toBe(10);
4
+ const chunky = chunk<number>(0);
5
+ expect(chunky.get()).toBe(0);
6
+ chunky.set(10);
7
+ expect(chunky.get()).toBe(10);
10
8
  });
11
9
 
12
10
  test("Chunk should notify subscribers on value change", () => {
13
- const chunk = createChunk<number>(0);
11
+ const chunky = chunk<number>(0);
14
12
  const callback = jest.fn();
15
- chunk.subscribe(callback);
13
+ const unsubscribe = chunky.subscribe(callback); // Store unsubscribe function
16
14
 
17
- chunk.set(5);
15
+ chunky.set(5);
18
16
  expect(callback).toHaveBeenCalledWith(5);
19
17
 
20
- chunk.set(10);
18
+ chunky.set(10);
21
19
  expect(callback).toHaveBeenCalledWith(10);
20
+
21
+ unsubscribe(); // Ensure cleanup after test
22
22
  });
23
23
 
24
+ test("Chunk should notify multiple subscribers correctly", () => {
25
+ const chunky = chunk<number>(0);
26
+ const callback1 = jest.fn();
27
+ const callback2 = jest.fn();
28
+
29
+ const unsubscribe1 = chunky.subscribe(callback1);
30
+ const unsubscribe2 = chunky.subscribe(callback2);
31
+
32
+
33
+ chunky.set(10);
34
+
35
+ expect(callback1).toHaveBeenCalledWith(10);
36
+ expect(callback2).toHaveBeenCalledWith(10);
37
+
38
+ unsubscribe1();
39
+ unsubscribe2();
40
+ });
41
+
42
+
24
43
  test("Chunk should allow unsubscribing from updates", () => {
25
- const chunk = createChunk<number>(0);
44
+ const chunky = chunk<number>(0);
26
45
  const callback = jest.fn();
27
- const unsubscribe = chunk.subscribe(callback);
46
+ const unsubscribe = chunky.subscribe(callback);
47
+
48
+ // Initial subscription call
49
+ expect(callback).toHaveBeenCalledWith(0);
50
+ expect(callback).toHaveBeenCalledTimes(1);
28
51
 
29
- chunk.set(5);
52
+ chunky.set(5);
30
53
  expect(callback).toHaveBeenCalledWith(5);
54
+ expect(callback).toHaveBeenCalledTimes(2);
31
55
 
32
56
  unsubscribe();
33
- chunk.set(10); // No callback should be called now
34
- expect(callback).toHaveBeenCalledTimes(1);
57
+ chunky.set(10);
58
+ expect(callback).toHaveBeenCalledTimes(2); // Still called only twice
59
+ });
60
+
61
+ describe("Chunk Derivation", () => {
62
+ it("should create a derived chunk and update it when the original chunk changes", () => {
63
+ const count = chunk(5);
64
+ const doubleCount = count.derive((value) => value * 2);
65
+
66
+ const countSpy = jest.fn();
67
+ const doubleCountSpy = jest.fn();
68
+
69
+ // Subscribe to both chunks
70
+ count.subscribe(countSpy);
71
+ doubleCount.subscribe(doubleCountSpy);
72
+
73
+ // Initial values
74
+ expect(count.get()).toBe(5);
75
+ expect(doubleCount.get()).toBe(10);
76
+ expect(countSpy).toHaveBeenCalledWith(5);
77
+ expect(doubleCountSpy).toHaveBeenCalledWith(10);
78
+
79
+ // Update count and verify updates
80
+ count.set(10);
81
+ expect(count.get()).toBe(10);
82
+ expect(doubleCount.get()).toBe(20);
83
+ expect(countSpy).toHaveBeenCalledWith(10);
84
+ expect(doubleCountSpy).toHaveBeenCalledWith(20);
85
+ });
86
+
87
+ it("should not update the derived chunk if the original chunk value does not change", () => {
88
+ const count = chunk(5);
89
+ const doubleCount = count.derive((value) => value * 2);
90
+
91
+ const doubleCountSpy = jest.fn();
92
+
93
+ // Subscribe to the derived chunk
94
+ doubleCount.subscribe(doubleCountSpy);
95
+
96
+ // Setting the same value
97
+ count.set(5);
98
+ expect(doubleCount.get()).toBe(10); // Derived value should remain the same
99
+ expect(doubleCountSpy).toHaveBeenCalledTimes(1); // Only initial value
100
+ });
101
+ });
102
+
103
+
104
+ test("Chunk should reset to initial value", () => {
105
+ const count = chunk(5);
106
+ count.set(10);
107
+ expect(count.get()).toBe(10);
108
+ count.reset();
109
+ expect(count.get()).toBe(5);
110
+ });
111
+
112
+
113
+ describe('Chunk destroy', () => {
114
+ const countChunk = chunk(0);
115
+ const anotherChunk = chunk(0);
116
+ const countCallback = jest.fn();
117
+ const anotherCallback = jest.fn();
118
+
119
+ beforeEach(() => {
120
+ // Reset the mocks
121
+ countCallback.mockClear();
122
+ anotherCallback.mockClear();
123
+ });
124
+
125
+ it('should stop notifying subscribers after destroy is called', () => {
126
+ // Subscribe to the chunks
127
+ const countUnsubscribe = countChunk.subscribe(countCallback);
128
+ const anotherUnsubscribe = anotherChunk.subscribe(anotherCallback);
129
+
130
+ // Verify initial subscription calls
131
+ expect(countCallback).toHaveBeenCalledTimes(1);
132
+ expect(countCallback).toHaveBeenCalledWith(0);
133
+ expect(anotherCallback).toHaveBeenCalledTimes(1);
134
+ expect(anotherCallback).toHaveBeenCalledWith(0);
135
+
136
+ // Clear the mocks to start fresh
137
+ countCallback.mockClear();
138
+ anotherCallback.mockClear();
139
+
140
+ // Cleanup subscriptions before destroy
141
+ countUnsubscribe();
142
+ anotherUnsubscribe();
143
+
144
+ // Now destroy the chunks - no warning should appear
145
+ countChunk.destroy();
146
+ anotherChunk.destroy();
147
+
148
+ // Try setting new values after destruction
149
+ countChunk.set(30);
150
+ anotherChunk.set(40);
151
+
152
+ // Ensure that the subscribers were not notified after destroy
153
+ expect(countCallback).toHaveBeenCalledTimes(0);
154
+ expect(anotherCallback).toHaveBeenCalledTimes(0);
155
+ });
156
+
157
+ it('should reset to initial value after destroy', () => {
158
+ // Set some values
159
+ countChunk.set(10);
160
+ anotherChunk.set(20);
161
+
162
+ // Destroy the chunks (no subscribers at this point, so no warning)
163
+ countChunk.destroy();
164
+ anotherChunk.destroy();
165
+
166
+ // Subscribe new callbacks after destroy
167
+ const newCountCallback = jest.fn();
168
+ const newAnotherCallback = jest.fn();
169
+
170
+ const newCountUnsubscribe = countChunk.subscribe(newCountCallback);
171
+ const newAnotherUnsubscribe = anotherChunk.subscribe(newAnotherCallback);
172
+
173
+ // Should receive initial values
174
+ expect(newCountCallback).toHaveBeenCalledWith(0);
175
+ expect(newAnotherCallback).toHaveBeenCalledWith(0);
176
+
177
+ // Cleanup
178
+ newCountUnsubscribe();
179
+ newAnotherUnsubscribe();
180
+ });
181
+
182
+ // Clean up after all tests
183
+ afterAll(() => {
184
+ countChunk.destroy();
185
+ anotherChunk.destroy();
186
+ });
35
187
  });
@@ -0,0 +1,99 @@
1
+ import { batch, chunk } from "../src/core/core";
2
+ import { withHistory } from "../src/middleware/history";
3
+
4
+
5
+ describe('Chunk with History', () => {
6
+ it('should maintain history of changes', () => {
7
+ const baseChunk = chunk(0);
8
+ const historyChunk = withHistory(baseChunk);
9
+
10
+ historyChunk.set(1);
11
+ historyChunk.set(2);
12
+ historyChunk.set(3);
13
+
14
+ expect(historyChunk.getHistory()).toEqual([0, 1, 2, 3]);
15
+ });
16
+
17
+ it('should handle undo and redo operations', () => {
18
+ const baseChunk = chunk(0);
19
+ const historyChunk = withHistory(baseChunk);
20
+ const callback = jest.fn();
21
+
22
+ historyChunk.subscribe(callback);
23
+ callback.mockClear(); // Clear initial subscription call
24
+
25
+ historyChunk.set(1);
26
+ historyChunk.set(2);
27
+
28
+ expect(historyChunk.get()).toBe(2);
29
+
30
+ historyChunk.undo();
31
+ expect(historyChunk.get()).toBe(1);
32
+
33
+ historyChunk.undo();
34
+ expect(historyChunk.get()).toBe(0);
35
+
36
+ historyChunk.redo();
37
+ expect(historyChunk.get()).toBe(1);
38
+
39
+ historyChunk.redo();
40
+ expect(historyChunk.get()).toBe(2);
41
+
42
+ expect(callback).toHaveBeenCalledTimes(6); // 2 sets + 2 undos + 2 redos
43
+ });
44
+
45
+ it('should handle branching history', () => {
46
+ const baseChunk = chunk(0);
47
+ const historyChunk = withHistory(baseChunk);
48
+
49
+ historyChunk.set(1);
50
+ historyChunk.set(2);
51
+ historyChunk.undo();
52
+ historyChunk.set(3); // This should create a new branch
53
+
54
+ expect(historyChunk.getHistory()).toEqual([0, 1, 3]);
55
+ expect(historyChunk.get()).toBe(3);
56
+ });
57
+
58
+ it('should respect maxHistory limit', () => {
59
+ const baseChunk = chunk(0);
60
+ const historyChunk = withHistory(baseChunk, { maxHistory: 3 });
61
+
62
+ historyChunk.set(1);
63
+ historyChunk.set(2);
64
+ historyChunk.set(3);
65
+ historyChunk.set(4);
66
+
67
+ expect(historyChunk.getHistory()).toEqual([2, 3, 4]);
68
+ });
69
+
70
+ it('should handle canUndo and canRedo correctly', () => {
71
+ const baseChunk = chunk(0);
72
+ const historyChunk = withHistory(baseChunk);
73
+
74
+ expect(historyChunk.canUndo()).toBe(false);
75
+ expect(historyChunk.canRedo()).toBe(false);
76
+
77
+ historyChunk.set(1);
78
+ expect(historyChunk.canUndo()).toBe(true);
79
+ expect(historyChunk.canRedo()).toBe(false);
80
+
81
+ historyChunk.undo();
82
+ expect(historyChunk.canUndo()).toBe(false);
83
+ expect(historyChunk.canRedo()).toBe(true);
84
+ });
85
+
86
+ it('should clear history properly', () => {
87
+ const baseChunk = chunk(0);
88
+ const historyChunk = withHistory(baseChunk);
89
+
90
+ historyChunk.set(1);
91
+ historyChunk.set(2);
92
+
93
+ historyChunk.clearHistory();
94
+
95
+ expect(historyChunk.getHistory()).toEqual([2]);
96
+ expect(historyChunk.canUndo()).toBe(false);
97
+ expect(historyChunk.canRedo()).toBe(false);
98
+ });
99
+ });
@@ -0,0 +1,37 @@
1
+ import { chunk } from "../src/core/core";
2
+ import { logger } from "../src/middleware/logger";
3
+ import { nonNegativeValidator } from "../src/middleware/validator";
4
+
5
+ describe("Middleware Tests", () => {
6
+ let consoleSpy: jest.SpyInstance;
7
+
8
+ beforeEach(() => {
9
+ consoleSpy = jest.spyOn(console, "log").mockImplementation();
10
+ });
11
+
12
+ afterEach(() => {
13
+ jest.restoreAllMocks(); // Restores all spies
14
+ jest.clearAllTimers(); // Clears any lingering timers
15
+ });
16
+
17
+ test("Logger middleware should log updates", () => {
18
+ const count = chunk(0, [logger]);
19
+
20
+ const unsubscribe = count.subscribe(() => { }); // Subscribe to capture updates
21
+
22
+ try {
23
+ count.set(5); // Should log: "Setting value: 5"
24
+ expect(consoleSpy).toHaveBeenCalledWith("Setting value:", 5);
25
+ } finally {
26
+ unsubscribe(); // Ensure cleanup after test
27
+ consoleSpy.mockRestore();
28
+ }
29
+ });
30
+
31
+ test("Non-negative validator middleware should prevent negative values", () => {
32
+ const count = chunk(0, [nonNegativeValidator]);
33
+
34
+ expect(() => count.set(-5)).toThrow("Value must be non-negative!");
35
+ expect(count.get()).toBe(0); // Value should remain unchanged
36
+ });
37
+ });
@@ -0,0 +1,132 @@
1
+ import { chunk, select } from '../src/core/core';
2
+
3
+ describe('select', () => {
4
+ it('should create a selector that initially returns the correct value', () => {
5
+ const source = chunk({ name: 'John', age: 25 });
6
+ const nameSelector = select(source, user => user.name);
7
+
8
+ expect(nameSelector.get()).toBe('John');
9
+ });
10
+
11
+ it('should update when selected value changes', () => {
12
+ const source = chunk({ name: 'John', age: 25 });
13
+ const nameSelector = select(source, user => user.name);
14
+
15
+ source.set({ name: 'Jane', age: 25 });
16
+ expect(nameSelector.get()).toBe('Jane');
17
+ });
18
+
19
+ it('should not notify subscribers when non-selected values change', () => {
20
+ const source = chunk({ name: 'John', age: 25 });
21
+ const nameSelector = select(source, user => user.name);
22
+
23
+ const subscriber = jest.fn();
24
+ nameSelector.subscribe(subscriber);
25
+
26
+ // Reset the mock to ignore initial call
27
+ subscriber.mockReset();
28
+
29
+ // Update age only
30
+ source.set({ name: 'John', age: 26 });
31
+
32
+ expect(subscriber).not.toHaveBeenCalled();
33
+ });
34
+
35
+ it('should notify subscribers when selected value changes', () => {
36
+ const source = chunk({ name: 'John', age: 25 });
37
+ const nameSelector = select(source, user => user.name);
38
+
39
+ const subscriber = jest.fn();
40
+ nameSelector.subscribe(subscriber);
41
+
42
+ // Reset the mock to ignore initial call
43
+ subscriber.mockReset();
44
+
45
+ source.set({ name: 'Jane', age: 25 });
46
+
47
+ expect(subscriber).toHaveBeenCalledTimes(1);
48
+ expect(subscriber).toHaveBeenCalledWith('Jane');
49
+ });
50
+
51
+ it('should prevent direct modifications to selector', () => {
52
+ const source = chunk({ name: 'John', age: 25 });
53
+ const nameSelector = select(source, user => user.name);
54
+
55
+ expect(() => {
56
+ nameSelector.set('Jane');
57
+ }).toThrow('Cannot set values directly on a selector');
58
+ });
59
+
60
+ it('should work with complex selectors', () => {
61
+ const source = chunk({ user: { profile: { name: 'John' } } });
62
+ const nameSelector = select(source, state => state.user.profile.name);
63
+
64
+ expect(nameSelector.get()).toBe('John');
65
+
66
+ source.set({ user: { profile: { name: 'Jane' } } });
67
+ expect(nameSelector.get()).toBe('Jane');
68
+ });
69
+
70
+ it('should handle array selectors', () => {
71
+ const source = chunk({ items: [1, 2, 3] });
72
+ const firstItemSelector = select(source, state => state.items[0]);
73
+
74
+ expect(firstItemSelector.get()).toBe(1);
75
+
76
+ source.set({ items: [4, 2, 3] });
77
+ expect(firstItemSelector.get()).toBe(4);
78
+ });
79
+
80
+ it('should work with computed values', () => {
81
+ const source = chunk({ numbers: [1, 2, 3, 4, 5] });
82
+ const sumSelector = select(source, state =>
83
+ state.numbers.reduce((sum, num) => sum + num, 0)
84
+ );
85
+
86
+ expect(sumSelector.get()).toBe(15);
87
+
88
+ source.set({ numbers: [1, 2, 3] });
89
+ expect(sumSelector.get()).toBe(6);
90
+ });
91
+
92
+ it('should properly clean up subscriptions on destroy', () => {
93
+ const source = chunk({ name: 'John', age: 25 });
94
+ const nameSelector = select(source, user => user.name);
95
+
96
+ const subscriber = jest.fn();
97
+ const unsubscribe = nameSelector.subscribe(subscriber);
98
+
99
+ // Reset mock to ignore initial call
100
+ subscriber.mockReset();
101
+
102
+ unsubscribe();
103
+ nameSelector.destroy();
104
+ source.set({ name: 'Jane', age: 25 });
105
+
106
+ expect(subscriber).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it('should work with multiple independent selectors', () => {
110
+ const source = chunk({ name: 'John', age: 25 });
111
+ const nameSelector = select(source, user => user.name);
112
+ const ageSelector = select(source, user => user.age);
113
+
114
+ const nameSubscriber = jest.fn();
115
+ const ageSubscriber = jest.fn();
116
+
117
+ nameSelector.subscribe(nameSubscriber);
118
+ ageSelector.subscribe(ageSubscriber);
119
+
120
+ // Reset mocks to ignore initial calls
121
+ nameSubscriber.mockReset();
122
+ ageSubscriber.mockReset();
123
+
124
+ source.set({ name: 'John', age: 26 });
125
+ expect(nameSubscriber).not.toHaveBeenCalled();
126
+ expect(ageSubscriber).toHaveBeenCalledWith(26);
127
+
128
+ source.set({ name: 'Jane', age: 26 });
129
+ expect(nameSubscriber).toHaveBeenCalledWith('Jane');
130
+ expect(ageSubscriber).toHaveBeenCalledTimes(1); // Still from previous update
131
+ });
132
+ });
package/types/stunk.d.ts CHANGED
@@ -5,7 +5,8 @@ declare module 'stunk' {
5
5
  get: () => T;
6
6
  set: (value: T) => void;
7
7
  subscribe: (callback: Subscriber<T>) => () => void;
8
+ derive?: <D>(fn: (value: T) => D) => Chunk<D>;
8
9
  }
9
10
 
10
- export function createChunk<T>(initialValue: T): Chunk<T>;
11
+ export function chunk<T>(initialValue: T): Chunk<T>;
11
12
  }