stunk 1.0.1 → 1.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/README.md +125 -0
- package/dist/core/computed.js +26 -47
- package/dist/core/core.js +0 -22
- package/dist/core/selector.js +23 -0
- package/dist/index.js +3 -1
- package/package.json +6 -3
- package/src/core/computed.ts +41 -51
- package/src/core/core.ts +0 -26
- package/src/core/selector.ts +27 -0
- package/src/index.ts +7 -3
- package/tests/computed.test.ts +93 -0
- package/tests/history.test.ts +1 -1
- package/tests/select-chunk.test.ts +2 -1
package/README.md
CHANGED
|
@@ -171,6 +171,110 @@ const counter = withHistory(chunk(0), { maxHistory: 5 });
|
|
|
171
171
|
|
|
172
172
|
This prevents the history from growing indefinitely and ensures efficient memory usage.
|
|
173
173
|
|
|
174
|
+
## Computed
|
|
175
|
+
|
|
176
|
+
Computed Chunks in Stunk allow you to create state derived from other chunks in a reactive way. Unlike derived chunks, computed chunks can depend on multiple sources, and they automatically recalculate when any of the source chunks change.
|
|
177
|
+
|
|
178
|
+
- Multiple Dependencies: Can depend on multiple chunks.
|
|
179
|
+
- Memoization: Only recalculates when dependencies change.
|
|
180
|
+
- Type-Safe: Fully typed in TypeScript for safe data handling.
|
|
181
|
+
- Reactive: Automatically updates subscribers when any dependency changes.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { chunk, computed } from "stunk";
|
|
185
|
+
|
|
186
|
+
const firstNameChunk = chunk("John");
|
|
187
|
+
const lastNameChunk = chunk("Doe");
|
|
188
|
+
const ageChunk = chunk(30);
|
|
189
|
+
// Create a computed chunk that depends on multiple sources
|
|
190
|
+
|
|
191
|
+
const fullInfoChunk = computed(
|
|
192
|
+
[firstNameChunk, lastNameChunk, ageChunk],
|
|
193
|
+
(firstName, lastName, age) => ({
|
|
194
|
+
fullName: `${firstName} ${lastName}`,
|
|
195
|
+
isAdult: age >= 18,
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
firstNameChunk.set("Ola");
|
|
200
|
+
ageChunk.set(10);
|
|
201
|
+
|
|
202
|
+
console.log(fullInfoChunk.get());
|
|
203
|
+
// ✅ { fullName: "Jane Doe", isAdult: true }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Computed chunks are ideal for scenarios where state depends on multiple sources or needs complex calculations. They ensure your application remains performant and maintainable.
|
|
207
|
+
|
|
208
|
+
### Advanced Examples
|
|
209
|
+
|
|
210
|
+
Form Validation Example
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// With derive - single field validation
|
|
214
|
+
const emailChunk = chunk("user@example.com");
|
|
215
|
+
const isValidEmailChunk = emailChunk.derive((email) =>
|
|
216
|
+
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// With computed - full form validation
|
|
220
|
+
const usernameChunk = chunk("john");
|
|
221
|
+
const emailChunk = chunk("user@example.com");
|
|
222
|
+
const passwordChunk = chunk("pass123");
|
|
223
|
+
const confirmPasswordChunk = chunk("pass123");
|
|
224
|
+
|
|
225
|
+
const formValidationChunk = computed(
|
|
226
|
+
[usernameChunk, emailChunk, passwordChunk, confirmPasswordChunk],
|
|
227
|
+
(username, email, password, confirmPass) => ({
|
|
228
|
+
isUsernameValid: username.length >= 3,
|
|
229
|
+
isEmailValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
|
|
230
|
+
isPasswordValid: password.length >= 6,
|
|
231
|
+
doPasswordsMatch: password === confirmPass,
|
|
232
|
+
isFormValid:
|
|
233
|
+
username.length >= 3 &&
|
|
234
|
+
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) &&
|
|
235
|
+
password.length >= 6 &&
|
|
236
|
+
password === confirmPass,
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
console.log(formValidationChunk.get());
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Data Filtering Example
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// With derive - simple filter
|
|
247
|
+
const postsChunk = chunk([
|
|
248
|
+
{ id: 1, title: "Post 1", published: true },
|
|
249
|
+
{ id: 2, title: "Post 2", published: false },
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
const publishedPostsChunk = postsChunk.derive((posts) =>
|
|
253
|
+
posts.filter((post) => post.published)
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// With computed - complex filtering with multiple conditions
|
|
257
|
+
const postsChunk = chunk([
|
|
258
|
+
{ id: 1, title: "Post 1", category: "tech", date: "2024-01-01" },
|
|
259
|
+
]);
|
|
260
|
+
const categoryFilterChunk = chunk("tech");
|
|
261
|
+
const dateRangeChunk = chunk({ start: "2024-01-01", end: "2024-02-01" });
|
|
262
|
+
const searchTermChunk = chunk("");
|
|
263
|
+
|
|
264
|
+
const filteredPostsChunk = computed(
|
|
265
|
+
[postsChunk, categoryFilterChunk, dateRangeChunk, searchTermChunk],
|
|
266
|
+
(posts, category, dateRange, searchTerm) =>
|
|
267
|
+
posts.filter(
|
|
268
|
+
(post) =>
|
|
269
|
+
(!category || post.category === category) &&
|
|
270
|
+
(!dateRange ||
|
|
271
|
+
(post.date >= dateRange.start && post.date <= dateRange.end)) &&
|
|
272
|
+
(!searchTerm ||
|
|
273
|
+
post.title.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
274
|
+
)
|
|
275
|
+
);
|
|
276
|
+
```
|
|
277
|
+
|
|
174
278
|
## State Persistence
|
|
175
279
|
|
|
176
280
|
Stunk provides a persistence middleware to automatically save state changes to storage (localStorage, sessionStorage, etc).
|
|
@@ -189,6 +293,16 @@ counterChunk.set({ count: 1 });
|
|
|
189
293
|
|
|
190
294
|
## Async State
|
|
191
295
|
|
|
296
|
+
Async Chunks in Stunk are designed to manage asynchronous state seamlessly. They handle loading, error, and data states automatically, making it easier to work with APIs and other asynchronous operations.
|
|
297
|
+
|
|
298
|
+
Key Features
|
|
299
|
+
|
|
300
|
+
- Built-in Loading and Error States: Automatically manages loading, error, and data properties.
|
|
301
|
+
|
|
302
|
+
- Type-Safe: Fully typed in TypeScript, ensuring safe data handling.
|
|
303
|
+
|
|
304
|
+
- Optimistic Updates: Update state optimistically and revert if needed.
|
|
305
|
+
|
|
192
306
|
```typescript
|
|
193
307
|
import { asyncChunk } from "stunk";
|
|
194
308
|
|
|
@@ -198,6 +312,7 @@ type User = {
|
|
|
198
312
|
email: string;
|
|
199
313
|
};
|
|
200
314
|
|
|
315
|
+
// Create an Async Chunk
|
|
201
316
|
const user = asyncChunk<User>(async () => {
|
|
202
317
|
const response = await fetch("/api/user");
|
|
203
318
|
return response.json(); // TypeScript expects this to return User;
|
|
@@ -212,14 +327,24 @@ user.subscribe((state) => {
|
|
|
212
327
|
}
|
|
213
328
|
});
|
|
214
329
|
|
|
330
|
+
// Subscribe to state changes
|
|
215
331
|
user.subscribe(({ loading, error, data }) => {
|
|
216
332
|
if (loading) console.log("Loading...");
|
|
217
333
|
if (error) console.log("Error:", error);
|
|
218
334
|
if (data) console.log("User:", data);
|
|
219
335
|
});
|
|
336
|
+
```
|
|
220
337
|
|
|
338
|
+
**Reloading Data**
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
221
341
|
// Reload data
|
|
222
342
|
await user.reload();
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Optimistic Updates**
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
223
348
|
|
|
224
349
|
// Optimistic update
|
|
225
350
|
user.mutate((currentData) => ({
|
package/dist/core/computed.js
CHANGED
|
@@ -1,61 +1,40 @@
|
|
|
1
|
-
import { isChunk } from "../utils";
|
|
2
1
|
import { chunk } from "./core";
|
|
3
|
-
export function computed(computeFn) {
|
|
4
|
-
|
|
5
|
-
let
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return this[prop];
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
// Initial computation
|
|
21
|
-
let cachedValue;
|
|
22
|
-
let isDirty = true;
|
|
23
|
-
const computeValue = () => {
|
|
24
|
-
if (!isDirty)
|
|
25
|
-
return cachedValue;
|
|
26
|
-
// Reset dependencies
|
|
27
|
-
dependencies.clear();
|
|
28
|
-
// Set the current computation context
|
|
29
|
-
currentComputation = computeFn;
|
|
30
|
-
try {
|
|
31
|
-
// Compute with tracking
|
|
32
|
-
cachedValue = computeFn.call(trackingProxy);
|
|
33
|
-
isDirty = false;
|
|
34
|
-
}
|
|
35
|
-
finally {
|
|
36
|
-
// Clear the current computation context
|
|
37
|
-
currentComputation = null;
|
|
2
|
+
export function computed(dependencies, computeFn) {
|
|
3
|
+
let isDirty = false; // Initialized to false
|
|
4
|
+
let cachedValue = computeFn(...dependencies.map(d => d.get()));
|
|
5
|
+
const recalculate = () => {
|
|
6
|
+
const values = dependencies.map(dep => dep.get());
|
|
7
|
+
cachedValue = computeFn(...values);
|
|
8
|
+
isDirty = false; // Reset to false after recomputation
|
|
9
|
+
};
|
|
10
|
+
const computedChunk = chunk(cachedValue);
|
|
11
|
+
const originalGet = computedChunk.get;
|
|
12
|
+
computedChunk.get = () => {
|
|
13
|
+
if (isDirty) {
|
|
14
|
+
recalculate();
|
|
15
|
+
computedChunk.set(cachedValue); // Update the chunk value after recomputation
|
|
38
16
|
}
|
|
39
|
-
return cachedValue;
|
|
17
|
+
return cachedValue; // Return the cached value directly
|
|
40
18
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Subscribe to all detected dependencies
|
|
44
|
-
dependencies.forEach(dep => {
|
|
19
|
+
const lastValues = dependencies.map(dep => dep.get());
|
|
20
|
+
dependencies.forEach((dep, index) => {
|
|
45
21
|
dep.subscribe(() => {
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
const newValue = dep.get();
|
|
23
|
+
if (newValue !== lastValues[index] && !isDirty) {
|
|
24
|
+
lastValues[index] = newValue;
|
|
25
|
+
isDirty = true;
|
|
26
|
+
}
|
|
48
27
|
});
|
|
49
28
|
});
|
|
50
29
|
return {
|
|
51
30
|
...computedChunk,
|
|
52
|
-
|
|
31
|
+
isDirty: () => isDirty,
|
|
32
|
+
recompute: () => {
|
|
53
33
|
if (isDirty) {
|
|
54
|
-
|
|
34
|
+
recalculate();
|
|
35
|
+
computedChunk.set(cachedValue); // Update the chunk value after manual recomputation
|
|
55
36
|
}
|
|
56
|
-
return cachedValue;
|
|
57
37
|
},
|
|
58
|
-
// Prevent direct setting
|
|
59
38
|
set: () => {
|
|
60
39
|
throw new Error('Cannot directly set a computed value');
|
|
61
40
|
}
|
package/dist/core/core.js
CHANGED
|
@@ -15,28 +15,6 @@ export function batch(callback) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
export function select(sourceChunk, selector) {
|
|
19
|
-
const initialValue = selector(sourceChunk.get());
|
|
20
|
-
const selectedChunk = chunk(initialValue);
|
|
21
|
-
let previousSelected = initialValue;
|
|
22
|
-
// Subscribe to source changes with equality checking
|
|
23
|
-
sourceChunk.subscribe((newValue) => {
|
|
24
|
-
const newSelected = selector(newValue);
|
|
25
|
-
// Only update if the selected value actually changed
|
|
26
|
-
if (!Object.is(newSelected, previousSelected)) {
|
|
27
|
-
previousSelected = newSelected;
|
|
28
|
-
selectedChunk.set(newSelected);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
// Return read-only version of the chunk
|
|
32
|
-
return {
|
|
33
|
-
...selectedChunk,
|
|
34
|
-
// Prevent setting values directly on the selector
|
|
35
|
-
set: () => {
|
|
36
|
-
throw new Error('Cannot set values directly on a selector. Modify the source chunk instead.');
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
18
|
export function chunk(initialValue, middleware = []) {
|
|
41
19
|
if (initialValue === undefined || initialValue === null) {
|
|
42
20
|
throw new Error("Initial value cannot be undefined or null.");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { chunk } from "./core";
|
|
2
|
+
export function select(sourceChunk, selector) {
|
|
3
|
+
const initialValue = selector(sourceChunk.get());
|
|
4
|
+
const selectedChunk = chunk(initialValue);
|
|
5
|
+
let previousSelected = initialValue;
|
|
6
|
+
// Subscribe to source changes with equality checking
|
|
7
|
+
sourceChunk.subscribe((newValue) => {
|
|
8
|
+
const newSelected = selector(newValue);
|
|
9
|
+
// Only update if the selected value actually changed
|
|
10
|
+
if (!Object.is(newSelected, previousSelected)) {
|
|
11
|
+
previousSelected = newSelected;
|
|
12
|
+
selectedChunk.set(newSelected);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
// Return read-only version of the chunk
|
|
16
|
+
return {
|
|
17
|
+
...selectedChunk,
|
|
18
|
+
// Prevent setting values directly on the selector
|
|
19
|
+
set: () => {
|
|
20
|
+
throw new Error('Cannot set values directly on a selector. Modify the source chunk instead.');
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { chunk, batch
|
|
1
|
+
export { chunk, batch } from './core/core';
|
|
2
2
|
export { asyncChunk } from './core/asyncChunk';
|
|
3
3
|
export { computed } from './core/computed';
|
|
4
|
+
export { select } from './core/selector';
|
|
5
|
+
export { combineAsyncChunks } from './utils';
|
|
4
6
|
export * from "./middleware";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stunk",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Stunk - A framework-agnostic state management library
|
|
3
|
+
"version": "1.2.2",
|
|
4
|
+
"description": "Stunk - A lightweight, framework-agnostic state management library using chunk-based units for efficient state updates.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/types/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"react",
|
|
21
21
|
"React state management",
|
|
22
22
|
"Vue state management",
|
|
23
|
-
"management"
|
|
23
|
+
"management",
|
|
24
|
+
"TypeScript state management",
|
|
25
|
+
"reactive state library",
|
|
26
|
+
"frontend state management"
|
|
24
27
|
],
|
|
25
28
|
"author": "AbdulAzeez",
|
|
26
29
|
"license": "MIT",
|
package/src/core/computed.ts
CHANGED
|
@@ -1,75 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { chunk, Chunk } from "./core";
|
|
1
|
+
import { Chunk, chunk, batch } from "./core";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
let currentComputation: (() => T) | null = null;
|
|
3
|
+
// Helper type to extract the value type from a Chunk
|
|
4
|
+
type ChunkValue<T> = T extends Chunk<infer U> ? U : never;
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
// Helper type to transform an array of Chunks into an array of their value types
|
|
7
|
+
type DependencyValues<T extends Chunk<any>[]> = {
|
|
8
|
+
[K in keyof T]: T[K] extends Chunk<any> ? ChunkValue<T[K]> : never;
|
|
9
|
+
};
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (isChunk(chunkValue)) {
|
|
16
|
-
dependencies.add(chunkValue);
|
|
17
|
-
return chunkValue.get();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return (this as any)[prop];
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// Initial computation
|
|
25
|
-
let cachedValue: T;
|
|
26
|
-
let isDirty = true;
|
|
27
|
-
|
|
28
|
-
const computeValue = () => {
|
|
29
|
-
if (!isDirty) return cachedValue
|
|
11
|
+
export interface Computed<T> extends Chunk<T> {
|
|
12
|
+
isDirty: () => boolean;
|
|
13
|
+
recompute: () => void;
|
|
14
|
+
}
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
|
|
16
|
+
export function computed<TDeps extends Chunk<any>[], TResult>(
|
|
17
|
+
dependencies: [...TDeps],
|
|
18
|
+
computeFn: (...args: DependencyValues<TDeps>) => TResult
|
|
19
|
+
): Computed<TResult> {
|
|
20
|
+
let isDirty = false; // Initialized to false
|
|
21
|
+
let cachedValue: TResult = computeFn(...dependencies.map(d => d.get()) as DependencyValues<TDeps>);
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
const recalculate = () => {
|
|
24
|
+
const values = dependencies.map(dep => dep.get()) as DependencyValues<TDeps>;
|
|
25
|
+
cachedValue = computeFn(...values);
|
|
26
|
+
isDirty = false; // Reset to false after recomputation
|
|
27
|
+
};
|
|
36
28
|
|
|
29
|
+
const computedChunk = chunk(cachedValue);
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Clear the current computation context
|
|
44
|
-
currentComputation = null;
|
|
31
|
+
const originalGet = computedChunk.get;
|
|
32
|
+
computedChunk.get = () => {
|
|
33
|
+
if (isDirty) {
|
|
34
|
+
recalculate();
|
|
35
|
+
computedChunk.set(cachedValue); // Update the chunk value after recomputation
|
|
45
36
|
}
|
|
46
|
-
return cachedValue;
|
|
47
|
-
|
|
48
|
-
}
|
|
37
|
+
return cachedValue; // Return the cached value directly
|
|
38
|
+
};
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
const computedChunk = chunk(computeValue());
|
|
40
|
+
const lastValues = dependencies.map(dep => dep.get());
|
|
52
41
|
|
|
53
|
-
|
|
54
|
-
dependencies.forEach(dep => {
|
|
42
|
+
dependencies.forEach((dep, index) => {
|
|
55
43
|
dep.subscribe(() => {
|
|
56
|
-
|
|
57
|
-
|
|
44
|
+
const newValue = dep.get();
|
|
45
|
+
if (newValue !== lastValues[index] && !isDirty) {
|
|
46
|
+
lastValues[index] = newValue;
|
|
47
|
+
isDirty = true;
|
|
48
|
+
}
|
|
58
49
|
});
|
|
59
50
|
});
|
|
60
51
|
|
|
61
52
|
return {
|
|
62
53
|
...computedChunk,
|
|
63
|
-
|
|
54
|
+
isDirty: () => isDirty,
|
|
55
|
+
recompute: () => {
|
|
64
56
|
if (isDirty) {
|
|
65
|
-
|
|
57
|
+
recalculate();
|
|
58
|
+
computedChunk.set(cachedValue); // Update the chunk value after manual recomputation
|
|
66
59
|
}
|
|
67
|
-
return cachedValue;
|
|
68
60
|
},
|
|
69
|
-
// Prevent direct setting
|
|
70
61
|
set: () => {
|
|
71
62
|
throw new Error('Cannot directly set a computed value');
|
|
72
63
|
}
|
|
73
64
|
};
|
|
74
|
-
|
|
75
65
|
}
|
package/src/core/core.ts
CHANGED
|
@@ -37,32 +37,6 @@ export function batch(callback: () => void) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export function select<T, S>(sourceChunk: Chunk<T>, selector: (value: T) => S): Chunk<S> {
|
|
41
|
-
const initialValue = selector(sourceChunk.get());
|
|
42
|
-
const selectedChunk = chunk(initialValue);
|
|
43
|
-
let previousSelected = initialValue;
|
|
44
|
-
|
|
45
|
-
// Subscribe to source changes with equality checking
|
|
46
|
-
sourceChunk.subscribe((newValue) => {
|
|
47
|
-
const newSelected = selector(newValue);
|
|
48
|
-
|
|
49
|
-
// Only update if the selected value actually changed
|
|
50
|
-
if (!Object.is(newSelected, previousSelected)) {
|
|
51
|
-
previousSelected = newSelected;
|
|
52
|
-
selectedChunk.set(newSelected);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Return read-only version of the chunk
|
|
57
|
-
return {
|
|
58
|
-
...selectedChunk,
|
|
59
|
-
// Prevent setting values directly on the selector
|
|
60
|
-
set: () => {
|
|
61
|
-
throw new Error('Cannot set values directly on a selector. Modify the source chunk instead.');
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
40
|
export function chunk<T>(initialValue: T, middleware: Middleware<T>[] = []): Chunk<T> {
|
|
67
41
|
if (initialValue === undefined || initialValue === null) {
|
|
68
42
|
throw new Error("Initial value cannot be undefined or null.");
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { chunk, Chunk } from "./core";
|
|
2
|
+
|
|
3
|
+
export function select<T, S>(sourceChunk: Chunk<T>, selector: (value: T) => S): Chunk<S> {
|
|
4
|
+
const initialValue = selector(sourceChunk.get());
|
|
5
|
+
const selectedChunk = chunk(initialValue);
|
|
6
|
+
let previousSelected = initialValue;
|
|
7
|
+
|
|
8
|
+
// Subscribe to source changes with equality checking
|
|
9
|
+
sourceChunk.subscribe((newValue) => {
|
|
10
|
+
const newSelected = selector(newValue);
|
|
11
|
+
|
|
12
|
+
// Only update if the selected value actually changed
|
|
13
|
+
if (!Object.is(newSelected, previousSelected)) {
|
|
14
|
+
previousSelected = newSelected;
|
|
15
|
+
selectedChunk.set(newSelected);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Return read-only version of the chunk
|
|
20
|
+
return {
|
|
21
|
+
...selectedChunk,
|
|
22
|
+
// Prevent setting values directly on the selector
|
|
23
|
+
set: () => {
|
|
24
|
+
throw new Error('Cannot set values directly on a selector. Modify the source chunk instead.');
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
export { chunk, batch
|
|
2
|
-
export { asyncChunk } from './core/asyncChunk'
|
|
3
|
-
export { computed } from './core/computed'
|
|
1
|
+
export { chunk, batch } from './core/core';
|
|
2
|
+
export { asyncChunk } from './core/asyncChunk';
|
|
3
|
+
export { computed } from './core/computed';
|
|
4
|
+
export { select } from './core/selector'
|
|
5
|
+
|
|
6
|
+
export { combineAsyncChunks } from './utils';
|
|
7
|
+
|
|
4
8
|
export type { Chunk, Middleware } from './core/core';
|
|
5
9
|
|
|
6
10
|
export * from "./middleware";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { chunk } from '../src/core/core';
|
|
2
|
+
import { computed } from '../src/core/computed';
|
|
3
|
+
|
|
4
|
+
describe('computed', () => {
|
|
5
|
+
it('should compute the value based on dependencies', () => {
|
|
6
|
+
const num1 = chunk(2);
|
|
7
|
+
const num2 = chunk(3);
|
|
8
|
+
|
|
9
|
+
const sum = computed([num1, num2], (a, b) => a + b);
|
|
10
|
+
|
|
11
|
+
expect(sum.get()).toBe(5);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should recompute when a dependency changes', () => {
|
|
15
|
+
const num1 = chunk(4);
|
|
16
|
+
const num2 = chunk(5);
|
|
17
|
+
|
|
18
|
+
const product = computed([num1, num2], (a, b) => a * b);
|
|
19
|
+
|
|
20
|
+
expect(product.get()).toBe(20);
|
|
21
|
+
|
|
22
|
+
num1.set(10);
|
|
23
|
+
|
|
24
|
+
// Trigger recomputation
|
|
25
|
+
expect(product.get()).toBe(50);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should cache the computed value until a dependency changes', () => {
|
|
29
|
+
const num1 = chunk(1);
|
|
30
|
+
const num2 = chunk(2);
|
|
31
|
+
|
|
32
|
+
const sum = computed([num1, num2], (a, b) => a + b);
|
|
33
|
+
|
|
34
|
+
const initialValue = sum.get();
|
|
35
|
+
expect(initialValue).toBe(3);
|
|
36
|
+
|
|
37
|
+
num1.set(1); // Setting to the same value, should not trigger recompute
|
|
38
|
+
const cachedValue = sum.get();
|
|
39
|
+
expect(cachedValue).toBe(3); // Cached value should be returned
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should mark as dirty when a dependency changes', () => {
|
|
43
|
+
const num1 = chunk(7);
|
|
44
|
+
const num2 = chunk(8);
|
|
45
|
+
|
|
46
|
+
const diff = computed([num1, num2], (a, b) => b - a);
|
|
47
|
+
|
|
48
|
+
expect(diff.isDirty()).toBe(false);
|
|
49
|
+
|
|
50
|
+
num2.set(10);
|
|
51
|
+
|
|
52
|
+
expect(diff.isDirty()).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should throw error when attempting to set computed value', () => {
|
|
56
|
+
const num1 = chunk(10);
|
|
57
|
+
const num2 = chunk(20);
|
|
58
|
+
|
|
59
|
+
const sum = computed([num1, num2], (a, b) => a + b);
|
|
60
|
+
|
|
61
|
+
expect(() => sum.set(100)).toThrow('Cannot directly set a computed value');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should manually recompute the value', () => {
|
|
65
|
+
const num1 = chunk(1);
|
|
66
|
+
const num2 = chunk(2);
|
|
67
|
+
|
|
68
|
+
const sum = computed([num1, num2], (a, b) => a + b);
|
|
69
|
+
|
|
70
|
+
expect(sum.get()).toBe(3);
|
|
71
|
+
|
|
72
|
+
num1.set(4);
|
|
73
|
+
expect(sum.isDirty()).toBe(true);
|
|
74
|
+
|
|
75
|
+
sum.recompute(); // Manually recompute
|
|
76
|
+
expect(sum.get()).toBe(6);
|
|
77
|
+
expect(sum.isDirty()).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should support multiple dependencies', () => {
|
|
81
|
+
const a = chunk(2);
|
|
82
|
+
const b = chunk(3);
|
|
83
|
+
const c = chunk(4);
|
|
84
|
+
|
|
85
|
+
const result = computed([a, b, c], (x, y, z) => x * y + z);
|
|
86
|
+
|
|
87
|
+
expect(result.get()).toBe(10);
|
|
88
|
+
|
|
89
|
+
b.set(5);
|
|
90
|
+
|
|
91
|
+
expect(result.get()).toBe(14);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/tests/history.test.ts
CHANGED