stunk 0.9.0 → 1.0.1
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 +5 -3
- package/dist/core/computed.js +63 -0
- package/dist/core/core.js +16 -26
- package/dist/index.js +3 -1
- package/dist/utils.js +34 -0
- package/package.json +1 -1
- package/src/core/computed.ts +75 -0
- package/src/core/core.ts +22 -32
- package/src/index.ts +4 -2
- package/src/utils.ts +43 -1
- package/tests/update.test.ts +70 -0
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
|
-
|
|
65
|
-
|
|
65
|
+
const processedValue = processMiddleware(newValue, middleware);
|
|
66
|
+
if (processedValue !== value) {
|
|
67
|
+
value = processedValue;
|
|
68
|
+
notifySubscribers();
|
|
66
69
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
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.
|
|
3
|
+
"version": "1.0.1",
|
|
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
|
|
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
|
-
|
|
93
|
+
const set = (newValue: T) => {
|
|
94
|
+
const processedValue = processMiddleware(newValue, middleware);
|
|
109
95
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
if (processedValue !== value) {
|
|
97
|
+
value = processedValue as T & {};
|
|
98
|
+
notifySubscribers();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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,6 @@
|
|
|
1
|
-
export { chunk } from './core/core';
|
|
2
|
-
export
|
|
1
|
+
export { chunk, batch, select } from './core/core';
|
|
2
|
+
export { asyncChunk } from './core/asyncChunk'
|
|
3
|
+
export { computed } from './core/computed'
|
|
4
|
+
export type { Chunk, Middleware } from './core/core';
|
|
3
5
|
|
|
4
6
|
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
|
+
});
|