stunk 0.8.0 → 1.0.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,215 @@
1
+ import { asyncChunk } from '../src/core/asyncChunk';
2
+ import { combineAsyncChunks } from '../src/utils';
3
+
4
+ // Mock types for testing
5
+ interface User {
6
+ id: number;
7
+ name: string;
8
+ }
9
+
10
+ interface Post {
11
+ id: number;
12
+ title: string;
13
+ }
14
+
15
+ describe('asyncChunk', () => {
16
+ // Helper to create a delayed response
17
+ const createDelayedResponse = <T>(data: T, delay = 50): Promise<T> => {
18
+ return new Promise((resolve) => setTimeout(() => resolve(data), delay));
19
+ };
20
+
21
+ it('should handle successful async operations', async () => {
22
+ const mockUser: User = { id: 1, name: 'Test User' };
23
+ const userChunk = asyncChunk<User>(async () => {
24
+ return createDelayedResponse(mockUser);
25
+ });
26
+
27
+ // Initial state
28
+ expect(userChunk.get()).toEqual({
29
+ loading: true,
30
+ error: null,
31
+ data: null
32
+ });
33
+
34
+ // Wait for async operation to complete
35
+ await new Promise(resolve => setTimeout(resolve, 100));
36
+
37
+ // Check final state
38
+ expect(userChunk.get()).toEqual({
39
+ loading: false,
40
+ error: null,
41
+ data: mockUser
42
+ });
43
+ });
44
+
45
+ it('should handle errors', async () => {
46
+ const errorMessage = 'Failed to fetch';
47
+ const userChunk = asyncChunk<User>(async () => {
48
+ throw new Error(errorMessage);
49
+ });
50
+
51
+ // Wait for async operation to complete
52
+ await new Promise(resolve => setTimeout(resolve, 100));
53
+
54
+ const state = userChunk.get();
55
+ expect(state.loading).toBe(false);
56
+ expect(state.error?.message).toBe(errorMessage);
57
+ expect(state.data).toBe(null);
58
+ });
59
+
60
+ it('should handle retries', async () => {
61
+ let attempts = 0;
62
+ const mockUser: User = { id: 1, name: 'Test User' };
63
+
64
+ const userChunk = asyncChunk<User>(
65
+ async () => {
66
+ attempts++;
67
+ if (attempts < 3) {
68
+ throw new Error('Temporary error');
69
+ }
70
+ return mockUser;
71
+ },
72
+ { retryCount: 2, retryDelay: 50 }
73
+ );
74
+
75
+ // Wait for all retries to complete
76
+ await new Promise(resolve => setTimeout(resolve, 200));
77
+
78
+ expect(attempts).toBe(3);
79
+ expect(userChunk.get().data).toEqual(mockUser);
80
+ });
81
+
82
+ it('should support optimistic updates via mutate', () => {
83
+ const mockUser: User = { id: 1, name: 'Test User' };
84
+ const userChunk = asyncChunk<User>(async () => mockUser);
85
+
86
+ userChunk.mutate(current => ({
87
+ ...current!,
88
+ name: 'Updated Name'
89
+ }));
90
+
91
+ const state = userChunk.get();
92
+ expect(state.data?.name).toBe('Updated Name');
93
+ });
94
+
95
+ it('should reload data when requested', async () => {
96
+ let counter = 0;
97
+ const userChunk = asyncChunk<User>(async () => {
98
+ counter++;
99
+ return { id: counter, name: `User ${counter}` };
100
+ });
101
+
102
+ // Wait for initial load
103
+ await new Promise(resolve => setTimeout(resolve, 100));
104
+ expect(userChunk.get().data?.id).toBe(1);
105
+
106
+ // Trigger reload
107
+ await userChunk.reload();
108
+ expect(userChunk.get().data?.id).toBe(2);
109
+ });
110
+ });
111
+
112
+ describe('combineAsyncChunks', () => {
113
+ it('should combine multiple async chunks', async () => {
114
+ const mockUser: User = { id: 1, name: 'Test User' };
115
+ const mockPosts: Post[] = [
116
+ { id: 1, title: 'Post 1' },
117
+ { id: 2, title: 'Post 2' }
118
+ ];
119
+
120
+ const userChunk = asyncChunk<User>(async () => {
121
+ return createDelayedResponse(mockUser, 50);
122
+ });
123
+
124
+ const postsChunk = asyncChunk<Post[]>(async () => {
125
+ return createDelayedResponse(mockPosts, 100);
126
+ });
127
+
128
+ const combined = combineAsyncChunks({
129
+ user: userChunk,
130
+ posts: postsChunk
131
+ });
132
+
133
+ // Initial state
134
+ expect(combined.get()).toEqual({
135
+ loading: true,
136
+ error: null,
137
+ data: {
138
+ user: null,
139
+ posts: null
140
+ }
141
+ });
142
+
143
+ // Wait for all async operations to complete
144
+ await new Promise(resolve => setTimeout(resolve, 150));
145
+
146
+ // Check final state
147
+ expect(combined.get()).toEqual({
148
+ loading: false,
149
+ error: null,
150
+ data: {
151
+ user: mockUser,
152
+ posts: mockPosts
153
+ }
154
+ });
155
+ });
156
+
157
+ it('should handle errors in combined chunks', async () => {
158
+ const mockUser: User = { id: 1, name: 'Test User' };
159
+ const errorMessage = 'Failed to fetch posts';
160
+
161
+ const userChunk = asyncChunk<User>(async () => {
162
+ return createDelayedResponse(mockUser, 50);
163
+ });
164
+
165
+ const postsChunk = asyncChunk<Post[]>(async () => {
166
+ throw new Error(errorMessage);
167
+ });
168
+
169
+ const combined = combineAsyncChunks({
170
+ user: userChunk,
171
+ posts: postsChunk
172
+ });
173
+
174
+ // Wait for all operations to complete
175
+ await new Promise(resolve => setTimeout(resolve, 150));
176
+
177
+ const state = combined.get();
178
+ expect(state.loading).toBe(false);
179
+ expect(state.error?.message).toBe(errorMessage);
180
+ expect(state.data.user).toEqual(mockUser);
181
+ expect(state.data.posts).toBe(null);
182
+ });
183
+
184
+ it('should update loading state correctly', async () => {
185
+ const loadingStates: boolean[] = [];
186
+ const userChunk = asyncChunk<User>(async () => {
187
+ return createDelayedResponse({ id: 1, name: 'Test User' }, 50);
188
+ });
189
+
190
+ const postsChunk = asyncChunk<Post[]>(async () => {
191
+ return createDelayedResponse([], 100);
192
+ });
193
+
194
+ const combined = combineAsyncChunks({
195
+ user: userChunk,
196
+ posts: postsChunk
197
+ });
198
+
199
+ combined.subscribe(state => {
200
+ loadingStates.push(state.loading);
201
+ });
202
+
203
+ // Wait for all operations
204
+ await new Promise(resolve => setTimeout(resolve, 150));
205
+
206
+ // Should start with loading true and end with false
207
+ expect(loadingStates[0]).toBe(true);
208
+ expect(loadingStates[loadingStates.length - 1]).toBe(false);
209
+ });
210
+ });
211
+
212
+ // Helper function
213
+ function createDelayedResponse<T>(data: T, delay = 50): Promise<T> {
214
+ return new Promise((resolve) => setTimeout(() => resolve(data), delay));
215
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { chunk } from '../src/core/core';
6
+ import { withPersistence } from "../src/middleware/persistence";
7
+
8
+ describe('withPersistence', () => {
9
+ beforeEach(() => {
10
+ localStorage.clear();
11
+ sessionStorage.clear();
12
+ });
13
+
14
+ it('should persist state to localStorage', () => {
15
+ const baseChunk = chunk({ count: 0 });
16
+ const persistedChunk = withPersistence(baseChunk, { key: 'test-key' });
17
+
18
+ persistedChunk.set({ count: 1 });
19
+ expect(JSON.parse(localStorage.getItem('test-key')!)).toEqual({ count: 1 });
20
+ });
21
+
22
+ it('should load persisted state on initialization', () => {
23
+ localStorage.setItem('test-key', JSON.stringify({ count: 5 }));
24
+
25
+ const baseChunk = chunk({ count: 0 });
26
+ const persistedChunk = withPersistence(baseChunk, { key: 'test-key' });
27
+
28
+ expect(persistedChunk.get()).toEqual({ count: 5 });
29
+ });
30
+
31
+ it('should use custom storage', () => {
32
+ const mockStorage = {
33
+ getItem: jest.fn(),
34
+ setItem: jest.fn(),
35
+ };
36
+
37
+ const baseChunk = chunk({ count: 0 });
38
+ withPersistence(baseChunk, {
39
+ key: 'test-key',
40
+ storage: mockStorage as unknown as Storage
41
+ });
42
+
43
+ expect(mockStorage.getItem).toHaveBeenCalledWith('test-key');
44
+ });
45
+
46
+ it('should use custom serializer/deserializer', () => {
47
+ const baseChunk = chunk({ count: 0 });
48
+ const persistedChunk = withPersistence(baseChunk, {
49
+ key: 'test-key',
50
+ serialize: value => btoa(JSON.stringify(value)),
51
+ deserialize: value => JSON.parse(atob(value))
52
+ });
53
+
54
+ persistedChunk.set({ count: 1 });
55
+ expect(localStorage.getItem('test-key')).toBe(btoa(JSON.stringify({ count: 1 })));
56
+ });
57
+ });
@@ -0,0 +1,70 @@
1
+ import { chunk } from '../src/core/core';
2
+
3
+ describe('chunk update', () => {
4
+ it('should update value using updater function', () => {
5
+ const store = chunk(5);
6
+ store.update(value => value + 1);
7
+ expect(store.get()).toBe(6);
8
+ });
9
+
10
+ it('should throw error if updater is not a function', () => {
11
+ const store = chunk(5);
12
+ // @ts-expect-error Testing invalid input
13
+ expect(() => store.update('not a function')).toThrow('Updater must be a function');
14
+ });
15
+
16
+ it('should throw error if updater returns null or undefined', () => {
17
+ const store = chunk(5);
18
+ // @ts-expect-error Testing invalid input
19
+ expect(() => store.update(() => null)).toThrow('Value cannot be null or undefined.');
20
+ // @ts-expect-error Testing invalid input
21
+ expect(() => store.update(() => undefined)).toThrow('Value cannot be null or undefined.');
22
+ });
23
+
24
+ it('should notify subscribers only if value changes', () => {
25
+ const store = chunk(5);
26
+ const subscriber = jest.fn();
27
+ store.subscribe(subscriber);
28
+
29
+ // Reset the mock to ignore initial subscription call
30
+ subscriber.mockReset();
31
+
32
+ // Update to same value
33
+ store.update(value => value);
34
+ expect(subscriber).not.toHaveBeenCalled();
35
+
36
+ // Update to new value
37
+ store.update(value => value + 1);
38
+ expect(subscriber).toHaveBeenCalledWith(6);
39
+ expect(subscriber).toHaveBeenCalledTimes(1);
40
+ });
41
+
42
+ it('should handle complex update logic', () => {
43
+ const store = chunk(5);
44
+ store.update(value => {
45
+ if (value > 3) {
46
+ return value * 2;
47
+ }
48
+ return value + 1;
49
+ });
50
+ expect(store.get()).toBe(10);
51
+ });
52
+
53
+ it('should maintain type safety', () => {
54
+ interface User {
55
+ name: string;
56
+ age: number;
57
+ }
58
+
59
+ const store = chunk<User>({ name: 'John', age: 30 });
60
+
61
+ store.update(user => ({
62
+ ...user,
63
+ age: user.age + 1
64
+ }));
65
+
66
+ const user = store.get();
67
+ expect(user.age).toBe(31);
68
+ expect(user.name).toBe('John');
69
+ });
70
+ });