stunk 0.9.0 → 1.0.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 CHANGED
@@ -1,9 +1,9 @@
1
1
  # Stunk
2
2
 
3
- - **Pronunciation**: _Stunk_ (A playful blend of "state" and "chunk")
4
-
5
3
  A lightweight, reactive state management library for TypeScript/JavaScript applications. Stunk combines atomic state management with powerful features like middleware, time travel, and async state handling.
6
4
 
5
+ - **Pronunciation**: _Stunk_ (A playful blend of "state" and "chunk")
6
+
7
7
  **Stunk** is like dividing your jar into many smaller containers, each holding a single piece of state. These smaller containers are called **chunks**. Each **chunk** can be updated and accessed easily, and any part of your app can subscribe to changes in a chunk so it gets updated automatically.
8
8
 
9
9
  ## Features
@@ -12,7 +12,7 @@ A lightweight, reactive state management library for TypeScript/JavaScript appli
12
12
  - 🔄 **Reactive**: Automatic updates when state changes
13
13
  - 📦 **Batch Updates**: Group multiple state updates together
14
14
  - 🎯 **Atomic State Management**: Break down state into manageable chunks
15
- - 🎭 **State Selection**: Select and derive specific parts of state
15
+ - 🎭 **State Selection**: Select and derive specific parts of the state
16
16
  - 🔄 **Async Support**: Handle async state with built-in loading and error states
17
17
  - 🔌 **Middleware Support**: Extend functionality with custom middleware
18
18
  - ⏱️ **Time Travel**: Undo/redo state changes
@@ -24,6 +24,8 @@ A lightweight, reactive state management library for TypeScript/JavaScript appli
24
24
  npm install stunk
25
25
  # or
26
26
  yarn add stunk
27
+ # or
28
+ pnpm install stunk
27
29
  ```
28
30
 
29
31
  ## Basic Usage
@@ -0,0 +1,63 @@
1
+ import { isChunk } from "../utils";
2
+ 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;
38
+ }
39
+ return cachedValue;
40
+ };
41
+ // Create the computed chunk
42
+ const computedChunk = chunk(computeValue());
43
+ // Subscribe to all detected dependencies
44
+ dependencies.forEach(dep => {
45
+ dep.subscribe(() => {
46
+ isDirty = true;
47
+ computedChunk.set(computeValue());
48
+ });
49
+ });
50
+ return {
51
+ ...computedChunk,
52
+ get: () => {
53
+ if (isDirty) {
54
+ return computeValue();
55
+ }
56
+ return cachedValue;
57
+ },
58
+ // Prevent direct setting
59
+ set: () => {
60
+ throw new Error('Cannot directly set a computed value');
61
+ }
62
+ };
63
+ }
package/dist/core/core.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { processMiddleware } from "../utils";
1
2
  let batchDepth = 0;
2
3
  const batchQueue = new Set();
3
4
  export function batch(callback) {
@@ -43,7 +44,6 @@ export function chunk(initialValue, middleware = []) {
43
44
  let value = initialValue;
44
45
  const subscribers = new Set();
45
46
  let isDirty = false;
46
- const get = () => value;
47
47
  const notifySubscribers = () => {
48
48
  if (batchDepth > 0) {
49
49
  if (!isDirty) {
@@ -60,30 +60,22 @@ export function chunk(initialValue, middleware = []) {
60
60
  subscribers.forEach((subscriber) => subscriber(value));
61
61
  }
62
62
  };
63
+ const get = () => value;
63
64
  const set = (newValue) => {
64
- if (newValue === null || newValue === undefined) {
65
- throw new Error("Value cannot be null or undefined.");
65
+ const processedValue = processMiddleware(newValue, middleware);
66
+ if (processedValue !== value) {
67
+ value = processedValue;
68
+ notifySubscribers();
66
69
  }
67
- let currentValue = newValue;
68
- let index = 0;
69
- while (index < middleware.length) {
70
- const currentMiddleware = middleware[index];
71
- let nextCalled = false;
72
- let nextValue = null;
73
- currentMiddleware(currentValue, (val) => {
74
- nextCalled = true;
75
- nextValue = val;
76
- });
77
- if (!nextCalled)
78
- break;
79
- if (nextValue === null || nextValue === undefined) {
80
- throw new Error("Value cannot be null or undefined.");
81
- }
82
- currentValue = nextValue;
83
- index++;
70
+ };
71
+ const update = (updater) => {
72
+ if (typeof updater !== 'function') {
73
+ throw new Error("Updater must be a function");
84
74
  }
85
- if (currentValue !== value) {
86
- value = currentValue;
75
+ const newValue = updater(value);
76
+ const processedValue = processMiddleware(newValue);
77
+ if (processedValue !== value) {
78
+ value = processedValue;
87
79
  notifySubscribers();
88
80
  }
89
81
  };
@@ -96,9 +88,7 @@ export function chunk(initialValue, middleware = []) {
96
88
  }
97
89
  subscribers.add(callback);
98
90
  callback(value);
99
- return () => {
100
- subscribers.delete(callback);
101
- };
91
+ return () => subscribers.delete(callback);
102
92
  };
103
93
  const reset = () => {
104
94
  value = initialValue;
@@ -124,5 +114,5 @@ export function chunk(initialValue, middleware = []) {
124
114
  });
125
115
  return derivedChunk;
126
116
  };
127
- return { get, set, subscribe, derive, reset, destroy };
117
+ return { get, set, update, subscribe, derive, reset, destroy };
128
118
  }
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- export { chunk } from './core/core';
1
+ export { chunk, batch, select } from './core/core';
2
+ export { asyncChunk } from './core/asyncChunk';
2
3
  export * from "./middleware";
package/dist/utils.js CHANGED
@@ -2,6 +2,16 @@ import { chunk } from "./core/core";
2
2
  export function isValidChunkValue(value) {
3
3
  return value !== null && value !== undefined;
4
4
  }
5
+ export function isChunk(value) {
6
+ return value &&
7
+ typeof value.get === 'function' &&
8
+ typeof value.set === 'function' &&
9
+ typeof value.update === 'function' &&
10
+ typeof value.subscribe === 'function' &&
11
+ typeof value.derive === 'function' &&
12
+ typeof value.reset === 'function' &&
13
+ typeof value.destroy === 'function';
14
+ }
5
15
  export function combineAsyncChunks(chunks) {
6
16
  // Create initial state with proper typing
7
17
  const initialData = Object.keys(chunks).reduce((acc, key) => {
@@ -31,3 +41,27 @@ export function combineAsyncChunks(chunks) {
31
41
  });
32
42
  return combined;
33
43
  }
44
+ export function processMiddleware(initialValue, middleware = []) {
45
+ if (initialValue === null || initialValue === undefined) {
46
+ throw new Error("Value cannot be null or undefined.");
47
+ }
48
+ let currentValue = initialValue;
49
+ let index = 0;
50
+ while (index < middleware.length) {
51
+ const currentMiddleware = middleware[index];
52
+ let nextCalled = false;
53
+ let nextValue = null;
54
+ currentMiddleware(currentValue, (val) => {
55
+ nextCalled = true;
56
+ nextValue = val;
57
+ });
58
+ if (!nextCalled)
59
+ break;
60
+ if (nextValue === null || nextValue === undefined) {
61
+ throw new Error("Value cannot be null or undefined.");
62
+ }
63
+ currentValue = nextValue;
64
+ index++;
65
+ }
66
+ return currentValue;
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stunk",
3
- "version": "0.9.0",
3
+ "version": "1.0.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",
@@ -0,0 +1,75 @@
1
+ import { isChunk } from "../utils";
2
+ import { chunk, Chunk } from "./core";
3
+
4
+ export function computed<T>(computeFn: () => T): Chunk<T> {
5
+ // Track the currently executing computed function
6
+ let currentComputation: (() => T) | null = null;
7
+
8
+ // Set to track dependencies
9
+ const dependencies = new Set<Chunk<any>>();
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
30
+
31
+ // Reset dependencies
32
+ dependencies.clear();
33
+
34
+ // Set the current computation context
35
+ currentComputation = computeFn;
36
+
37
+
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;
45
+ }
46
+ return cachedValue;
47
+
48
+ }
49
+
50
+ // Create the computed chunk
51
+ const computedChunk = chunk(computeValue());
52
+
53
+ // Subscribe to all detected dependencies
54
+ dependencies.forEach(dep => {
55
+ dep.subscribe(() => {
56
+ isDirty = true;
57
+ computedChunk.set(computeValue());
58
+ });
59
+ });
60
+
61
+ return {
62
+ ...computedChunk,
63
+ get: () => {
64
+ if (isDirty) {
65
+ return computeValue();
66
+ }
67
+ return cachedValue;
68
+ },
69
+ // Prevent direct setting
70
+ set: () => {
71
+ throw new Error('Cannot directly set a computed value');
72
+ }
73
+ };
74
+
75
+ }
package/src/core/core.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { processMiddleware } from "../utils";
2
+
1
3
  export type Subscriber<T> = (newValue: T) => void;
2
4
  export type Middleware<T> = (value: T, next: (newValue: T) => void) => void;
3
5
 
@@ -6,6 +8,8 @@ export interface Chunk<T> {
6
8
  get: () => T;
7
9
  /** Set a new value for the chunk. */
8
10
  set: (value: T) => void;
11
+ /** Update existing value efficiently */
12
+ update: (updater: (currentValue: T) => T) => void;
9
13
  /** Subscribe to changes in the chunk. Returns an unsubscribe function. */
10
14
  subscribe: (callback: Subscriber<T>) => () => void;
11
15
  /** Create a derived chunk based on this chunk's value. */
@@ -33,7 +37,6 @@ export function batch(callback: () => void) {
33
37
  }
34
38
  }
35
39
 
36
-
37
40
  export function select<T, S>(sourceChunk: Chunk<T>, selector: (value: T) => S): Chunk<S> {
38
41
  const initialValue = selector(sourceChunk.get());
39
42
  const selectedChunk = chunk(initialValue);
@@ -69,8 +72,6 @@ export function chunk<T>(initialValue: T, middleware: Middleware<T>[] = []): Chu
69
72
  const subscribers = new Set<Subscriber<T>>();
70
73
  let isDirty = false;
71
74
 
72
- const get = () => value;
73
-
74
75
  const notifySubscribers = () => {
75
76
  if (batchDepth > 0) {
76
77
  if (!isDirty) {
@@ -87,36 +88,27 @@ export function chunk<T>(initialValue: T, middleware: Middleware<T>[] = []): Chu
87
88
  }
88
89
  }
89
90
 
90
- const set = (newValue: T) => {
91
- if (newValue === null || newValue === undefined) {
92
- throw new Error("Value cannot be null or undefined.");
93
- }
94
-
95
- let currentValue = newValue;
96
- let index = 0;
97
-
98
- while (index < middleware.length) {
99
- const currentMiddleware = middleware[index];
100
- let nextCalled = false;
101
- let nextValue: T | null = null;
102
-
103
- currentMiddleware(currentValue, (val) => {
104
- nextCalled = true;
105
- nextValue = val;
106
- });
91
+ const get = () => value;
107
92
 
108
- if (!nextCalled) break;
93
+ const set = (newValue: T) => {
94
+ const processedValue = processMiddleware(newValue, middleware);
109
95
 
110
- if (nextValue === null || nextValue === undefined) {
111
- throw new Error("Value cannot be null or undefined.");
112
- }
96
+ if (processedValue !== value) {
97
+ value = processedValue as T & {};
98
+ notifySubscribers();
99
+ }
100
+ };
113
101
 
114
- currentValue = nextValue;
115
- index++;
102
+ const update = (updater: (currentValue: T) => T) => {
103
+ if (typeof updater !== 'function') {
104
+ throw new Error("Updater must be a function");
116
105
  }
117
106
 
118
- if (currentValue !== value) {
119
- value = currentValue;
107
+ const newValue = updater(value);
108
+ const processedValue = processMiddleware(newValue);
109
+
110
+ if (processedValue !== value) {
111
+ value = processedValue as T & {};
120
112
  notifySubscribers();
121
113
  }
122
114
  };
@@ -131,9 +123,7 @@ export function chunk<T>(initialValue: T, middleware: Middleware<T>[] = []): Chu
131
123
  subscribers.add(callback);
132
124
  callback(value);
133
125
 
134
- return () => {
135
- subscribers.delete(callback);
136
- };
126
+ return () => subscribers.delete(callback);
137
127
  };
138
128
 
139
129
  const reset = () => {
@@ -166,5 +156,5 @@ export function chunk<T>(initialValue: T, middleware: Middleware<T>[] = []): Chu
166
156
  return derivedChunk;
167
157
  };
168
158
 
169
- return { get, set, subscribe, derive, reset, destroy };
159
+ return { get, set, update, subscribe, derive, reset, destroy };
170
160
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { chunk } from './core/core';
2
- export type { Chunk } from './core/core';
1
+ export { chunk, batch, select } from './core/core';
2
+ export { asyncChunk } from './core/asyncChunk'
3
+ export type { Chunk, Middleware } from './core/core';
3
4
 
4
5
  export * from "./middleware";
package/src/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { chunk, Chunk } from "./core/core";
1
+ import { chunk, Chunk, Middleware } from "./core/core";
2
2
 
3
3
  import { AsyncChunk } from "./core/asyncChunk";
4
4
  import { CombinedData, CombinedState, InferAsyncData } from "./core/types";
@@ -7,6 +7,17 @@ export function isValidChunkValue(value: any): boolean {
7
7
  return value !== null && value !== undefined;
8
8
  }
9
9
 
10
+ export function isChunk<T>(value: any): value is Chunk<T> {
11
+ return value &&
12
+ typeof value.get === 'function' &&
13
+ typeof value.set === 'function' &&
14
+ typeof value.update === 'function' &&
15
+ typeof value.subscribe === 'function' &&
16
+ typeof value.derive === 'function' &&
17
+ typeof value.reset === 'function' &&
18
+ typeof value.destroy === 'function';
19
+ }
20
+
10
21
  export function combineAsyncChunks<T extends Record<string, AsyncChunk<any>>>(
11
22
  chunks: T
12
23
  ): Chunk<{
@@ -47,3 +58,34 @@ export function combineAsyncChunks<T extends Record<string, AsyncChunk<any>>>(
47
58
 
48
59
  return combined;
49
60
  }
61
+
62
+ export function processMiddleware<T>(initialValue: T, middleware: Middleware<T>[] = []): T {
63
+ if (initialValue === null || initialValue === undefined) {
64
+ throw new Error("Value cannot be null or undefined.");
65
+ }
66
+
67
+ let currentValue = initialValue;
68
+ let index = 0;
69
+
70
+ while (index < middleware.length) {
71
+ const currentMiddleware = middleware[index];
72
+ let nextCalled = false;
73
+ let nextValue: T | null = null;
74
+
75
+ currentMiddleware(currentValue, (val) => {
76
+ nextCalled = true;
77
+ nextValue = val;
78
+ });
79
+
80
+ if (!nextCalled) break;
81
+
82
+ if (nextValue === null || nextValue === undefined) {
83
+ throw new Error("Value cannot be null or undefined.");
84
+ }
85
+
86
+ currentValue = nextValue;
87
+ index++;
88
+ }
89
+
90
+ return currentValue;
91
+ }
@@ -0,0 +1,70 @@
1
+ import { chunk } from '../src/core/core';
2
+
3
+ describe('chunk update', () => {
4
+ it('should update value using updater function', () => {
5
+ const store = chunk(5);
6
+ store.update(value => value + 1);
7
+ expect(store.get()).toBe(6);
8
+ });
9
+
10
+ it('should throw error if updater is not a function', () => {
11
+ const store = chunk(5);
12
+ // @ts-expect-error Testing invalid input
13
+ expect(() => store.update('not a function')).toThrow('Updater must be a function');
14
+ });
15
+
16
+ it('should throw error if updater returns null or undefined', () => {
17
+ const store = chunk(5);
18
+ // @ts-expect-error Testing invalid input
19
+ expect(() => store.update(() => null)).toThrow('Value cannot be null or undefined.');
20
+ // @ts-expect-error Testing invalid input
21
+ expect(() => store.update(() => undefined)).toThrow('Value cannot be null or undefined.');
22
+ });
23
+
24
+ it('should notify subscribers only if value changes', () => {
25
+ const store = chunk(5);
26
+ const subscriber = jest.fn();
27
+ store.subscribe(subscriber);
28
+
29
+ // Reset the mock to ignore initial subscription call
30
+ subscriber.mockReset();
31
+
32
+ // Update to same value
33
+ store.update(value => value);
34
+ expect(subscriber).not.toHaveBeenCalled();
35
+
36
+ // Update to new value
37
+ store.update(value => value + 1);
38
+ expect(subscriber).toHaveBeenCalledWith(6);
39
+ expect(subscriber).toHaveBeenCalledTimes(1);
40
+ });
41
+
42
+ it('should handle complex update logic', () => {
43
+ const store = chunk(5);
44
+ store.update(value => {
45
+ if (value > 3) {
46
+ return value * 2;
47
+ }
48
+ return value + 1;
49
+ });
50
+ expect(store.get()).toBe(10);
51
+ });
52
+
53
+ it('should maintain type safety', () => {
54
+ interface User {
55
+ name: string;
56
+ age: number;
57
+ }
58
+
59
+ const store = chunk<User>({ name: 'John', age: 30 });
60
+
61
+ store.update(user => ({
62
+ ...user,
63
+ age: user.age + 1
64
+ }));
65
+
66
+ const user = store.get();
67
+ expect(user.age).toBe(31);
68
+ expect(user.name).toBe('John');
69
+ });
70
+ });