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.
- package/LICENSE +27 -27
- package/README.md +43 -43
- package/dist/src/core/asyncChunk.d.ts +28 -0
- package/dist/{core → src/core}/asyncChunk.js +8 -4
- package/dist/{core → src/core}/computed.d.ts +5 -0
- package/dist/src/core/computed.js +58 -0
- package/dist/{core → src/core}/core.d.ts +4 -0
- package/dist/{core → src/core}/core.js +41 -33
- package/dist/src/core/selector.d.ts +17 -0
- package/dist/src/core/selector.js +45 -0
- package/dist/{core → src/core}/types.d.ts +3 -3
- package/dist/{use-react → src/use-react}/hooks/useAsyncChunk.d.ts +3 -3
- package/dist/{use-react → src/use-react}/hooks/useAsyncChunk.js +1 -1
- package/dist/{use-react → src/use-react}/hooks/useDerive.js +8 -2
- package/dist/{utils.d.ts → src/utils.d.ts} +1 -0
- package/dist/{utils.js → src/utils.js} +34 -0
- package/dist/tests/async-chunk.test.d.ts +1 -0
- package/dist/tests/async-chunk.test.js +164 -0
- package/dist/tests/batch-chunk.test.d.ts +1 -0
- package/dist/tests/batch-chunk.test.js +89 -0
- package/dist/tests/chunk.test.d.ts +1 -0
- package/dist/tests/chunk.test.js +215 -0
- package/dist/tests/computed.test.d.ts +1 -0
- package/dist/tests/computed.test.js +192 -0
- package/dist/tests/diamond-dep.test.d.ts +1 -0
- package/dist/tests/diamond-dep.test.js +74 -0
- package/dist/tests/history.test.d.ts +1 -0
- package/dist/tests/history.test.js +73 -0
- package/dist/tests/middleware.test.d.ts +1 -0
- package/dist/tests/middleware.test.js +30 -0
- package/dist/tests/persist.test.d.ts +1 -0
- package/dist/tests/persist.test.js +43 -0
- package/dist/tests/select-chunk.test.d.ts +1 -0
- package/dist/tests/select-chunk.test.js +167 -0
- package/package.json +97 -79
- package/dist/core/asyncChunk.d.ts +0 -22
- package/dist/core/computed.js +0 -42
- package/dist/core/selector.d.ts +0 -2
- package/dist/core/selector.js +0 -23
- package/jest.config.js +0 -4
- package/src/core/asyncChunk.ts +0 -83
- package/src/core/computed.ts +0 -65
- package/src/core/core.ts +0 -128
- package/src/core/selector.ts +0 -27
- package/src/core/types.ts +0 -17
- package/src/index.ts +0 -10
- package/src/middleware/history.ts +0 -113
- package/src/middleware/index.ts +0 -7
- package/src/middleware/logger.ts +0 -6
- package/src/middleware/persistence.ts +0 -45
- package/src/middleware/validator.ts +0 -8
- package/src/use-react/hooks/useAsyncChunk.ts +0 -38
- package/src/use-react/hooks/useChunk.ts +0 -40
- package/src/use-react/hooks/useChunkProperty.ts +0 -21
- package/src/use-react/hooks/useChunkValue.ts +0 -15
- package/src/use-react/hooks/useChunkValues.ts +0 -35
- package/src/use-react/hooks/useComputed.ts +0 -34
- package/src/use-react/hooks/useDerive.ts +0 -15
- package/src/use-react/index.ts +0 -9
- package/src/utils.ts +0 -103
- package/tests/async-chunk.test.ts +0 -215
- package/tests/batch-chunk.test.ts +0 -108
- package/tests/chunk.test.ts +0 -189
- package/tests/computed.test.ts +0 -93
- package/tests/history.test.ts +0 -99
- package/tests/middleware.test.ts +0 -37
- package/tests/persist.test.ts +0 -57
- package/tests/select-chunk.test.ts +0 -133
- package/tests/update.test.ts +0 -70
- package/tsconfig.json +0 -23
- /package/dist/{core → src/core}/types.js +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{index.js → src/index.js} +0 -0
- /package/dist/{middleware → src/middleware}/history.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/history.js +0 -0
- /package/dist/{middleware → src/middleware}/index.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/index.js +0 -0
- /package/dist/{middleware → src/middleware}/logger.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/logger.js +0 -0
- /package/dist/{middleware → src/middleware}/persistence.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/persistence.js +0 -0
- /package/dist/{middleware → src/middleware}/validator.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/validator.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunk.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunk.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkProperty.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkProperty.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkValue.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkValue.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkValues.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkValues.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useComputed.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useComputed.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useDerive.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/index.d.ts +0 -0
- /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
|
-
}
|
package/src/use-react/index.ts
DELETED
|
@@ -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
|
-
});
|