stunk 2.2.0 → 2.2.2
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/dist/index.cjs +1 -0
- package/package.json +7 -4
- package/dist/tests/async-chunk.test.d.ts +0 -1
- package/dist/tests/async-chunk.test.js +0 -164
- package/dist/tests/batch-chunk.test.d.ts +0 -1
- package/dist/tests/batch-chunk.test.js +0 -89
- package/dist/tests/chunk.test.d.ts +0 -1
- package/dist/tests/chunk.test.js +0 -215
- package/dist/tests/computed.test.d.ts +0 -1
- package/dist/tests/computed.test.js +0 -192
- package/dist/tests/diamond-dep.test.d.ts +0 -1
- package/dist/tests/diamond-dep.test.js +0 -74
- package/dist/tests/history.test.d.ts +0 -1
- package/dist/tests/history.test.js +0 -73
- package/dist/tests/middleware.test.d.ts +0 -1
- package/dist/tests/middleware.test.js +0 -30
- package/dist/tests/persist.test.d.ts +0 -1
- package/dist/tests/persist.test.js +0 -43
- package/dist/tests/select-chunk.test.d.ts +0 -1
- package/dist/tests/select-chunk.test.js +0 -167
- /package/dist/{src/core → core}/asyncChunk.d.ts +0 -0
- /package/dist/{src/core → core}/asyncChunk.js +0 -0
- /package/dist/{src/core → core}/computed.d.ts +0 -0
- /package/dist/{src/core → core}/computed.js +0 -0
- /package/dist/{src/core → core}/core.d.ts +0 -0
- /package/dist/{src/core → core}/core.js +0 -0
- /package/dist/{src/core → core}/selector.d.ts +0 -0
- /package/dist/{src/core → core}/selector.js +0 -0
- /package/dist/{src/core → core}/types.d.ts +0 -0
- /package/dist/{src/core → core}/types.js +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/index.js → index.js} +0 -0
- /package/dist/{src/middleware → middleware}/history.d.ts +0 -0
- /package/dist/{src/middleware → middleware}/history.js +0 -0
- /package/dist/{src/middleware → middleware}/index.d.ts +0 -0
- /package/dist/{src/middleware → middleware}/index.js +0 -0
- /package/dist/{src/middleware → middleware}/logger.d.ts +0 -0
- /package/dist/{src/middleware → middleware}/logger.js +0 -0
- /package/dist/{src/middleware → middleware}/persistence.d.ts +0 -0
- /package/dist/{src/middleware → middleware}/persistence.js +0 -0
- /package/dist/{src/middleware → middleware}/validator.d.ts +0 -0
- /package/dist/{src/middleware → middleware}/validator.js +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useAsyncChunk.d.ts +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useAsyncChunk.js +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useChunk.d.ts +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useChunk.js +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useChunkProperty.d.ts +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useChunkProperty.js +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useChunkValue.d.ts +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useChunkValue.js +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useChunkValues.d.ts +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useChunkValues.js +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useComputed.d.ts +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useComputed.js +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useDerive.d.ts +0 -0
- /package/dist/{src/use-react → use-react}/hooks/useDerive.js +0 -0
- /package/dist/{src/use-react → use-react}/index.d.ts +0 -0
- /package/dist/{src/use-react → use-react}/index.js +0 -0
- /package/dist/{src/utils.d.ts → utils.d.ts} +0 -0
- /package/dist/{src/utils.js → utils.js} +0 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./index.js");
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stunk",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Stunk is a lightweight, framework-agnostic state management library for JavaScript and TypeScript. It uses chunk-based state units for efficient updates, reactivity, and performance optimization in React, Vue, Svelte, and Vanilla JS/TS applications.",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"build": "tsc",
|
|
7
|
-
"
|
|
6
|
+
"build": "tsc --project tsconfig.json && npm run build:cjs",
|
|
7
|
+
"build:cjs": "echo 'module.exports = require(\"./index.js\");' > dist/index.cjs",
|
|
8
|
+
"test": "tsc --project tsconfig.test.json && vitest",
|
|
8
9
|
"prepublishOnly": "vitest && tsc",
|
|
9
10
|
"prepare": "npm run build",
|
|
10
11
|
"lint": "eslint . --ext .js,.ts,.tsx,.vue"
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"exports": {
|
|
27
28
|
".": {
|
|
28
29
|
"import": "./dist/index.js",
|
|
30
|
+
"require": "./dist/index.cjs",
|
|
29
31
|
"types": "./dist/index.d.ts"
|
|
30
32
|
},
|
|
31
33
|
"./middleware": {
|
|
@@ -39,7 +41,8 @@
|
|
|
39
41
|
"./vue": {
|
|
40
42
|
"import": "./dist/use-vue/index.js",
|
|
41
43
|
"types": "./dist/types/use-vue/index.d.ts"
|
|
42
|
-
}
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
43
46
|
},
|
|
44
47
|
"keywords": [
|
|
45
48
|
"state-management",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,164 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,89 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/tests/chunk.test.js
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,192 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { chunk } from '../src/core/core';
|
|
3
|
-
import { computed } from '../src/core/computed';
|
|
4
|
-
describe('computed > diamond dependency pattern', () => {
|
|
5
|
-
it('should handle diamond dependency pattern correctly', () => {
|
|
6
|
-
// Create the root chunk (A)
|
|
7
|
-
const root = chunk(1);
|
|
8
|
-
// Create two intermediate chunks (B and C) that depend on root
|
|
9
|
-
const left = computed([root], value => value * 2);
|
|
10
|
-
const right = computed([root], value => value + 5);
|
|
11
|
-
// Create a final computed chunk (D) that depends on both B and C
|
|
12
|
-
const final = computed([left, right], (leftValue, rightValue) => `${leftValue}-${rightValue}`);
|
|
13
|
-
// Verify initial computed values before subscribing
|
|
14
|
-
expect(root.get()).toBe(1);
|
|
15
|
-
expect(left.get()).toBe(2); // 1 * 2
|
|
16
|
-
expect(right.get()).toBe(6); // 1 + 5
|
|
17
|
-
expect(final.get()).toBe('2-6');
|
|
18
|
-
// Setup a spy to track updates
|
|
19
|
-
const subscriber = vi.fn();
|
|
20
|
-
const unsubscribe = final.subscribe(subscriber);
|
|
21
|
-
expect(subscriber).toHaveBeenCalledWith('2-6');
|
|
22
|
-
subscriber.mockClear();
|
|
23
|
-
// Update the root
|
|
24
|
-
root.set(4);
|
|
25
|
-
expect(root.get()).toBe(4);
|
|
26
|
-
expect(left.get()).toBe(8); // 4 * 2
|
|
27
|
-
expect(right.get()).toBe(9); // 4 + 5
|
|
28
|
-
expect(final.get()).toBe('8-9');
|
|
29
|
-
expect(subscriber).toHaveBeenCalledWith('8-9');
|
|
30
|
-
// Clean up
|
|
31
|
-
unsubscribe();
|
|
32
|
-
});
|
|
33
|
-
it('should handle complex update chains in diamond patterns', () => {
|
|
34
|
-
const root = chunk(10);
|
|
35
|
-
// First level of dependencies
|
|
36
|
-
const pathA = computed([root], val => val + 5);
|
|
37
|
-
const pathB = computed([root], val => val * 2);
|
|
38
|
-
// Second level - depends on both branches
|
|
39
|
-
const merged = computed([pathA, pathB], (a, b) => ({ sum: a + b, product: a * b }));
|
|
40
|
-
// Verify initial values before subscribing
|
|
41
|
-
expect(pathA.get()).toBe(15); // 10 + 5
|
|
42
|
-
expect(pathB.get()).toBe(20); // 10 * 2
|
|
43
|
-
expect(merged.get()).toEqual({ sum: 35, product: 300 });
|
|
44
|
-
// Set up capture for last values only
|
|
45
|
-
let lastUpdate = null;
|
|
46
|
-
const updates = [];
|
|
47
|
-
const unsubscribe = merged.subscribe(val => {
|
|
48
|
-
lastUpdate = { ...val };
|
|
49
|
-
});
|
|
50
|
-
// First update
|
|
51
|
-
root.set(20);
|
|
52
|
-
if (lastUpdate)
|
|
53
|
-
updates.push(lastUpdate);
|
|
54
|
-
// Verify values after first update
|
|
55
|
-
expect(pathA.get()).toBe(25); // 20 + 5
|
|
56
|
-
expect(pathB.get()).toBe(40); // 20 * 2
|
|
57
|
-
expect(merged.get()).toEqual({ sum: 65, product: 1000 });
|
|
58
|
-
// Second update
|
|
59
|
-
root.set(0);
|
|
60
|
-
if (lastUpdate)
|
|
61
|
-
updates.push(lastUpdate);
|
|
62
|
-
// Verify values after second update
|
|
63
|
-
expect(pathA.get()).toBe(5); // 0 + 5
|
|
64
|
-
expect(pathB.get()).toBe(0); // 0 * 2
|
|
65
|
-
expect(merged.get()).toEqual({ sum: 5, product: 0 });
|
|
66
|
-
// Check that we received both values, ignoring intermediate ones
|
|
67
|
-
expect(updates).toEqual([
|
|
68
|
-
{ sum: 65, product: 1000 },
|
|
69
|
-
{ sum: 5, product: 0 }
|
|
70
|
-
]);
|
|
71
|
-
// Clean up
|
|
72
|
-
unsubscribe();
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { chunk } from "../src/core/core";
|
|
3
|
-
import { withHistory } from "../src/middleware/history";
|
|
4
|
-
describe('Chunk with History', () => {
|
|
5
|
-
it('should maintain history of changes', () => {
|
|
6
|
-
const baseChunk = chunk(0);
|
|
7
|
-
const historyChunk = withHistory(baseChunk);
|
|
8
|
-
historyChunk.set(1);
|
|
9
|
-
historyChunk.set(2);
|
|
10
|
-
historyChunk.set(3);
|
|
11
|
-
expect(historyChunk.getHistory()).toEqual([0, 1, 2, 3]);
|
|
12
|
-
});
|
|
13
|
-
it('should handle undo and redo operations', () => {
|
|
14
|
-
const baseChunk = chunk(0);
|
|
15
|
-
const historyChunk = withHistory(baseChunk);
|
|
16
|
-
const callback = vi.fn();
|
|
17
|
-
historyChunk.subscribe(callback);
|
|
18
|
-
callback.mockClear(); // Clear initial subscription call
|
|
19
|
-
historyChunk.set(1);
|
|
20
|
-
historyChunk.set(2);
|
|
21
|
-
expect(historyChunk.get()).toBe(2);
|
|
22
|
-
historyChunk.undo();
|
|
23
|
-
expect(historyChunk.get()).toBe(1);
|
|
24
|
-
historyChunk.undo();
|
|
25
|
-
expect(historyChunk.get()).toBe(0);
|
|
26
|
-
historyChunk.redo();
|
|
27
|
-
expect(historyChunk.get()).toBe(1);
|
|
28
|
-
historyChunk.redo();
|
|
29
|
-
expect(historyChunk.get()).toBe(2);
|
|
30
|
-
expect(callback).toHaveBeenCalledTimes(6); // 2 sets + 2 undos + 2 redos
|
|
31
|
-
});
|
|
32
|
-
it('should handle branching history', () => {
|
|
33
|
-
const baseChunk = chunk(0);
|
|
34
|
-
const historyChunk = withHistory(baseChunk);
|
|
35
|
-
historyChunk.set(1);
|
|
36
|
-
historyChunk.set(2);
|
|
37
|
-
historyChunk.undo();
|
|
38
|
-
historyChunk.set(3); // This should create a new branch
|
|
39
|
-
expect(historyChunk.getHistory()).toEqual([0, 1, 3]);
|
|
40
|
-
expect(historyChunk.get()).toBe(3);
|
|
41
|
-
});
|
|
42
|
-
it('should respect maxHistory limit', () => {
|
|
43
|
-
const baseChunk = chunk(0);
|
|
44
|
-
const historyChunk = withHistory(baseChunk, { maxHistory: 3 });
|
|
45
|
-
historyChunk.set(1);
|
|
46
|
-
historyChunk.set(2);
|
|
47
|
-
historyChunk.set(3);
|
|
48
|
-
historyChunk.set(4);
|
|
49
|
-
expect(historyChunk.getHistory()).toEqual([2, 3, 4]);
|
|
50
|
-
});
|
|
51
|
-
it('should handle canUndo and canRedo correctly', () => {
|
|
52
|
-
const baseChunk = chunk(0);
|
|
53
|
-
const historyChunk = withHistory(baseChunk);
|
|
54
|
-
expect(historyChunk.canUndo()).toBe(false);
|
|
55
|
-
expect(historyChunk.canRedo()).toBe(false);
|
|
56
|
-
historyChunk.set(1);
|
|
57
|
-
expect(historyChunk.canUndo()).toBe(true);
|
|
58
|
-
expect(historyChunk.canRedo()).toBe(false);
|
|
59
|
-
historyChunk.undo();
|
|
60
|
-
expect(historyChunk.canUndo()).toBe(false);
|
|
61
|
-
expect(historyChunk.canRedo()).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
it('should clear history properly', () => {
|
|
64
|
-
const baseChunk = chunk(0);
|
|
65
|
-
const historyChunk = withHistory(baseChunk);
|
|
66
|
-
historyChunk.set(1);
|
|
67
|
-
historyChunk.set(2);
|
|
68
|
-
historyChunk.clearHistory();
|
|
69
|
-
expect(historyChunk.getHistory()).toEqual([2]);
|
|
70
|
-
expect(historyChunk.canUndo()).toBe(false);
|
|
71
|
-
expect(historyChunk.canRedo()).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, beforeEach, vi, afterEach, test, expect } from "vitest";
|
|
2
|
-
import { chunk } from "../src/core/core";
|
|
3
|
-
import { logger } from "../src/middleware/logger";
|
|
4
|
-
import { nonNegativeValidator } from "../src/middleware/validator";
|
|
5
|
-
describe("Middleware Tests", () => {
|
|
6
|
-
let consoleSpy;
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
consoleSpy = vi.spyOn(console, "log");
|
|
9
|
-
});
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
vi.restoreAllMocks(); // Restores all spies
|
|
12
|
-
vi.clearAllTimers(); // Clears any lingering timers
|
|
13
|
-
});
|
|
14
|
-
test("Logger middleware should log updates", () => {
|
|
15
|
-
const count = chunk(0, [logger]);
|
|
16
|
-
const unsubscribe = count.subscribe(() => { }); // Subscribe to capture updates
|
|
17
|
-
try {
|
|
18
|
-
count.set(5); // Should log: "Setting value: 5"
|
|
19
|
-
expect(consoleSpy).toHaveBeenCalledWith("Setting value:", 5);
|
|
20
|
-
}
|
|
21
|
-
finally {
|
|
22
|
-
unsubscribe(); // Ensure cleanup after test
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
test("Non-negative validator middleware should prevent negative values", () => {
|
|
26
|
-
const count = chunk(0, [nonNegativeValidator]);
|
|
27
|
-
expect(() => count.set(-5)).toThrow("Value must be non-negative!");
|
|
28
|
-
expect(count.get()).toBe(0); // Value should remain unchanged
|
|
29
|
-
});
|
|
30
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, beforeEach, it, expect, vi } from 'vitest';
|
|
2
|
-
import { chunk } from '../src/core/core';
|
|
3
|
-
import { withPersistence } from "../src/middleware/persistence";
|
|
4
|
-
describe('withPersistence', () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
localStorage.clear();
|
|
7
|
-
sessionStorage.clear();
|
|
8
|
-
});
|
|
9
|
-
it('should persist state to localStorage', () => {
|
|
10
|
-
const baseChunk = chunk({ count: 0 });
|
|
11
|
-
const persistedChunk = withPersistence(baseChunk, { key: 'test-key' });
|
|
12
|
-
persistedChunk.set({ count: 1 });
|
|
13
|
-
expect(JSON.parse(localStorage.getItem('test-key'))).toEqual({ count: 1 });
|
|
14
|
-
});
|
|
15
|
-
it('should load persisted state on initialization', () => {
|
|
16
|
-
localStorage.setItem('test-key', JSON.stringify({ count: 5 }));
|
|
17
|
-
const baseChunk = chunk({ count: 0 });
|
|
18
|
-
const persistedChunk = withPersistence(baseChunk, { key: 'test-key' });
|
|
19
|
-
expect(persistedChunk.get()).toEqual({ count: 5 });
|
|
20
|
-
});
|
|
21
|
-
it('should use custom storage', () => {
|
|
22
|
-
const mockStorage = {
|
|
23
|
-
getItem: vi.fn(),
|
|
24
|
-
setItem: vi.fn(),
|
|
25
|
-
};
|
|
26
|
-
const baseChunk = chunk({ count: 0 });
|
|
27
|
-
withPersistence(baseChunk, {
|
|
28
|
-
key: 'test-key',
|
|
29
|
-
storage: mockStorage
|
|
30
|
-
});
|
|
31
|
-
expect(mockStorage.getItem).toHaveBeenCalledWith('test-key');
|
|
32
|
-
});
|
|
33
|
-
it('should use custom serializer/deserializer', () => {
|
|
34
|
-
const baseChunk = chunk({ count: 0 });
|
|
35
|
-
const persistedChunk = withPersistence(baseChunk, {
|
|
36
|
-
key: 'test-key',
|
|
37
|
-
serialize: value => btoa(JSON.stringify(value)),
|
|
38
|
-
deserialize: value => JSON.parse(atob(value))
|
|
39
|
-
});
|
|
40
|
-
persistedChunk.set({ count: 1 });
|
|
41
|
-
expect(localStorage.getItem('test-key')).toBe(btoa(JSON.stringify({ count: 1 })));
|
|
42
|
-
});
|
|
43
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { chunk } from '../src/core/core';
|
|
3
|
-
import { select } from '../src/core/selector';
|
|
4
|
-
describe('select', () => {
|
|
5
|
-
it('should create a selector that initially returns the correct value', () => {
|
|
6
|
-
const source = chunk({ name: 'John', age: 25 });
|
|
7
|
-
const nameSelector = select(source, user => user.name);
|
|
8
|
-
expect(nameSelector.get()).toBe('John');
|
|
9
|
-
});
|
|
10
|
-
it('should update when selected value changes', () => {
|
|
11
|
-
const source = chunk({ name: 'John', age: 25 });
|
|
12
|
-
const nameSelector = select(source, user => user.name);
|
|
13
|
-
source.set({ name: 'Jane', age: 25 });
|
|
14
|
-
expect(nameSelector.get()).toBe('Jane');
|
|
15
|
-
});
|
|
16
|
-
it('should not notify subscribers when non-selected values change', () => {
|
|
17
|
-
const source = chunk({ name: 'John', age: 25 });
|
|
18
|
-
const nameSelector = select(source, user => user.name);
|
|
19
|
-
const subscriber = vi.fn();
|
|
20
|
-
nameSelector.subscribe(subscriber);
|
|
21
|
-
// Reset the mock to ignore initial call
|
|
22
|
-
subscriber.mockReset();
|
|
23
|
-
// Update age only
|
|
24
|
-
source.set({ name: 'John', age: 26 });
|
|
25
|
-
expect(subscriber).not.toHaveBeenCalled();
|
|
26
|
-
});
|
|
27
|
-
it('should notify subscribers when selected value changes', () => {
|
|
28
|
-
const source = chunk({ name: 'John', age: 25 });
|
|
29
|
-
const nameSelector = select(source, user => user.name);
|
|
30
|
-
const subscriber = vi.fn();
|
|
31
|
-
nameSelector.subscribe(subscriber);
|
|
32
|
-
// Reset the mock to ignore initial call
|
|
33
|
-
subscriber.mockReset();
|
|
34
|
-
source.set({ name: 'Jane', age: 25 });
|
|
35
|
-
expect(subscriber).toHaveBeenCalledTimes(1);
|
|
36
|
-
expect(subscriber).toHaveBeenCalledWith('Jane');
|
|
37
|
-
});
|
|
38
|
-
it('should prevent direct modifications to selector', () => {
|
|
39
|
-
const source = chunk({ name: 'John', age: 25 });
|
|
40
|
-
const nameSelector = select(source, user => user.name);
|
|
41
|
-
expect(() => {
|
|
42
|
-
nameSelector.set('Jane');
|
|
43
|
-
}).toThrow('Cannot set values directly on a selector');
|
|
44
|
-
});
|
|
45
|
-
it('should work with complex selectors', () => {
|
|
46
|
-
const source = chunk({ user: { profile: { name: 'John' } } });
|
|
47
|
-
const nameSelector = select(source, state => state.user.profile.name);
|
|
48
|
-
expect(nameSelector.get()).toBe('John');
|
|
49
|
-
source.set({ user: { profile: { name: 'Jane' } } });
|
|
50
|
-
expect(nameSelector.get()).toBe('Jane');
|
|
51
|
-
});
|
|
52
|
-
it('should handle array selectors', () => {
|
|
53
|
-
const source = chunk({ items: [1, 2, 3] });
|
|
54
|
-
const firstItemSelector = select(source, state => state.items[0]);
|
|
55
|
-
expect(firstItemSelector.get()).toBe(1);
|
|
56
|
-
source.set({ items: [4, 2, 3] });
|
|
57
|
-
expect(firstItemSelector.get()).toBe(4);
|
|
58
|
-
});
|
|
59
|
-
it('should work with computed values', () => {
|
|
60
|
-
const source = chunk({ numbers: [1, 2, 3, 4, 5] });
|
|
61
|
-
const sumSelector = select(source, state => state.numbers.reduce((sum, num) => sum + num, 0));
|
|
62
|
-
expect(sumSelector.get()).toBe(15);
|
|
63
|
-
source.set({ numbers: [1, 2, 3] });
|
|
64
|
-
expect(sumSelector.get()).toBe(6);
|
|
65
|
-
});
|
|
66
|
-
it('should properly clean up subscriptions on destroy', () => {
|
|
67
|
-
const source = chunk({ name: 'John', age: 25 });
|
|
68
|
-
const nameSelector = select(source, user => user.name);
|
|
69
|
-
const subscriber = vi.fn();
|
|
70
|
-
const unsubscribe = nameSelector.subscribe(subscriber);
|
|
71
|
-
// Reset mock to ignore initial call
|
|
72
|
-
subscriber.mockReset();
|
|
73
|
-
unsubscribe();
|
|
74
|
-
nameSelector.destroy();
|
|
75
|
-
source.set({ name: 'Jane', age: 25 });
|
|
76
|
-
expect(subscriber).not.toHaveBeenCalled();
|
|
77
|
-
});
|
|
78
|
-
it('should work with multiple independent selectors', () => {
|
|
79
|
-
const source = chunk({ name: 'John', age: 25 });
|
|
80
|
-
const nameSelector = select(source, user => user.name);
|
|
81
|
-
const ageSelector = select(source, user => user.age);
|
|
82
|
-
const nameSubscriber = vi.fn();
|
|
83
|
-
const ageSubscriber = vi.fn();
|
|
84
|
-
nameSelector.subscribe(nameSubscriber);
|
|
85
|
-
ageSelector.subscribe(ageSubscriber);
|
|
86
|
-
// Reset mocks to ignore initial calls
|
|
87
|
-
nameSubscriber.mockReset();
|
|
88
|
-
ageSubscriber.mockReset();
|
|
89
|
-
source.set({ name: 'John', age: 26 });
|
|
90
|
-
expect(nameSubscriber).not.toHaveBeenCalled();
|
|
91
|
-
expect(ageSubscriber).toHaveBeenCalledWith(26);
|
|
92
|
-
source.set({ name: 'Jane', age: 26 });
|
|
93
|
-
expect(nameSubscriber).toHaveBeenCalledWith('Jane');
|
|
94
|
-
expect(ageSubscriber).toHaveBeenCalledTimes(1); // Still from previous update
|
|
95
|
-
});
|
|
96
|
-
it("should not update if selected object has the same values (shallow equal)", () => {
|
|
97
|
-
const source = chunk({ name: "John", details: { age: 25, city: "Lagos" } });
|
|
98
|
-
const detailsSelector = select(source, (user) => user.details, { useShallowEqual: true });
|
|
99
|
-
const callback = vi.fn();
|
|
100
|
-
detailsSelector.subscribe(callback);
|
|
101
|
-
callback.mockReset();
|
|
102
|
-
// Setting a new object with the same values
|
|
103
|
-
source.set({ name: "John", details: { age: 25, city: "Lagos" } });
|
|
104
|
-
expect(callback).not.toHaveBeenCalled();
|
|
105
|
-
});
|
|
106
|
-
// Test without shallow equality
|
|
107
|
-
it("should update if selected object is new but has same values (without shallow equal)", () => {
|
|
108
|
-
const source = chunk({ name: "John", details: { age: 25, city: "Lagos" } });
|
|
109
|
-
// Not using shallow equality here
|
|
110
|
-
const detailsSelector = select(source, (user) => user.details);
|
|
111
|
-
const callback = vi.fn();
|
|
112
|
-
detailsSelector.subscribe(callback);
|
|
113
|
-
// Reset mock to clear initial call
|
|
114
|
-
callback.mockReset();
|
|
115
|
-
// Setting a new object with the same values
|
|
116
|
-
source.set({ name: "John", details: { age: 25, city: "Lagos" } });
|
|
117
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
118
|
-
});
|
|
119
|
-
// Test nested derivation
|
|
120
|
-
it('should support nested derivation', () => {
|
|
121
|
-
const source = chunk({ user: { profile: { name: 'John' } } });
|
|
122
|
-
const profileSelector = select(source, (data) => data.user.profile);
|
|
123
|
-
const nameSelector = profileSelector.derive(profile => profile.name);
|
|
124
|
-
expect(nameSelector.get()).toBe('John');
|
|
125
|
-
source.set({ user: { profile: { name: 'Alice' } } });
|
|
126
|
-
expect(nameSelector.get()).toBe('Alice');
|
|
127
|
-
});
|
|
128
|
-
it('should pass options to nested selectors', () => {
|
|
129
|
-
const source = chunk({
|
|
130
|
-
user: {
|
|
131
|
-
profile: { details: { age: 30, city: 'New York' } }
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
const profileSelector = select(source, (data) => data.user.profile, { useShallowEqual: true });
|
|
135
|
-
const detailsSelector = profileSelector.derive(profile => profile.details);
|
|
136
|
-
const callback = vi.fn();
|
|
137
|
-
detailsSelector.subscribe(callback);
|
|
138
|
-
// Reset mock to clear initial call
|
|
139
|
-
callback.mockReset();
|
|
140
|
-
// Update with new object but same values
|
|
141
|
-
source.set({
|
|
142
|
-
user: {
|
|
143
|
-
profile: { details: { age: 30, city: 'New York' } }
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
expect(callback).not.toHaveBeenCalled(); // Should NOT trigger due to shallow equality
|
|
147
|
-
});
|
|
148
|
-
// Test that set and reset throw errors
|
|
149
|
-
it('should throw error when trying to set or reset a selector', () => {
|
|
150
|
-
const source = chunk({ name: 'John' });
|
|
151
|
-
const nameSelector = select(source, (user) => user.name);
|
|
152
|
-
expect(() => nameSelector.set('Alice')).toThrow();
|
|
153
|
-
expect(() => nameSelector.reset()).toThrow();
|
|
154
|
-
});
|
|
155
|
-
// Test cleanup
|
|
156
|
-
it('should unsubscribe from source when destroyed', () => {
|
|
157
|
-
const source = chunk({ name: 'John' });
|
|
158
|
-
const nameSelector = select(source, (user) => user.name);
|
|
159
|
-
const callback = vi.fn();
|
|
160
|
-
nameSelector.subscribe(callback);
|
|
161
|
-
// Reset mock to clear initial call
|
|
162
|
-
callback.mockReset();
|
|
163
|
-
nameSelector.destroy();
|
|
164
|
-
source.set({ name: 'Alice' });
|
|
165
|
-
expect(callback).not.toHaveBeenCalled();
|
|
166
|
-
});
|
|
167
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|