stunk 1.0.1 → 1.2.3

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 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) => ({
@@ -1,61 +1,40 @@
1
- import { isChunk } from "../utils";
2
1
  import { chunk } from "./core";
3
- export function computed(computeFn) {
4
- // Track the currently executing computed function
5
- let currentComputation = null;
6
- // Set to track dependencies
7
- const dependencies = new Set();
8
- const trackingProxy = new Proxy({}, {
9
- get(_, prop) {
10
- if (currentComputation && prop === 'value') {
11
- const chunkValue = this[prop];
12
- if (isChunk(chunkValue)) {
13
- dependencies.add(chunkValue);
14
- return chunkValue.get();
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
- // Create the computed chunk
42
- const computedChunk = chunk(computeValue());
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
- isDirty = true;
47
- computedChunk.set(computeValue());
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
- get: () => {
31
+ isDirty: () => isDirty,
32
+ recompute: () => {
53
33
  if (isDirty) {
54
- return computeValue();
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, select } from './core/core';
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.0.1",
4
- "description": "Stunk - A framework-agnostic state management library implementing the Atomic State technique, utilizing chunk-based units for efficient state management.",
3
+ "version": "1.2.3",
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",
@@ -1,75 +1,65 @@
1
- import { isChunk } from "../utils";
2
- import { chunk, Chunk } from "./core";
1
+ import { Chunk, chunk, batch } from "./core";
3
2
 
4
- export function computed<T>(computeFn: () => T): Chunk<T> {
5
- // Track the currently executing computed function
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
- // Set to track dependencies
9
- const dependencies = new Set<Chunk<any>>();
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
- const trackingProxy = new Proxy({}, {
12
- get(_, prop) {
13
- if (currentComputation && prop === 'value') {
14
- const chunkValue = (this as any)[prop];
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
- // Reset dependencies
32
- dependencies.clear();
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
- // Set the current computation context
35
- currentComputation = computeFn;
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
- try {
39
- // Compute with tracking
40
- cachedValue = computeFn.call(trackingProxy);
41
- isDirty = false;
42
- } finally {
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
- // Create the computed chunk
51
- const computedChunk = chunk(computeValue());
40
+ const lastValues = dependencies.map(dep => dep.get());
52
41
 
53
- // Subscribe to all detected dependencies
54
- dependencies.forEach(dep => {
42
+ dependencies.forEach((dep, index) => {
55
43
  dep.subscribe(() => {
56
- isDirty = true;
57
- computedChunk.set(computeValue());
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
- get: () => {
54
+ isDirty: () => isDirty,
55
+ recompute: () => {
64
56
  if (isDirty) {
65
- return computeValue();
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, select } from './core/core';
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
+ });
@@ -1,4 +1,4 @@
1
- import { batch, chunk } from "../src/core/core";
1
+ import { chunk } from "../src/core/core";
2
2
  import { withHistory } from "../src/middleware/history";
3
3
 
4
4
 
@@ -1,4 +1,5 @@
1
- import { chunk, select } from '../src/core/core';
1
+ import { chunk } from '../src/core/core';
2
+ import { select } from '../src/core/selector'
2
3
 
3
4
  describe('select', () => {
4
5
  it('should create a selector that initially returns the correct value', () => {