stunk 2.0.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 (96) hide show
  1. package/LICENSE +27 -27
  2. package/README.md +43 -43
  3. package/dist/src/core/asyncChunk.d.ts +28 -0
  4. package/dist/{core → src/core}/asyncChunk.js +8 -4
  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/{core → src/core}/types.d.ts +3 -3
  12. package/dist/{use-react → src/use-react}/hooks/useAsyncChunk.d.ts +3 -3
  13. package/dist/{use-react → src/use-react}/hooks/useAsyncChunk.js +1 -1
  14. package/dist/{use-react → src/use-react}/hooks/useDerive.js +8 -2
  15. package/dist/{utils.d.ts → src/utils.d.ts} +1 -0
  16. package/dist/{utils.js → src/utils.js} +34 -0
  17. package/dist/tests/async-chunk.test.d.ts +1 -0
  18. package/dist/tests/async-chunk.test.js +164 -0
  19. package/dist/tests/batch-chunk.test.d.ts +1 -0
  20. package/dist/tests/batch-chunk.test.js +89 -0
  21. package/dist/tests/chunk.test.d.ts +1 -0
  22. package/dist/tests/chunk.test.js +215 -0
  23. package/dist/tests/computed.test.d.ts +1 -0
  24. package/dist/tests/computed.test.js +192 -0
  25. package/dist/tests/diamond-dep.test.d.ts +1 -0
  26. package/dist/tests/diamond-dep.test.js +74 -0
  27. package/dist/tests/history.test.d.ts +1 -0
  28. package/dist/tests/history.test.js +73 -0
  29. package/dist/tests/middleware.test.d.ts +1 -0
  30. package/dist/tests/middleware.test.js +30 -0
  31. package/dist/tests/persist.test.d.ts +1 -0
  32. package/dist/tests/persist.test.js +43 -0
  33. package/dist/tests/select-chunk.test.d.ts +1 -0
  34. package/dist/tests/select-chunk.test.js +167 -0
  35. package/package.json +97 -79
  36. package/dist/core/asyncChunk.d.ts +0 -22
  37. package/dist/core/computed.js +0 -42
  38. package/dist/core/selector.d.ts +0 -2
  39. package/dist/core/selector.js +0 -23
  40. package/jest.config.js +0 -4
  41. package/src/core/asyncChunk.ts +0 -83
  42. package/src/core/computed.ts +0 -65
  43. package/src/core/core.ts +0 -128
  44. package/src/core/selector.ts +0 -27
  45. package/src/core/types.ts +0 -17
  46. package/src/index.ts +0 -10
  47. package/src/middleware/history.ts +0 -113
  48. package/src/middleware/index.ts +0 -7
  49. package/src/middleware/logger.ts +0 -6
  50. package/src/middleware/persistence.ts +0 -45
  51. package/src/middleware/validator.ts +0 -8
  52. package/src/use-react/hooks/useAsyncChunk.ts +0 -38
  53. package/src/use-react/hooks/useChunk.ts +0 -40
  54. package/src/use-react/hooks/useChunkProperty.ts +0 -21
  55. package/src/use-react/hooks/useChunkValue.ts +0 -15
  56. package/src/use-react/hooks/useChunkValues.ts +0 -35
  57. package/src/use-react/hooks/useComputed.ts +0 -34
  58. package/src/use-react/hooks/useDerive.ts +0 -15
  59. package/src/use-react/index.ts +0 -9
  60. package/src/utils.ts +0 -103
  61. package/tests/async-chunk.test.ts +0 -215
  62. package/tests/batch-chunk.test.ts +0 -108
  63. package/tests/chunk.test.ts +0 -189
  64. package/tests/computed.test.ts +0 -93
  65. package/tests/history.test.ts +0 -99
  66. package/tests/middleware.test.ts +0 -37
  67. package/tests/persist.test.ts +0 -57
  68. package/tests/select-chunk.test.ts +0 -133
  69. package/tests/update.test.ts +0 -70
  70. package/tsconfig.json +0 -23
  71. /package/dist/{core → src/core}/types.js +0 -0
  72. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  73. /package/dist/{index.js → src/index.js} +0 -0
  74. /package/dist/{middleware → src/middleware}/history.d.ts +0 -0
  75. /package/dist/{middleware → src/middleware}/history.js +0 -0
  76. /package/dist/{middleware → src/middleware}/index.d.ts +0 -0
  77. /package/dist/{middleware → src/middleware}/index.js +0 -0
  78. /package/dist/{middleware → src/middleware}/logger.d.ts +0 -0
  79. /package/dist/{middleware → src/middleware}/logger.js +0 -0
  80. /package/dist/{middleware → src/middleware}/persistence.d.ts +0 -0
  81. /package/dist/{middleware → src/middleware}/persistence.js +0 -0
  82. /package/dist/{middleware → src/middleware}/validator.d.ts +0 -0
  83. /package/dist/{middleware → src/middleware}/validator.js +0 -0
  84. /package/dist/{use-react → src/use-react}/hooks/useChunk.d.ts +0 -0
  85. /package/dist/{use-react → src/use-react}/hooks/useChunk.js +0 -0
  86. /package/dist/{use-react → src/use-react}/hooks/useChunkProperty.d.ts +0 -0
  87. /package/dist/{use-react → src/use-react}/hooks/useChunkProperty.js +0 -0
  88. /package/dist/{use-react → src/use-react}/hooks/useChunkValue.d.ts +0 -0
  89. /package/dist/{use-react → src/use-react}/hooks/useChunkValue.js +0 -0
  90. /package/dist/{use-react → src/use-react}/hooks/useChunkValues.d.ts +0 -0
  91. /package/dist/{use-react → src/use-react}/hooks/useChunkValues.js +0 -0
  92. /package/dist/{use-react → src/use-react}/hooks/useComputed.d.ts +0 -0
  93. /package/dist/{use-react → src/use-react}/hooks/useComputed.js +0 -0
  94. /package/dist/{use-react → src/use-react}/hooks/useDerive.d.ts +0 -0
  95. /package/dist/{use-react → src/use-react}/index.d.ts +0 -0
  96. /package/dist/{use-react → src/use-react}/index.js +0 -0
@@ -1,35 +0,0 @@
1
- import { useState, useEffect } from "react";
2
-
3
- import type { Chunk } from "../../core/core";
4
-
5
- /**
6
- * Hook to read values from multiple chunks at once.
7
- * Only re-renders when any of the chunk values change.
8
- */
9
- export function useChunkValues<T extends Chunk<any>[]>(
10
- chunks: [...T]
11
- ): { [K in keyof T]: T[K] extends Chunk<infer U> ? U : never } {
12
- type ReturnType = { [K in keyof T]: T[K] extends Chunk<infer U> ? U : never };
13
-
14
- const [values, setValues] = useState<ReturnType>(() => {
15
- return chunks.map(chunk => chunk.get()) as ReturnType;
16
- });
17
-
18
- useEffect(() => {
19
- const unsubscribes = chunks.map((chunk, index) => {
20
- return chunk.subscribe((newValue) => {
21
- setValues(prev => {
22
- const newValues = [...prev] as ReturnType;
23
- newValues[index] = newValue;
24
- return newValues;
25
- });
26
- });
27
- });
28
-
29
- return () => {
30
- unsubscribes.forEach(unsubscribe => unsubscribe());
31
- };
32
- }, [chunks]);
33
-
34
- return values;
35
- }
@@ -1,34 +0,0 @@
1
- import { useState, useEffect, useMemo } from "react";
2
-
3
- import { computed, DependencyValues } from "../../core/computed";
4
- import type { Chunk } from "../../core/core";
5
-
6
- /**
7
- * A hook that computes a value based on multiple chunks.
8
- * Automatically re-computes when any dependency changes.
9
- */
10
- export function useComputed<TDeps extends Chunk<any>[], TResult>(
11
- dependencies: [...TDeps],
12
- computeFn: (...args: DependencyValues<TDeps>) => TResult
13
- ): TResult {
14
- // Create the computed value - memoize it based on dependencies to prevent recreation
15
- const computedValue = useMemo(
16
- () => computed(dependencies, computeFn),
17
- // eslint-disable-next-line react-hooks/exhaustive-deps
18
- [...dependencies]
19
- );
20
-
21
- const [state, setState] = useState<TResult>(() => computedValue.get());
22
-
23
- useEffect(() => {
24
- const unsubscribe = computedValue.subscribe((newValue) => {
25
- setState(newValue);
26
- });
27
-
28
- return () => {
29
- unsubscribe();
30
- };
31
- }, [computedValue]);
32
-
33
- return state;
34
- }
@@ -1,15 +0,0 @@
1
- import { useMemo } from "react";
2
-
3
- import { useChunk } from "./useChunk";
4
- import type { Chunk } from "../../core/core";
5
-
6
- /**
7
- * A hook for creating a read-only derived value from a chunk.
8
- * Ensures reactivity and updates when the source chunk changes.
9
- */
10
- export function useDerive<T, D>(chunk: Chunk<T>, fn: (value: T) => D): D {
11
- const derivedChunk = useMemo(() => chunk.derive(fn), [chunk, fn]);
12
- const [derivedValue] = useChunk(derivedChunk);
13
-
14
- return derivedValue;
15
- }
@@ -1,9 +0,0 @@
1
- export { useChunk } from './hooks/useChunk'
2
- export { useDerive } from './hooks/useDerive'
3
- export { useComputed } from './hooks/useComputed'
4
-
5
- export { useChunkValue } from './hooks/useChunkValue'
6
- export { useChunkProperty } from './hooks/useChunkProperty'
7
- export { useChunkValues } from './hooks/useChunkValues'
8
-
9
- export { useAsyncChunk } from './hooks/useAsyncChunk'
package/src/utils.ts DELETED
@@ -1,103 +0,0 @@
1
- import { chunk, Chunk, Middleware } from "./core/core";
2
-
3
- import { AsyncChunk } from "./core/asyncChunk";
4
- import { CombinedData, CombinedState, InferAsyncData } from "./core/types";
5
-
6
- export function isValidChunkValue(value: any): boolean {
7
- return value !== null && value !== undefined;
8
- }
9
-
10
- export function isChunk<T>(value: any): value is Chunk<T> {
11
- return value &&
12
- typeof value.get === 'function' &&
13
- typeof value.set === 'function' &&
14
- typeof value.update === 'function' &&
15
- typeof value.subscribe === 'function' &&
16
- typeof value.derive === 'function' &&
17
- typeof value.reset === 'function' &&
18
- typeof value.destroy === 'function';
19
- }
20
-
21
- export function once<T>(fn: () => T): () => T {
22
- let called = false;
23
- let result: T;
24
- return () => {
25
- if (!called) {
26
- result = fn();
27
- called = true;
28
- }
29
- return result;
30
- };
31
- };
32
-
33
- export function combineAsyncChunks<T extends Record<string, AsyncChunk<any>>>(
34
- chunks: T
35
- ): Chunk<{
36
- loading: boolean;
37
- error: Error | null;
38
- data: { [K in keyof T]: InferAsyncData<T[K]> | null };
39
- }> {
40
- // Create initial state with proper typing
41
- const initialData = Object.keys(chunks).reduce((acc, key) => {
42
- acc[key as keyof T] = null;
43
- return acc;
44
- }, {} as CombinedData<T>);
45
-
46
- const initialState: CombinedState<T> = {
47
- loading: true,
48
- error: null,
49
- data: initialData
50
- };
51
-
52
- const combined = chunk(initialState);
53
-
54
- Object.entries(chunks).forEach(([key, asyncChunk]) => {
55
- asyncChunk.subscribe((state) => {
56
- const currentState = combined.get();
57
-
58
- combined.set({
59
- loading: Object.values(chunks).some(chunk => chunk.get().loading),
60
- error: Object.values(chunks)
61
- .map(chunk => chunk.get().error)
62
- .find(error => error !== null) || null,
63
- data: {
64
- ...currentState.data,
65
- [key]: state.data
66
- },
67
- });
68
- });
69
- });
70
-
71
- return combined;
72
- }
73
-
74
- export function processMiddleware<T>(initialValue: T, middleware: Middleware<T>[] = []): T {
75
- if (initialValue === null || initialValue === undefined) {
76
- throw new Error("Value cannot be null or undefined.");
77
- }
78
-
79
- let currentValue = initialValue;
80
- let index = 0;
81
-
82
- while (index < middleware.length) {
83
- const currentMiddleware = middleware[index];
84
- let nextCalled = false;
85
- let nextValue: T | null = null;
86
-
87
- currentMiddleware(currentValue, (val) => {
88
- nextCalled = true;
89
- nextValue = val;
90
- });
91
-
92
- if (!nextCalled) break;
93
-
94
- if (nextValue === null || nextValue === undefined) {
95
- throw new Error("Value cannot be null or undefined.");
96
- }
97
-
98
- currentValue = nextValue;
99
- index++;
100
- }
101
-
102
- return currentValue;
103
- }
@@ -1,215 +0,0 @@
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
- }
@@ -1,108 +0,0 @@
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
- });