stunk 2.1.0 → 2.2.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.
Files changed (64) hide show
  1. package/LICENSE +27 -27
  2. package/README.md +43 -43
  3. package/dist/{core → src/core}/asyncChunk.d.ts +6 -0
  4. package/dist/{core → src/core}/asyncChunk.js +6 -1
  5. package/dist/{core → src/core}/computed.d.ts +5 -0
  6. package/dist/src/core/computed.js +58 -0
  7. package/dist/{core → src/core}/core.d.ts +4 -0
  8. package/dist/{core → src/core}/core.js +41 -33
  9. package/dist/src/core/selector.d.ts +17 -0
  10. package/dist/src/core/selector.js +45 -0
  11. package/dist/{use-react → src/use-react}/hooks/useDerive.js +8 -2
  12. package/dist/{utils.d.ts → src/utils.d.ts} +1 -0
  13. package/dist/{utils.js → src/utils.js} +34 -0
  14. package/dist/tests/async-chunk.test.d.ts +1 -0
  15. package/dist/tests/async-chunk.test.js +164 -0
  16. package/dist/tests/batch-chunk.test.d.ts +1 -0
  17. package/dist/tests/batch-chunk.test.js +89 -0
  18. package/dist/tests/chunk.test.d.ts +1 -0
  19. package/dist/tests/chunk.test.js +215 -0
  20. package/dist/tests/computed.test.d.ts +1 -0
  21. package/dist/tests/computed.test.js +192 -0
  22. package/dist/tests/diamond-dep.test.d.ts +1 -0
  23. package/dist/tests/diamond-dep.test.js +74 -0
  24. package/dist/tests/history.test.d.ts +1 -0
  25. package/dist/tests/history.test.js +73 -0
  26. package/dist/tests/middleware.test.d.ts +1 -0
  27. package/dist/tests/middleware.test.js +30 -0
  28. package/dist/tests/persist.test.d.ts +1 -0
  29. package/dist/tests/persist.test.js +43 -0
  30. package/dist/tests/select-chunk.test.d.ts +1 -0
  31. package/dist/tests/select-chunk.test.js +167 -0
  32. package/package.json +97 -91
  33. package/dist/core/computed.js +0 -54
  34. package/dist/core/selector.d.ts +0 -2
  35. package/dist/core/selector.js +0 -9
  36. /package/dist/{core → src/core}/types.d.ts +0 -0
  37. /package/dist/{core → src/core}/types.js +0 -0
  38. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  39. /package/dist/{index.js → src/index.js} +0 -0
  40. /package/dist/{middleware → src/middleware}/history.d.ts +0 -0
  41. /package/dist/{middleware → src/middleware}/history.js +0 -0
  42. /package/dist/{middleware → src/middleware}/index.d.ts +0 -0
  43. /package/dist/{middleware → src/middleware}/index.js +0 -0
  44. /package/dist/{middleware → src/middleware}/logger.d.ts +0 -0
  45. /package/dist/{middleware → src/middleware}/logger.js +0 -0
  46. /package/dist/{middleware → src/middleware}/persistence.d.ts +0 -0
  47. /package/dist/{middleware → src/middleware}/persistence.js +0 -0
  48. /package/dist/{middleware → src/middleware}/validator.d.ts +0 -0
  49. /package/dist/{middleware → src/middleware}/validator.js +0 -0
  50. /package/dist/{use-react → src/use-react}/hooks/useAsyncChunk.d.ts +0 -0
  51. /package/dist/{use-react → src/use-react}/hooks/useAsyncChunk.js +0 -0
  52. /package/dist/{use-react → src/use-react}/hooks/useChunk.d.ts +0 -0
  53. /package/dist/{use-react → src/use-react}/hooks/useChunk.js +0 -0
  54. /package/dist/{use-react → src/use-react}/hooks/useChunkProperty.d.ts +0 -0
  55. /package/dist/{use-react → src/use-react}/hooks/useChunkProperty.js +0 -0
  56. /package/dist/{use-react → src/use-react}/hooks/useChunkValue.d.ts +0 -0
  57. /package/dist/{use-react → src/use-react}/hooks/useChunkValue.js +0 -0
  58. /package/dist/{use-react → src/use-react}/hooks/useChunkValues.d.ts +0 -0
  59. /package/dist/{use-react → src/use-react}/hooks/useChunkValues.js +0 -0
  60. /package/dist/{use-react → src/use-react}/hooks/useComputed.d.ts +0 -0
  61. /package/dist/{use-react → src/use-react}/hooks/useComputed.js +0 -0
  62. /package/dist/{use-react → src/use-react}/hooks/useDerive.d.ts +0 -0
  63. /package/dist/{use-react → src/use-react}/index.d.ts +0 -0
  64. /package/dist/{use-react → src/use-react}/index.js +0 -0
@@ -0,0 +1,164 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { asyncChunk } from '../src/core/asyncChunk';
3
+ import { combineAsyncChunks } from '../src/utils';
4
+ describe('asyncChunk', () => {
5
+ // Helper to create a delayed response
6
+ const createDelayedResponse = (data, delay = 50) => {
7
+ return new Promise((resolve) => setTimeout(() => resolve(data), delay));
8
+ };
9
+ it('should handle successful async operations', async () => {
10
+ const mockUser = { id: 1, name: 'Test User' };
11
+ const userChunk = asyncChunk(async () => {
12
+ return createDelayedResponse(mockUser);
13
+ });
14
+ // Initial state
15
+ expect(userChunk.get()).toEqual({
16
+ loading: true,
17
+ error: null,
18
+ data: null
19
+ });
20
+ // Wait for async operation to complete
21
+ await new Promise(resolve => setTimeout(resolve, 100));
22
+ // Check final state
23
+ expect(userChunk.get()).toEqual({
24
+ loading: false,
25
+ error: null,
26
+ data: mockUser
27
+ });
28
+ });
29
+ it('should handle errors', async () => {
30
+ const errorMessage = 'Failed to fetch';
31
+ const userChunk = asyncChunk(async () => {
32
+ throw new Error(errorMessage);
33
+ });
34
+ // Wait for async operation to complete
35
+ await new Promise(resolve => setTimeout(resolve, 100));
36
+ const state = userChunk.get();
37
+ expect(state.loading).toBe(false);
38
+ expect(state.error?.message).toBe(errorMessage);
39
+ expect(state.data).toBe(null);
40
+ });
41
+ it('should handle retries', async () => {
42
+ let attempts = 0;
43
+ const mockUser = { id: 1, name: 'Test User' };
44
+ const userChunk = asyncChunk(async () => {
45
+ attempts++;
46
+ if (attempts < 3) {
47
+ throw new Error('Temporary error');
48
+ }
49
+ return mockUser;
50
+ }, { retryCount: 2, retryDelay: 50 });
51
+ // Wait for all retries to complete
52
+ await new Promise(resolve => setTimeout(resolve, 200));
53
+ expect(attempts).toBe(3);
54
+ expect(userChunk.get().data).toEqual(mockUser);
55
+ });
56
+ it('should support optimistic updates via mutate', () => {
57
+ const mockUser = { id: 1, name: 'Test User' };
58
+ const userChunk = asyncChunk(async () => mockUser);
59
+ userChunk.mutate(current => ({
60
+ ...current,
61
+ name: 'Updated Name'
62
+ }));
63
+ const state = userChunk.get();
64
+ expect(state.data?.name).toBe('Updated Name');
65
+ });
66
+ it('should reload data when requested', async () => {
67
+ let counter = 0;
68
+ const userChunk = asyncChunk(async () => {
69
+ counter++;
70
+ return { id: counter, name: `User ${counter}` };
71
+ });
72
+ // Wait for initial load
73
+ await new Promise(resolve => setTimeout(resolve, 100));
74
+ expect(userChunk.get().data?.id).toBe(1);
75
+ // Trigger reload
76
+ await userChunk.reload();
77
+ expect(userChunk.get().data?.id).toBe(2);
78
+ });
79
+ });
80
+ describe('combineAsyncChunks', () => {
81
+ it('should combine multiple async chunks', async () => {
82
+ const mockUser = { id: 1, name: 'Test User' };
83
+ const mockPosts = [
84
+ { id: 1, title: 'Post 1' },
85
+ { id: 2, title: 'Post 2' }
86
+ ];
87
+ const userChunk = asyncChunk(async () => {
88
+ return createDelayedResponse(mockUser, 50);
89
+ });
90
+ const postsChunk = asyncChunk(async () => {
91
+ return createDelayedResponse(mockPosts, 100);
92
+ });
93
+ const combined = combineAsyncChunks({
94
+ user: userChunk,
95
+ posts: postsChunk
96
+ });
97
+ // Initial state
98
+ expect(combined.get()).toEqual({
99
+ loading: true,
100
+ error: null,
101
+ data: {
102
+ user: null,
103
+ posts: null
104
+ }
105
+ });
106
+ // Wait for all async operations to complete
107
+ await new Promise(resolve => setTimeout(resolve, 150));
108
+ // Check final state
109
+ expect(combined.get()).toEqual({
110
+ loading: false,
111
+ error: null,
112
+ data: {
113
+ user: mockUser,
114
+ posts: mockPosts
115
+ }
116
+ });
117
+ });
118
+ it('should handle errors in combined chunks', async () => {
119
+ const mockUser = { id: 1, name: 'Test User' };
120
+ const errorMessage = 'Failed to fetch posts';
121
+ const userChunk = asyncChunk(async () => {
122
+ return createDelayedResponse(mockUser, 50);
123
+ });
124
+ const postsChunk = asyncChunk(async () => {
125
+ throw new Error(errorMessage);
126
+ });
127
+ const combined = combineAsyncChunks({
128
+ user: userChunk,
129
+ posts: postsChunk
130
+ });
131
+ // Wait for all operations to complete
132
+ await new Promise(resolve => setTimeout(resolve, 150));
133
+ const state = combined.get();
134
+ expect(state.loading).toBe(false);
135
+ expect(state.error?.message).toBe(errorMessage);
136
+ expect(state.data.user).toEqual(mockUser);
137
+ expect(state.data.posts).toBe(null);
138
+ });
139
+ it('should update loading state correctly', async () => {
140
+ const loadingStates = [];
141
+ const userChunk = asyncChunk(async () => {
142
+ return createDelayedResponse({ id: 1, name: 'Test User' }, 50);
143
+ });
144
+ const postsChunk = asyncChunk(async () => {
145
+ return createDelayedResponse([], 100);
146
+ });
147
+ const combined = combineAsyncChunks({
148
+ user: userChunk,
149
+ posts: postsChunk
150
+ });
151
+ combined.subscribe(state => {
152
+ loadingStates.push(state.loading);
153
+ });
154
+ // Wait for all operations
155
+ await new Promise(resolve => setTimeout(resolve, 150));
156
+ // Should start with loading true and end with false
157
+ expect(loadingStates[0]).toBe(true);
158
+ expect(loadingStates[loadingStates.length - 1]).toBe(false);
159
+ });
160
+ });
161
+ // Helper function
162
+ function createDelayedResponse(data, delay = 50) {
163
+ return new Promise((resolve) => setTimeout(() => resolve(data), delay));
164
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { chunk, batch } from '../src/core/core';
3
+ describe('Chunk batch updates', () => {
4
+ it('should batch multiple updates into a single notification', () => {
5
+ const countChunk = chunk(0);
6
+ const callback = vi.fn();
7
+ countChunk.subscribe(callback);
8
+ callback.mockClear(); // Clear initial subscription call
9
+ batch(() => {
10
+ countChunk.set(1);
11
+ countChunk.set(2);
12
+ countChunk.set(3); // Should only notify once
13
+ });
14
+ expect(callback).toHaveBeenCalledTimes(1);
15
+ expect(callback).toHaveBeenLastCalledWith(3);
16
+ });
17
+ it('should handle nested batch calls', () => {
18
+ const countChunk = chunk(0);
19
+ const callback = vi.fn();
20
+ countChunk.subscribe(callback);
21
+ callback.mockClear();
22
+ batch(() => {
23
+ countChunk.set(1); // Should not notify yet
24
+ batch(() => {
25
+ countChunk.set(2);
26
+ countChunk.set(3);
27
+ });
28
+ countChunk.set(4);
29
+ });
30
+ expect(callback).toHaveBeenCalledTimes(1);
31
+ expect(callback).toHaveBeenLastCalledWith(4);
32
+ });
33
+ it('should handle errors in batch without breaking state', () => {
34
+ const countChunk = chunk(0);
35
+ const callback = vi.fn();
36
+ countChunk.subscribe(callback);
37
+ callback.mockClear();
38
+ expect(() => {
39
+ batch(() => {
40
+ countChunk.set(1); // Should trigger callback
41
+ throw new Error('Test error');
42
+ // countChunk.set(2); // Unreachable
43
+ });
44
+ }).toThrow('Test error');
45
+ expect(callback).toHaveBeenCalledTimes(1);
46
+ expect(callback).toHaveBeenLastCalledWith(1);
47
+ expect(countChunk.get()).toBe(1);
48
+ });
49
+ it('should work with multiple chunks in the same batch', () => {
50
+ const chunk1 = chunk(0);
51
+ const chunk2 = chunk(0);
52
+ const callback1 = vi.fn();
53
+ const callback2 = vi.fn();
54
+ chunk1.subscribe(callback1);
55
+ chunk2.subscribe(callback2);
56
+ callback1.mockClear();
57
+ callback2.mockClear();
58
+ batch(() => {
59
+ chunk1.set(1);
60
+ chunk2.set(1);
61
+ chunk1.set(2);
62
+ chunk2.set(2);
63
+ });
64
+ expect(callback1).toHaveBeenCalledTimes(1);
65
+ expect(callback2).toHaveBeenCalledTimes(1);
66
+ expect(callback1).toHaveBeenLastCalledWith(2);
67
+ expect(callback2).toHaveBeenLastCalledWith(2);
68
+ });
69
+ it('should handle derived chunks in batch updates', () => {
70
+ const sourceChunk = chunk(0);
71
+ const derivedChunk = sourceChunk.derive(x => x * 2);
72
+ const sourceCallback = vi.fn();
73
+ const derivedCallback = vi.fn();
74
+ sourceChunk.subscribe(sourceCallback);
75
+ derivedChunk.subscribe(derivedCallback);
76
+ // Clear mocks after initial calls
77
+ sourceCallback.mockClear();
78
+ derivedCallback.mockClear();
79
+ batch(() => {
80
+ sourceChunk.set(1);
81
+ sourceChunk.set(2);
82
+ sourceChunk.set(3);
83
+ });
84
+ expect(sourceCallback).toHaveBeenCalledTimes(1);
85
+ expect(derivedCallback).toHaveBeenCalledTimes(1);
86
+ expect(sourceCallback).toHaveBeenLastCalledWith(3);
87
+ expect(derivedCallback).toHaveBeenLastCalledWith(6);
88
+ });
89
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,215 @@
1
+ import { test, expect, describe, it, beforeEach, afterAll, vi } from "vitest";
2
+ import { batch, chunk } from "../src/core/core";
3
+ test("Chunk should get and set values correctly", () => {
4
+ const chunky = chunk(0);
5
+ expect(chunky.get()).toBe(0);
6
+ chunky.set(10);
7
+ expect(chunky.get()).toBe(10);
8
+ chunky.set((prev) => prev + 1);
9
+ expect(chunky.get()).toBe(11);
10
+ });
11
+ test("Chunk should notify subscribers on value change", () => {
12
+ const chunky = chunk(0);
13
+ const callback = vi.fn();
14
+ const unsubscribe = chunky.subscribe(callback);
15
+ chunky.set(5);
16
+ expect(callback).toHaveBeenCalledWith(5);
17
+ chunky.set(10);
18
+ expect(callback).toHaveBeenCalledWith(10);
19
+ unsubscribe(); // Ensure cleanup after test
20
+ });
21
+ test("Chunk should notify multiple subscribers correctly", () => {
22
+ const chunky = chunk(0);
23
+ const callback1 = vi.fn();
24
+ const callback2 = vi.fn();
25
+ const unsubscribe1 = chunky.subscribe(callback1);
26
+ const unsubscribe2 = chunky.subscribe(callback2);
27
+ chunky.set(10);
28
+ expect(callback1).toHaveBeenCalledWith(10);
29
+ expect(callback2).toHaveBeenCalledWith(10);
30
+ unsubscribe1();
31
+ unsubscribe2();
32
+ });
33
+ test("Chunk should allow unsubscribing from updates", () => {
34
+ const chunky = chunk(0);
35
+ const callback = vi.fn();
36
+ const unsubscribe = chunky.subscribe(callback);
37
+ // Initial subscription call
38
+ expect(callback).toHaveBeenCalledWith(0);
39
+ expect(callback).toHaveBeenCalledTimes(1);
40
+ chunky.set(5);
41
+ expect(callback).toHaveBeenCalledWith(5);
42
+ expect(callback).toHaveBeenCalledTimes(2);
43
+ unsubscribe();
44
+ chunky.set(10);
45
+ expect(callback).toHaveBeenCalledTimes(2);
46
+ });
47
+ describe("Chunk Derivation", () => {
48
+ it("should create a derived chunk and update it when the original chunk changes", () => {
49
+ const count = chunk(5);
50
+ const doubleCount = count.derive((value) => value * 2);
51
+ const countSpy = vi.fn();
52
+ const doubleCountSpy = vi.fn();
53
+ // Subscribe to both chunks
54
+ count.subscribe(countSpy);
55
+ doubleCount.subscribe(doubleCountSpy);
56
+ expect(count.get()).toBe(5);
57
+ expect(doubleCount.get()).toBe(10);
58
+ expect(countSpy).toHaveBeenCalledWith(5);
59
+ expect(doubleCountSpy).toHaveBeenCalledWith(10);
60
+ // Update count and verify updates
61
+ count.set(10);
62
+ expect(count.get()).toBe(10);
63
+ expect(doubleCount.get()).toBe(20);
64
+ expect(countSpy).toHaveBeenCalledWith(10);
65
+ expect(doubleCountSpy).toHaveBeenCalledWith(20);
66
+ });
67
+ it("should not update the derived chunk if the original chunk value does not change", () => {
68
+ const count = chunk(5);
69
+ const doubleCount = count.derive((value) => value * 2);
70
+ const doubleCountSpy = vi.fn();
71
+ doubleCount.subscribe(doubleCountSpy);
72
+ // Setting the same value
73
+ count.set(5);
74
+ expect(doubleCount.get()).toBe(10);
75
+ expect(doubleCountSpy).toHaveBeenCalledTimes(1);
76
+ });
77
+ });
78
+ test("Chunk should reset to initial value", () => {
79
+ const count = chunk(5);
80
+ count.set(10);
81
+ expect(count.get()).toBe(10);
82
+ count.reset();
83
+ expect(count.get()).toBe(5);
84
+ });
85
+ describe('Chunk destroy', () => {
86
+ const countChunk = chunk(0);
87
+ const anotherChunk = chunk(0);
88
+ const countCallback = vi.fn();
89
+ const anotherCallback = vi.fn();
90
+ beforeEach(() => {
91
+ countCallback.mockClear();
92
+ anotherCallback.mockClear();
93
+ });
94
+ it('should stop notifying subscribers after destroy is called', () => {
95
+ // Subscribe to the chunks
96
+ const countUnsubscribe = countChunk.subscribe(countCallback);
97
+ const anotherUnsubscribe = anotherChunk.subscribe(anotherCallback);
98
+ // Verify initial subscription calls
99
+ expect(countCallback).toHaveBeenCalledTimes(1);
100
+ expect(countCallback).toHaveBeenCalledWith(0);
101
+ expect(anotherCallback).toHaveBeenCalledTimes(1);
102
+ expect(anotherCallback).toHaveBeenCalledWith(0);
103
+ // Clear the mocks to start fresh
104
+ countCallback.mockClear();
105
+ anotherCallback.mockClear();
106
+ // Cleanup subscriptions before destroy
107
+ countUnsubscribe();
108
+ anotherUnsubscribe();
109
+ // Now destroy the chunks - no warning should appear
110
+ countChunk.destroy();
111
+ anotherChunk.destroy();
112
+ // Try setting new values after destruction
113
+ countChunk.set(30);
114
+ anotherChunk.set(40);
115
+ // Ensure that the subscribers were not notified after destroy
116
+ expect(countCallback).toHaveBeenCalledTimes(0);
117
+ expect(anotherCallback).toHaveBeenCalledTimes(0);
118
+ });
119
+ it('should reset to initial value after destroy', () => {
120
+ // Set some values
121
+ countChunk.set(10);
122
+ anotherChunk.set(20);
123
+ // Destroy the chunks (no subscribers at this point, so no warning)
124
+ countChunk.destroy();
125
+ anotherChunk.destroy();
126
+ // Subscribe new callbacks after destroy
127
+ const newCountCallback = vi.fn();
128
+ const newAnotherCallback = vi.fn();
129
+ const newCountUnsubscribe = countChunk.subscribe(newCountCallback);
130
+ const newAnotherUnsubscribe = anotherChunk.subscribe(newAnotherCallback);
131
+ // Should receive initial values
132
+ expect(newCountCallback).toHaveBeenCalledWith(0);
133
+ expect(newAnotherCallback).toHaveBeenCalledWith(0);
134
+ newCountUnsubscribe();
135
+ newAnotherUnsubscribe();
136
+ });
137
+ afterAll(() => {
138
+ countChunk.destroy();
139
+ anotherChunk.destroy();
140
+ });
141
+ });
142
+ describe('chunk update', () => {
143
+ it('should update value using updater function', () => {
144
+ const store = chunk(5);
145
+ store.set(value => value + 1);
146
+ expect(store.get()).toBe(6);
147
+ });
148
+ it('should notify subscribers only if value changes', () => {
149
+ const store = chunk(5);
150
+ const subscriber = vi.fn();
151
+ store.subscribe(subscriber);
152
+ // Reset the mock to ignore initial subscription call
153
+ subscriber.mockReset();
154
+ // Update to same value
155
+ store.set(value => value);
156
+ expect(subscriber).not.toHaveBeenCalled();
157
+ store.set(value => value + 1);
158
+ expect(subscriber).toHaveBeenCalledWith(6);
159
+ expect(subscriber).toHaveBeenCalledTimes(1);
160
+ });
161
+ it('should handle complex update logic', () => {
162
+ const store = chunk(5);
163
+ store.set(value => {
164
+ if (value > 3) {
165
+ return value * 2;
166
+ }
167
+ return value + 1;
168
+ });
169
+ expect(store.get()).toBe(10);
170
+ });
171
+ it('should maintain type safety', () => {
172
+ const store = chunk({ name: 'John', age: 30 });
173
+ store.set(user => ({
174
+ ...user,
175
+ age: user.age + 1
176
+ }));
177
+ const user = store.get();
178
+ expect(user.age).toBe(31);
179
+ expect(user.name).toBe('John');
180
+ });
181
+ });
182
+ describe("Chunk Shallow Check", () => {
183
+ it('should not notify on same primitive', () => {
184
+ const numChunk = chunk(1);
185
+ const callback = vi.fn();
186
+ numChunk.subscribe(callback);
187
+ callback.mockClear();
188
+ batch(() => {
189
+ numChunk.set(1);
190
+ });
191
+ expect(callback).not.toHaveBeenCalled();
192
+ });
193
+ it('should notify on primitive change', () => {
194
+ const numChunk = chunk(1);
195
+ const callback = vi.fn();
196
+ numChunk.subscribe(callback);
197
+ callback.mockClear();
198
+ batch(() => {
199
+ numChunk.set(2);
200
+ });
201
+ expect(callback).toHaveBeenCalledTimes(1);
202
+ expect(callback).toHaveBeenLastCalledWith(2);
203
+ });
204
+ it('should notify on shallow different objects', () => {
205
+ const objChunk = chunk({ a: 1 });
206
+ const callback = vi.fn();
207
+ objChunk.subscribe(callback);
208
+ callback.mockClear();
209
+ batch(() => {
210
+ objChunk.set({ a: 2 });
211
+ });
212
+ expect(callback).toHaveBeenCalledTimes(1);
213
+ expect(callback).toHaveBeenLastCalledWith({ a: 2 });
214
+ });
215
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,192 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { batch, chunk } from '../src/core/core';
3
+ import { computed } from '../src/core/computed';
4
+ function createSubscriber(chunk) {
5
+ const fn = vi.fn();
6
+ const cleanup = chunk.subscribe(() => fn(chunk.get()));
7
+ return { fn, cleanup };
8
+ }
9
+ describe('computed', () => {
10
+ it('should compute the value based on dependencies', () => {
11
+ const num1 = chunk(2);
12
+ const num2 = chunk(3);
13
+ const sum = computed([num1, num2], (a, b) => a + b);
14
+ expect(sum.get()).toBe(5);
15
+ });
16
+ it('should recompute when a dependency changes', () => {
17
+ const num1 = chunk(4);
18
+ const num2 = chunk(5);
19
+ const product = computed([num1, num2], (a, b) => a * b);
20
+ expect(product.get()).toBe(20);
21
+ num1.set(10);
22
+ expect(product.get()).toBe(50);
23
+ });
24
+ it('should cache the computed value until a dependency changes', () => {
25
+ const num1 = chunk(1);
26
+ const num2 = chunk(2);
27
+ const sum = computed([num1, num2], (a, b) => a + b);
28
+ expect(sum.get()).toBe(3);
29
+ num1.set(1);
30
+ expect(sum.get()).toBe(3);
31
+ });
32
+ it('should throw error when attempting to set computed value', () => {
33
+ const num1 = chunk(10);
34
+ const num2 = chunk(20);
35
+ const sum = computed([num1, num2], (a, b) => a + b);
36
+ expect(() => sum.set(100)).toThrow('Cannot set values directly on computed. Modify the source chunk instead.');
37
+ });
38
+ it('should manually recompute the value', () => {
39
+ const num1 = chunk(1);
40
+ const num2 = chunk(2);
41
+ const sum = computed([num1, num2], (a, b) => a + b);
42
+ expect(sum.get()).toBe(3);
43
+ num1.set(4);
44
+ sum.recompute(); // Manually recompute
45
+ expect(sum.get()).toBe(6);
46
+ });
47
+ it('should support multiple dependencies', () => {
48
+ const a = chunk(2);
49
+ const b = chunk(3);
50
+ const c = chunk(4);
51
+ const result = computed([a, b, c], (x, y, z) => x * y + z);
52
+ expect(result.get()).toBe(10);
53
+ b.set(5);
54
+ expect(result.get()).toBe(14);
55
+ });
56
+ it('should handle nested computed values correctly', () => {
57
+ const a = chunk(2);
58
+ const b = chunk(3);
59
+ const sum = computed([a, b], (x, y) => x + y);
60
+ const doubled = computed([sum], (s) => s * 2);
61
+ expect(sum.get()).toBe(5);
62
+ expect(doubled.get()).toBe(10);
63
+ a.set(5);
64
+ expect(sum.get()).toBe(8);
65
+ expect(doubled.get()).toBe(16);
66
+ b.set(7);
67
+ expect(sum.get()).toBe(12);
68
+ expect(doubled.get()).toBe(24);
69
+ });
70
+ it('should notify subscribers when dependencies change', () => {
71
+ const a = chunk(5);
72
+ const b = chunk(10);
73
+ const sum = computed([a, b], (aVal, bVal) => aVal + bVal);
74
+ const { fn: subscriber, cleanup } = createSubscriber(sum);
75
+ expect(subscriber).toHaveBeenCalledWith(15);
76
+ subscriber.mockReset();
77
+ a.set(7);
78
+ expect(subscriber).toHaveBeenCalledWith(17);
79
+ cleanup();
80
+ });
81
+ it("should mark computed as dirty when dependencies change", () => {
82
+ const a = chunk(5);
83
+ const b = chunk(10);
84
+ const sum = computed([a, b], (aVal, bVal) => aVal + bVal);
85
+ expect(sum.isDirty()).toBe(true);
86
+ a.set(7);
87
+ expect(sum.isDirty()).toBe(false);
88
+ expect(sum.get()).toBe(17);
89
+ expect(sum.isDirty()).toBe(false);
90
+ });
91
+ it('should handle notifications properly even when computed value does not change', () => {
92
+ const a = chunk(5);
93
+ const b = chunk(10);
94
+ const alwaysFifteen = computed([a, b], () => 15);
95
+ const { fn: subscriber, cleanup } = createSubscriber(alwaysFifteen);
96
+ subscriber.mockReset();
97
+ a.set(7);
98
+ expect(alwaysFifteen.get()).toBe(15);
99
+ cleanup();
100
+ });
101
+ it("should not recompute unnecessarily", () => {
102
+ const a = chunk(4);
103
+ const b = chunk(6);
104
+ const computeFn = vi.fn((x, y) => x + y);
105
+ const sum = computed([a, b], computeFn);
106
+ expect(sum.get()).toBe(10);
107
+ expect(computeFn).toHaveBeenCalledTimes(1);
108
+ batch(() => {
109
+ a.set(4); // No real change
110
+ b.set(6); // No real change
111
+ });
112
+ expect(computeFn).toHaveBeenCalledTimes(1);
113
+ batch(() => {
114
+ a.set(5);
115
+ });
116
+ expect(computeFn).toHaveBeenCalledTimes(2);
117
+ });
118
+ it('should only compute once on initialization', () => {
119
+ const a = chunk(1);
120
+ const b = chunk(2);
121
+ const computeFn = vi.fn((x, y) => x + y);
122
+ const sum = computed([a, b], computeFn);
123
+ expect(sum.get()).toBe(3);
124
+ expect(computeFn).toHaveBeenCalledTimes(1);
125
+ sum.get();
126
+ expect(computeFn).toHaveBeenCalledTimes(1);
127
+ });
128
+ it('should not recompute when dependencies change but values stay the same', () => {
129
+ const a = chunk(4);
130
+ const b = chunk(6);
131
+ const computeFn = vi.fn((x, y) => x + y);
132
+ const sum = computed([a, b], computeFn);
133
+ expect(sum.get()).toBe(10);
134
+ expect(computeFn).toHaveBeenCalledTimes(1);
135
+ a.set(4); // Setting to same value
136
+ expect(computeFn).toHaveBeenCalledTimes(1); // Should not recompute
137
+ b.set(6); // Setting to same value
138
+ expect(computeFn).toHaveBeenCalledTimes(1); // Should not recompute
139
+ });
140
+ it('should recompute when dependencies actually change values', () => {
141
+ const a = chunk(4);
142
+ const b = chunk(6);
143
+ const computeFn = vi.fn((x, y) => x + y);
144
+ const sum = computed([a, b], computeFn);
145
+ expect(sum.get()).toBe(10);
146
+ expect(computeFn).toHaveBeenCalledTimes(1);
147
+ a.set(5); // Real change
148
+ expect(computeFn).toHaveBeenCalledTimes(2);
149
+ expect(sum.get()).toBe(11);
150
+ });
151
+ it('should work with batched operations', () => {
152
+ const a = chunk(4);
153
+ const b = chunk(6);
154
+ const computeFn = vi.fn((x, y) => x + y);
155
+ const sum = computed([a, b], computeFn);
156
+ expect(sum.get()).toBe(10);
157
+ expect(computeFn).toHaveBeenCalledTimes(1);
158
+ computeFn.mockClear();
159
+ batch(() => {
160
+ a.set(5);
161
+ b.set(7);
162
+ });
163
+ // Only one computation should happen, not two
164
+ expect(computeFn).toHaveBeenCalledTimes(1);
165
+ expect(sum.get()).toBe(12);
166
+ });
167
+ it('should notify subscribers when dependencies change values', () => {
168
+ const a = chunk(5);
169
+ const b = chunk(10);
170
+ const sum = computed([a, b], (aVal, bVal) => aVal + bVal);
171
+ const { fn: subscriber, cleanup } = createSubscriber(sum);
172
+ // Initial notification
173
+ expect(subscriber).toHaveBeenCalledWith(15);
174
+ subscriber.mockReset();
175
+ a.set(7);
176
+ expect(subscriber).toHaveBeenCalledWith(17);
177
+ subscriber.mockReset();
178
+ a.set(7);
179
+ expect(subscriber).not.toHaveBeenCalled();
180
+ cleanup();
181
+ });
182
+ it('should correctly handle the isDirty state', () => {
183
+ const a = chunk(5);
184
+ const b = chunk(10);
185
+ const sum = computed([a, b], (aVal, bVal) => aVal + bVal);
186
+ expect(sum.isDirty()).toBe(true);
187
+ a.set(7);
188
+ expect(sum.isDirty()).toBe(false);
189
+ sum.recompute();
190
+ expect(sum.isDirty()).toBe(false);
191
+ });
192
+ });
@@ -0,0 +1 @@
1
+ export {};