stunk 0.6.0 → 0.7.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/README.md +65 -21
- package/dist/{core.js → core/core.js} +22 -0
- package/dist/index.js +2 -3
- package/dist/middleware/history.js +58 -0
- package/dist/middleware/index.js +3 -0
- package/package.json +1 -1
- package/src/{core.ts → core/core.ts} +26 -0
- package/src/index.ts +3 -4
- package/src/middleware/history.ts +86 -0
- package/src/middleware/index.ts +3 -0
- package/src/middleware/logger.ts +1 -1
- package/src/middleware/validator.ts +1 -1
- package/tests/batch-chunk.test.ts +1 -1
- package/tests/chunk.test.ts +1 -1
- package/tests/history.test.ts +99 -0
- package/tests/middleware.test.ts +1 -1
- package/tests/select-chunk.test.ts +132 -0
package/README.md
CHANGED
|
@@ -70,6 +70,25 @@ count.subscribe(callback);
|
|
|
70
70
|
count.set(10); // Will log: "Updated value: 10"
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
### 1.1.0. **Unsubscribing**
|
|
74
|
+
|
|
75
|
+
You can **unsubscribe** from a **chunk**, which means you stop getting notifications when the **value changes**. You can do this by calling the function that's returned when you **subscribe..** Well, would you wanna do that? 😂
|
|
76
|
+
|
|
77
|
+
### Usage
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const count = chunk(0);
|
|
81
|
+
const callback = (newValue: number) => console.log("Updated value:", newValue);
|
|
82
|
+
|
|
83
|
+
const unsubscribe = count.subscribe(callback);
|
|
84
|
+
|
|
85
|
+
count.set(10); // Will log: "Updated value: 10"
|
|
86
|
+
|
|
87
|
+
unsubscribe(); // Unsubscribe
|
|
88
|
+
|
|
89
|
+
count.set(20); // Nothing will happen now, because you unsubscribed
|
|
90
|
+
```
|
|
91
|
+
|
|
73
92
|
### 1.2. **Deriving New Chunks**
|
|
74
93
|
|
|
75
94
|
With Stunk, you can create **derived chunks**. This means you can create a new **chunk** based on the value of another **chunk**. When the original **chunk** changes, the **derived chunk** will automatically update.
|
|
@@ -91,26 +110,7 @@ count.set(10);
|
|
|
91
110
|
// "Double count: 20"
|
|
92
111
|
```
|
|
93
112
|
|
|
94
|
-
### 1.3. **
|
|
95
|
-
|
|
96
|
-
You can **unsubscribe** from a **chunk**, which means you stop getting notifications when the **value changes**. You can do this by calling the function that's returned when you **subscribe..** Well, would you wanna do that? 😂
|
|
97
|
-
|
|
98
|
-
### Usage
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
const count = chunk(0);
|
|
102
|
-
const callback = (newValue: number) => console.log("Updated value:", newValue);
|
|
103
|
-
|
|
104
|
-
const unsubscribe = count.subscribe(callback);
|
|
105
|
-
|
|
106
|
-
count.set(10); // Will log: "Updated value: 10"
|
|
107
|
-
|
|
108
|
-
unsubscribe(); // Unsubscribe
|
|
109
|
-
|
|
110
|
-
count.set(20); // Nothing will happen now, because you unsubscribed
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### 1.4. **Batch Updates**
|
|
113
|
+
### 1.3. **Batch Updates**
|
|
114
114
|
|
|
115
115
|
Batch updates allow you to group multiple **state changes** together and notify **subscribers** only once at the end of the **batch**. This is particularly useful for **optimizing performance** when you need to **update multiple** chunks at the same time.
|
|
116
116
|
|
|
@@ -166,7 +166,7 @@ The batch function ensures that:
|
|
|
166
166
|
- Nested batches are handled correctly
|
|
167
167
|
- Updates are processed even if an error occurs (using try/finally)
|
|
168
168
|
|
|
169
|
-
### 1.
|
|
169
|
+
### 1.4. **Middleware**
|
|
170
170
|
|
|
171
171
|
Middleware allows you to customize how values are set in a **chunk**. For example, you can add **logging**, **validation**, or any custom behavior when a chunk's value changes.
|
|
172
172
|
|
|
@@ -194,6 +194,50 @@ age.set(30); // Logs: "Setting value: 30"
|
|
|
194
194
|
age.set(-5); // Throws an error: "Value must be non-negative!"
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
+
### 1.4.1. Middleware: Undo & Redo (withHistory)
|
|
198
|
+
|
|
199
|
+
The **withHistory** middleware extends a chunk to support undo and redo functionality. This allows you to navigate back and forth between previous **states**, making it useful for implementing features like **undo/redo**, form history, and state time travel.
|
|
200
|
+
|
|
201
|
+
### Usage
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { chunk } from "stunk";
|
|
205
|
+
import { withHistory } from "stunk/middleware"; // Import the history middleware
|
|
206
|
+
|
|
207
|
+
const counter = withHistory(chunk(0));
|
|
208
|
+
|
|
209
|
+
counter.set(10);
|
|
210
|
+
counter.set(20);
|
|
211
|
+
|
|
212
|
+
console.log(counter.get()); // 20
|
|
213
|
+
|
|
214
|
+
counter.undo(); // Go back one step
|
|
215
|
+
console.log(counter.get()); // 10
|
|
216
|
+
|
|
217
|
+
counter.redo(); // Go forward one step
|
|
218
|
+
console.log(counter.get()); // 20
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Available Methods**
|
|
222
|
+
|
|
223
|
+
| Method | Description |
|
|
224
|
+
| ---------------- | ----------------------------------------------------------- |
|
|
225
|
+
| `undo()` | Reverts to the previous state (if available). |
|
|
226
|
+
| `redo()` | Moves forward to the next state (if available). |
|
|
227
|
+
| `canUndo()` | Returns `true` if there are past states available. |
|
|
228
|
+
| `canRedo()` | Returns `true` if there are future states available. |
|
|
229
|
+
| `getHistory()` | Returns an `array` of all past states. |
|
|
230
|
+
| `clearHistory()` | Clears all stored history and keeps only the current state. |
|
|
231
|
+
|
|
232
|
+
**Example: Limiting History Size (Optional)**
|
|
233
|
+
You can specify a max history size to prevent excessive memory usage.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const counter = withHistory(chunk(0), { maxHistory: 5 }); // Only keeps the last 5 changes -- default is 100.
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This prevents the history from growing indefinitely and ensures efficient memory usage.
|
|
240
|
+
|
|
197
241
|
## 2. **Atomic State Technique**
|
|
198
242
|
|
|
199
243
|
The **Atomic State** technique is all about breaking down your state into small, manageable chunks. This allows you to:
|
|
@@ -14,6 +14,28 @@ export function batch(callback) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
export function select(sourceChunk, selector) {
|
|
18
|
+
const initialValue = selector(sourceChunk.get());
|
|
19
|
+
const selectedChunk = chunk(initialValue);
|
|
20
|
+
let previousSelected = initialValue;
|
|
21
|
+
// Subscribe to source changes with equality checking
|
|
22
|
+
sourceChunk.subscribe((newValue) => {
|
|
23
|
+
const newSelected = selector(newValue);
|
|
24
|
+
// Only update if the selected value actually changed
|
|
25
|
+
if (!Object.is(newSelected, previousSelected)) {
|
|
26
|
+
previousSelected = newSelected;
|
|
27
|
+
selectedChunk.set(newSelected);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// Return read-only version of the chunk
|
|
31
|
+
return {
|
|
32
|
+
...selectedChunk,
|
|
33
|
+
// Prevent setting values directly on the selector
|
|
34
|
+
set: () => {
|
|
35
|
+
throw new Error('Cannot set values directly on a selector. Modify the source chunk instead.');
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
17
39
|
export function chunk(initialValue, middleware = []) {
|
|
18
40
|
if (initialValue === undefined || initialValue === null) {
|
|
19
41
|
throw new Error("Initial value cannot be undefined or null.");
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
export { chunk } from './core';
|
|
2
|
-
export
|
|
3
|
-
export { nonNegativeValidator } from "./middleware/validator";
|
|
1
|
+
export { chunk } from './core/core';
|
|
2
|
+
export * from "./middleware";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function withHistory(baseChunk, options = {}) {
|
|
2
|
+
const { maxHistory = 100 } = options;
|
|
3
|
+
const history = [baseChunk.get()];
|
|
4
|
+
let currentIndex = 0;
|
|
5
|
+
let isHistoryAction = false;
|
|
6
|
+
const historyChunk = {
|
|
7
|
+
...baseChunk,
|
|
8
|
+
set: (newValue) => {
|
|
9
|
+
if (isHistoryAction) {
|
|
10
|
+
baseChunk.set(newValue);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// Remove any future history when setting a new value
|
|
14
|
+
history.splice(currentIndex + 1);
|
|
15
|
+
history.push(newValue);
|
|
16
|
+
// Limit history size
|
|
17
|
+
if (history.length > maxHistory) {
|
|
18
|
+
console.warn("History limit reached. Removing oldest entries.");
|
|
19
|
+
const removeCount = history.length - maxHistory;
|
|
20
|
+
history.splice(0, removeCount);
|
|
21
|
+
currentIndex = Math.max(0, currentIndex - removeCount);
|
|
22
|
+
}
|
|
23
|
+
currentIndex = history.length - 1;
|
|
24
|
+
baseChunk.set(newValue);
|
|
25
|
+
},
|
|
26
|
+
undo: () => {
|
|
27
|
+
if (!historyChunk.canUndo())
|
|
28
|
+
return;
|
|
29
|
+
isHistoryAction = true;
|
|
30
|
+
currentIndex--;
|
|
31
|
+
historyChunk.set(history[currentIndex]);
|
|
32
|
+
isHistoryAction = false;
|
|
33
|
+
},
|
|
34
|
+
redo: () => {
|
|
35
|
+
if (!historyChunk.canRedo())
|
|
36
|
+
return;
|
|
37
|
+
isHistoryAction = true;
|
|
38
|
+
currentIndex++;
|
|
39
|
+
historyChunk.set(history[currentIndex]);
|
|
40
|
+
isHistoryAction = false;
|
|
41
|
+
},
|
|
42
|
+
canUndo: () => currentIndex > 0,
|
|
43
|
+
canRedo: () => currentIndex < history.length - 1,
|
|
44
|
+
getHistory: () => [...history],
|
|
45
|
+
clearHistory: () => {
|
|
46
|
+
const currentValue = baseChunk.get();
|
|
47
|
+
history.length = 0;
|
|
48
|
+
history.push(currentValue);
|
|
49
|
+
currentIndex = 0;
|
|
50
|
+
},
|
|
51
|
+
// Override destroy to clean up history
|
|
52
|
+
destroy: () => {
|
|
53
|
+
history.length = 0;
|
|
54
|
+
baseChunk.destroy();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return historyChunk;
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stunk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Stunk - A framework-agnostic state management library implementing the Atomic State technique, utilizing chunk-based units for efficient state management.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/types/index.d.ts",
|
|
@@ -34,6 +34,32 @@ export function batch(callback: () => void) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
export function select<T, S>(sourceChunk: Chunk<T>, selector: (value: T) => S): Chunk<S> {
|
|
38
|
+
const initialValue = selector(sourceChunk.get());
|
|
39
|
+
const selectedChunk = chunk(initialValue);
|
|
40
|
+
let previousSelected = initialValue;
|
|
41
|
+
|
|
42
|
+
// Subscribe to source changes with equality checking
|
|
43
|
+
sourceChunk.subscribe((newValue) => {
|
|
44
|
+
const newSelected = selector(newValue);
|
|
45
|
+
|
|
46
|
+
// Only update if the selected value actually changed
|
|
47
|
+
if (!Object.is(newSelected, previousSelected)) {
|
|
48
|
+
previousSelected = newSelected;
|
|
49
|
+
selectedChunk.set(newSelected);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Return read-only version of the chunk
|
|
54
|
+
return {
|
|
55
|
+
...selectedChunk,
|
|
56
|
+
// Prevent setting values directly on the selector
|
|
57
|
+
set: () => {
|
|
58
|
+
throw new Error('Cannot set values directly on a selector. Modify the source chunk instead.');
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
export function chunk<T>(initialValue: T, middleware: Middleware<T>[] = []): Chunk<T> {
|
|
38
64
|
if (initialValue === undefined || initialValue === null) {
|
|
39
65
|
throw new Error("Initial value cannot be undefined or null.");
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export { chunk } from './core';
|
|
2
|
-
export type { Chunk } from './core';
|
|
1
|
+
export { chunk } from './core/core';
|
|
2
|
+
export type { Chunk } from './core/core';
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
export { nonNegativeValidator } from "./middleware/validator";
|
|
4
|
+
export * from "./middleware";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Chunk } from "../core/core";
|
|
2
|
+
|
|
3
|
+
export interface ChunkWithHistory<T> extends Chunk<T> {
|
|
4
|
+
undo: () => void;
|
|
5
|
+
redo: () => void;
|
|
6
|
+
canUndo: () => boolean;
|
|
7
|
+
canRedo: () => boolean;
|
|
8
|
+
getHistory: () => T[];
|
|
9
|
+
clearHistory: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function withHistory<T>(
|
|
13
|
+
baseChunk: Chunk<T>,
|
|
14
|
+
options: { maxHistory?: number } = {}
|
|
15
|
+
): ChunkWithHistory<T> {
|
|
16
|
+
const { maxHistory = 100 } = options;
|
|
17
|
+
const history: T[] = [baseChunk.get()];
|
|
18
|
+
let currentIndex = 0;
|
|
19
|
+
let isHistoryAction = false;
|
|
20
|
+
|
|
21
|
+
const historyChunk: ChunkWithHistory<T> = {
|
|
22
|
+
...baseChunk,
|
|
23
|
+
|
|
24
|
+
set: (newValue: T) => {
|
|
25
|
+
if (isHistoryAction) {
|
|
26
|
+
baseChunk.set(newValue);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Remove any future history when setting a new value
|
|
31
|
+
history.splice(currentIndex + 1);
|
|
32
|
+
history.push(newValue);
|
|
33
|
+
|
|
34
|
+
// Limit history size
|
|
35
|
+
if (history.length > maxHistory) {
|
|
36
|
+
console.warn("History limit reached. Removing oldest entries.");
|
|
37
|
+
const removeCount = history.length - maxHistory;
|
|
38
|
+
history.splice(0, removeCount);
|
|
39
|
+
currentIndex = Math.max(0, currentIndex - removeCount);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
currentIndex = history.length - 1;
|
|
43
|
+
baseChunk.set(newValue);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
undo: () => {
|
|
47
|
+
if (!historyChunk.canUndo()) return;
|
|
48
|
+
|
|
49
|
+
isHistoryAction = true;
|
|
50
|
+
currentIndex--;
|
|
51
|
+
historyChunk.set(history[currentIndex]);
|
|
52
|
+
isHistoryAction = false;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
redo: () => {
|
|
56
|
+
if (!historyChunk.canRedo()) return;
|
|
57
|
+
|
|
58
|
+
isHistoryAction = true;
|
|
59
|
+
currentIndex++;
|
|
60
|
+
historyChunk.set(history[currentIndex]);
|
|
61
|
+
isHistoryAction = false;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
canUndo: () => currentIndex > 0,
|
|
65
|
+
|
|
66
|
+
canRedo: () => currentIndex < history.length - 1,
|
|
67
|
+
|
|
68
|
+
getHistory: () => [...history],
|
|
69
|
+
|
|
70
|
+
clearHistory: () => {
|
|
71
|
+
const currentValue = baseChunk.get();
|
|
72
|
+
history.length = 0;
|
|
73
|
+
history.push(currentValue);
|
|
74
|
+
currentIndex = 0;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Override destroy to clean up history
|
|
78
|
+
destroy: () => {
|
|
79
|
+
history.length = 0;
|
|
80
|
+
baseChunk.destroy();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return historyChunk;
|
|
85
|
+
|
|
86
|
+
}
|
package/src/middleware/logger.ts
CHANGED
package/tests/chunk.test.ts
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { batch, chunk } from "../src/core/core";
|
|
2
|
+
import { withHistory } from "../src/middleware/history";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('Chunk with History', () => {
|
|
6
|
+
it('should maintain history of changes', () => {
|
|
7
|
+
const baseChunk = chunk(0);
|
|
8
|
+
const historyChunk = withHistory(baseChunk);
|
|
9
|
+
|
|
10
|
+
historyChunk.set(1);
|
|
11
|
+
historyChunk.set(2);
|
|
12
|
+
historyChunk.set(3);
|
|
13
|
+
|
|
14
|
+
expect(historyChunk.getHistory()).toEqual([0, 1, 2, 3]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should handle undo and redo operations', () => {
|
|
18
|
+
const baseChunk = chunk(0);
|
|
19
|
+
const historyChunk = withHistory(baseChunk);
|
|
20
|
+
const callback = jest.fn();
|
|
21
|
+
|
|
22
|
+
historyChunk.subscribe(callback);
|
|
23
|
+
callback.mockClear(); // Clear initial subscription call
|
|
24
|
+
|
|
25
|
+
historyChunk.set(1);
|
|
26
|
+
historyChunk.set(2);
|
|
27
|
+
|
|
28
|
+
expect(historyChunk.get()).toBe(2);
|
|
29
|
+
|
|
30
|
+
historyChunk.undo();
|
|
31
|
+
expect(historyChunk.get()).toBe(1);
|
|
32
|
+
|
|
33
|
+
historyChunk.undo();
|
|
34
|
+
expect(historyChunk.get()).toBe(0);
|
|
35
|
+
|
|
36
|
+
historyChunk.redo();
|
|
37
|
+
expect(historyChunk.get()).toBe(1);
|
|
38
|
+
|
|
39
|
+
historyChunk.redo();
|
|
40
|
+
expect(historyChunk.get()).toBe(2);
|
|
41
|
+
|
|
42
|
+
expect(callback).toHaveBeenCalledTimes(6); // 2 sets + 2 undos + 2 redos
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle branching history', () => {
|
|
46
|
+
const baseChunk = chunk(0);
|
|
47
|
+
const historyChunk = withHistory(baseChunk);
|
|
48
|
+
|
|
49
|
+
historyChunk.set(1);
|
|
50
|
+
historyChunk.set(2);
|
|
51
|
+
historyChunk.undo();
|
|
52
|
+
historyChunk.set(3); // This should create a new branch
|
|
53
|
+
|
|
54
|
+
expect(historyChunk.getHistory()).toEqual([0, 1, 3]);
|
|
55
|
+
expect(historyChunk.get()).toBe(3);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should respect maxHistory limit', () => {
|
|
59
|
+
const baseChunk = chunk(0);
|
|
60
|
+
const historyChunk = withHistory(baseChunk, { maxHistory: 3 });
|
|
61
|
+
|
|
62
|
+
historyChunk.set(1);
|
|
63
|
+
historyChunk.set(2);
|
|
64
|
+
historyChunk.set(3);
|
|
65
|
+
historyChunk.set(4);
|
|
66
|
+
|
|
67
|
+
expect(historyChunk.getHistory()).toEqual([2, 3, 4]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle canUndo and canRedo correctly', () => {
|
|
71
|
+
const baseChunk = chunk(0);
|
|
72
|
+
const historyChunk = withHistory(baseChunk);
|
|
73
|
+
|
|
74
|
+
expect(historyChunk.canUndo()).toBe(false);
|
|
75
|
+
expect(historyChunk.canRedo()).toBe(false);
|
|
76
|
+
|
|
77
|
+
historyChunk.set(1);
|
|
78
|
+
expect(historyChunk.canUndo()).toBe(true);
|
|
79
|
+
expect(historyChunk.canRedo()).toBe(false);
|
|
80
|
+
|
|
81
|
+
historyChunk.undo();
|
|
82
|
+
expect(historyChunk.canUndo()).toBe(false);
|
|
83
|
+
expect(historyChunk.canRedo()).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should clear history properly', () => {
|
|
87
|
+
const baseChunk = chunk(0);
|
|
88
|
+
const historyChunk = withHistory(baseChunk);
|
|
89
|
+
|
|
90
|
+
historyChunk.set(1);
|
|
91
|
+
historyChunk.set(2);
|
|
92
|
+
|
|
93
|
+
historyChunk.clearHistory();
|
|
94
|
+
|
|
95
|
+
expect(historyChunk.getHistory()).toEqual([2]);
|
|
96
|
+
expect(historyChunk.canUndo()).toBe(false);
|
|
97
|
+
expect(historyChunk.canRedo()).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/tests/middleware.test.ts
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { chunk, select } from '../src/core/core';
|
|
2
|
+
|
|
3
|
+
describe('select', () => {
|
|
4
|
+
it('should create a selector that initially returns the correct value', () => {
|
|
5
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
6
|
+
const nameSelector = select(source, user => user.name);
|
|
7
|
+
|
|
8
|
+
expect(nameSelector.get()).toBe('John');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should update when selected value changes', () => {
|
|
12
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
13
|
+
const nameSelector = select(source, user => user.name);
|
|
14
|
+
|
|
15
|
+
source.set({ name: 'Jane', age: 25 });
|
|
16
|
+
expect(nameSelector.get()).toBe('Jane');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should not notify subscribers when non-selected values change', () => {
|
|
20
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
21
|
+
const nameSelector = select(source, user => user.name);
|
|
22
|
+
|
|
23
|
+
const subscriber = jest.fn();
|
|
24
|
+
nameSelector.subscribe(subscriber);
|
|
25
|
+
|
|
26
|
+
// Reset the mock to ignore initial call
|
|
27
|
+
subscriber.mockReset();
|
|
28
|
+
|
|
29
|
+
// Update age only
|
|
30
|
+
source.set({ name: 'John', age: 26 });
|
|
31
|
+
|
|
32
|
+
expect(subscriber).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should notify subscribers when selected value changes', () => {
|
|
36
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
37
|
+
const nameSelector = select(source, user => user.name);
|
|
38
|
+
|
|
39
|
+
const subscriber = jest.fn();
|
|
40
|
+
nameSelector.subscribe(subscriber);
|
|
41
|
+
|
|
42
|
+
// Reset the mock to ignore initial call
|
|
43
|
+
subscriber.mockReset();
|
|
44
|
+
|
|
45
|
+
source.set({ name: 'Jane', age: 25 });
|
|
46
|
+
|
|
47
|
+
expect(subscriber).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(subscriber).toHaveBeenCalledWith('Jane');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should prevent direct modifications to selector', () => {
|
|
52
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
53
|
+
const nameSelector = select(source, user => user.name);
|
|
54
|
+
|
|
55
|
+
expect(() => {
|
|
56
|
+
nameSelector.set('Jane');
|
|
57
|
+
}).toThrow('Cannot set values directly on a selector');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should work with complex selectors', () => {
|
|
61
|
+
const source = chunk({ user: { profile: { name: 'John' } } });
|
|
62
|
+
const nameSelector = select(source, state => state.user.profile.name);
|
|
63
|
+
|
|
64
|
+
expect(nameSelector.get()).toBe('John');
|
|
65
|
+
|
|
66
|
+
source.set({ user: { profile: { name: 'Jane' } } });
|
|
67
|
+
expect(nameSelector.get()).toBe('Jane');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle array selectors', () => {
|
|
71
|
+
const source = chunk({ items: [1, 2, 3] });
|
|
72
|
+
const firstItemSelector = select(source, state => state.items[0]);
|
|
73
|
+
|
|
74
|
+
expect(firstItemSelector.get()).toBe(1);
|
|
75
|
+
|
|
76
|
+
source.set({ items: [4, 2, 3] });
|
|
77
|
+
expect(firstItemSelector.get()).toBe(4);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should work with computed values', () => {
|
|
81
|
+
const source = chunk({ numbers: [1, 2, 3, 4, 5] });
|
|
82
|
+
const sumSelector = select(source, state =>
|
|
83
|
+
state.numbers.reduce((sum, num) => sum + num, 0)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(sumSelector.get()).toBe(15);
|
|
87
|
+
|
|
88
|
+
source.set({ numbers: [1, 2, 3] });
|
|
89
|
+
expect(sumSelector.get()).toBe(6);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should properly clean up subscriptions on destroy', () => {
|
|
93
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
94
|
+
const nameSelector = select(source, user => user.name);
|
|
95
|
+
|
|
96
|
+
const subscriber = jest.fn();
|
|
97
|
+
const unsubscribe = nameSelector.subscribe(subscriber);
|
|
98
|
+
|
|
99
|
+
// Reset mock to ignore initial call
|
|
100
|
+
subscriber.mockReset();
|
|
101
|
+
|
|
102
|
+
unsubscribe();
|
|
103
|
+
nameSelector.destroy();
|
|
104
|
+
source.set({ name: 'Jane', age: 25 });
|
|
105
|
+
|
|
106
|
+
expect(subscriber).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should work with multiple independent selectors', () => {
|
|
110
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
111
|
+
const nameSelector = select(source, user => user.name);
|
|
112
|
+
const ageSelector = select(source, user => user.age);
|
|
113
|
+
|
|
114
|
+
const nameSubscriber = jest.fn();
|
|
115
|
+
const ageSubscriber = jest.fn();
|
|
116
|
+
|
|
117
|
+
nameSelector.subscribe(nameSubscriber);
|
|
118
|
+
ageSelector.subscribe(ageSubscriber);
|
|
119
|
+
|
|
120
|
+
// Reset mocks to ignore initial calls
|
|
121
|
+
nameSubscriber.mockReset();
|
|
122
|
+
ageSubscriber.mockReset();
|
|
123
|
+
|
|
124
|
+
source.set({ name: 'John', age: 26 });
|
|
125
|
+
expect(nameSubscriber).not.toHaveBeenCalled();
|
|
126
|
+
expect(ageSubscriber).toHaveBeenCalledWith(26);
|
|
127
|
+
|
|
128
|
+
source.set({ name: 'Jane', age: 26 });
|
|
129
|
+
expect(nameSubscriber).toHaveBeenCalledWith('Jane');
|
|
130
|
+
expect(ageSubscriber).toHaveBeenCalledTimes(1); // Still from previous update
|
|
131
|
+
});
|
|
132
|
+
});
|