stunk 0.2.0 → 0.6.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 +186 -6
- package/dist/core.js +96 -13
- package/dist/index.js +3 -2
- package/dist/middleware/logger.js +4 -0
- package/dist/middleware/validator.js +6 -0
- package/package.json +5 -3
- package/src/core.ts +121 -13
- package/src/index.ts +4 -3
- package/src/middleware/logger.ts +6 -0
- package/src/middleware/validator.ts +8 -0
- package/tests/batch-chunk.test.ts +108 -0
- package/tests/chunk.test.ts +168 -16
- package/tests/middleware.test.ts +37 -0
- package/types/stunk.d.ts +2 -1
package/README.md
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# Stunk
|
|
2
2
|
|
|
3
|
-
**Stunk** is a framework-agnostic state management library
|
|
3
|
+
**Stunk** is a **framework-agnostic** state management library that helps you manage your application's state in a clean and simple way. It uses a technique called **Atomic State**, breaking down state into smaller **chunks** that are easy to update, subscribe to, and manage.
|
|
4
|
+
|
|
5
|
+
---
|
|
4
6
|
|
|
5
7
|
## Pronunciation and Meaning
|
|
6
8
|
|
|
7
9
|
- **Pronunciation**: _Stunk_ (A playful blend of "state" and "chunk")
|
|
8
10
|
- **Meaning**: "Stunk" represents the combination of state management with chunk-based atomic units. The term captures the essence of atomic state management while using "chunk" to refer to these discrete units of state.
|
|
9
11
|
|
|
10
|
-
##
|
|
12
|
+
## What is Stunk?
|
|
13
|
+
|
|
14
|
+
Think of your application's state as a big jar of data. In traditional state management, you keep everything in one big jar, and every time you want to change something, you have to dig through the whole jar.
|
|
15
|
+
|
|
16
|
+
**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.
|
|
17
|
+
|
|
18
|
+
### Why Stunk
|
|
19
|
+
|
|
20
|
+
- lightweight, framework-agnostic state management.
|
|
21
|
+
- granular control over state updates and subscriptions.
|
|
22
|
+
- modular and composable state architecture.
|
|
11
23
|
|
|
12
|
-
|
|
13
|
-
- **Atomic State technique**: Implements state management using atomic units (chunks) that are discrete and easily manageable.
|
|
14
|
-
- **TypeScript support**: Fully typed for better developer experience and type safety.
|
|
15
|
-
- **Easy to use**: Simple API for creating, getting, and updating state.
|
|
24
|
+
---
|
|
16
25
|
|
|
17
26
|
## Installation
|
|
18
27
|
|
|
@@ -21,3 +30,174 @@ You can install **Stunk** from NPM:
|
|
|
21
30
|
```bash
|
|
22
31
|
npm install stunk
|
|
23
32
|
```
|
|
33
|
+
|
|
34
|
+
## 1 Features
|
|
35
|
+
|
|
36
|
+
### 1.0 **Chunks**
|
|
37
|
+
|
|
38
|
+
A **chunk** is a small container of state. It holds a value, and you can do three things with it:
|
|
39
|
+
|
|
40
|
+
- **Get** the current value of the chunk
|
|
41
|
+
- **Set** a new value for the chunk
|
|
42
|
+
- **Subscribe** to the chunk to get notified whenever the value changes
|
|
43
|
+
|
|
44
|
+
### Usage:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { chunk } from "stunk";
|
|
48
|
+
|
|
49
|
+
const count = chunk(0);
|
|
50
|
+
|
|
51
|
+
console.log(count.get()); // 0
|
|
52
|
+
|
|
53
|
+
count.set(5);
|
|
54
|
+
|
|
55
|
+
console.log(count.get()); // 5
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 1.1. **Subscription**
|
|
59
|
+
|
|
60
|
+
You can **subscribe** to a **chunk**. This means you get notified whenever the value inside the chunk changes. This is super useful for updating your app automatically when **state** changes.
|
|
61
|
+
|
|
62
|
+
### Usage
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const count = chunk(0);
|
|
66
|
+
const callback = (newValue: number) => console.log("Updated value:", newValue);
|
|
67
|
+
|
|
68
|
+
count.subscribe(callback);
|
|
69
|
+
|
|
70
|
+
count.set(10); // Will log: "Updated value: 10"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 1.2. **Deriving New Chunks**
|
|
74
|
+
|
|
75
|
+
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.
|
|
76
|
+
|
|
77
|
+
### Usage
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const count = chunk(5);
|
|
81
|
+
|
|
82
|
+
// Create a derived chunk that doubles the count
|
|
83
|
+
const doubleCount = count.derive((value) => value * 2);
|
|
84
|
+
|
|
85
|
+
count.subscribe((newValue) => console.log("Count:", newValue));
|
|
86
|
+
doubleCount.subscribe((newValue) => console.log("Double count:", newValue));
|
|
87
|
+
|
|
88
|
+
count.set(10);
|
|
89
|
+
// Will log:
|
|
90
|
+
// "Count: 10"
|
|
91
|
+
// "Double count: 20"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 1.3. **Unsubscribing**
|
|
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**
|
|
114
|
+
|
|
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
|
+
|
|
117
|
+
### Usage
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { chunk, batch } from "stunk";
|
|
121
|
+
|
|
122
|
+
const firstName = chunk("John");
|
|
123
|
+
const lastName = chunk("Doe");
|
|
124
|
+
const age = chunk(25);
|
|
125
|
+
|
|
126
|
+
// Subscribe to changes
|
|
127
|
+
firstName.subscribe((name) => console.log("First name changed:", name));
|
|
128
|
+
lastName.subscribe((name) => console.log("Last name changed:", name));
|
|
129
|
+
age.subscribe((age) => console.log("Age changed:", age));
|
|
130
|
+
|
|
131
|
+
// Without batch - triggers three separate updates
|
|
132
|
+
firstName.set("Jane"); // Logs immediately
|
|
133
|
+
lastName.set("Smith"); // Logs immediately
|
|
134
|
+
age.set(26); // Logs immediately
|
|
135
|
+
|
|
136
|
+
// With batch - triggers only one update per chunk at the end
|
|
137
|
+
batch(() => {
|
|
138
|
+
firstName.set("Jane"); // Doesn't log yet
|
|
139
|
+
lastName.set("Smith"); // Doesn't log yet
|
|
140
|
+
age.set(26); // Doesn't log yet
|
|
141
|
+
}); // All logs happen here at once
|
|
142
|
+
|
|
143
|
+
// Nested batches are also supported
|
|
144
|
+
batch(() => {
|
|
145
|
+
firstName.set("Jane");
|
|
146
|
+
batch(() => {
|
|
147
|
+
lastName.set("Smith");
|
|
148
|
+
age.set(26);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Looks intresting right? There's more!
|
|
154
|
+
|
|
155
|
+
Batching is particularly useful when:
|
|
156
|
+
|
|
157
|
+
- Updating multiple related pieces of state at once
|
|
158
|
+
- Performing form updates
|
|
159
|
+
- Handling complex state transitions
|
|
160
|
+
- Optimizing performance in data-heavy applications
|
|
161
|
+
|
|
162
|
+
The batch function ensures that:
|
|
163
|
+
|
|
164
|
+
- All updates within the batch are processed together
|
|
165
|
+
- Subscribers are notified only once with the final value
|
|
166
|
+
- Nested batches are handled correctly
|
|
167
|
+
- Updates are processed even if an error occurs (using try/finally)
|
|
168
|
+
|
|
169
|
+
### 1.5. **Middleware**
|
|
170
|
+
|
|
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
|
+
|
|
173
|
+
A middleware is a function with the following structure:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
export type Middleware<T> = (value: T, next: (newValue: T) => void) => void;
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- value: The value that is about to be set to the chunk.
|
|
180
|
+
- next(value): A function you must call with the processed (or unaltered) value to continue the chain of middleware and eventually update the chunk's state.
|
|
181
|
+
|
|
182
|
+
### Usage
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { chunk } from "stunk";
|
|
186
|
+
import { logger } from "stunk/middleware"; // native to stunk
|
|
187
|
+
import { nonNegativeValidator } from "stunk/middleware"; // native to stunk
|
|
188
|
+
// You can also create yours and pass it []
|
|
189
|
+
|
|
190
|
+
// Use middleware for logging and validation
|
|
191
|
+
const age = chunk(25, [logger, nonNegativeValidator]);
|
|
192
|
+
|
|
193
|
+
age.set(30); // Logs: "Setting value: 30"
|
|
194
|
+
age.set(-5); // Throws an error: "Value must be non-negative!"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## 2. **Atomic State Technique**
|
|
198
|
+
|
|
199
|
+
The **Atomic State** technique is all about breaking down your state into small, manageable chunks. This allows you to:
|
|
200
|
+
|
|
201
|
+
- Keep state changes focused and efficient
|
|
202
|
+
- Update only the parts of your app that need to change
|
|
203
|
+
- Easily manage and subscribe to state changes
|
package/dist/core.js
CHANGED
|
@@ -1,23 +1,106 @@
|
|
|
1
|
-
|
|
1
|
+
let batchDepth = 0;
|
|
2
|
+
const batchQueue = new Set();
|
|
3
|
+
export function batch(callback) {
|
|
4
|
+
batchDepth++;
|
|
5
|
+
try {
|
|
6
|
+
callback();
|
|
7
|
+
}
|
|
8
|
+
finally {
|
|
9
|
+
batchDepth--;
|
|
10
|
+
if (batchDepth === 0) {
|
|
11
|
+
// Execute all queued updates
|
|
12
|
+
batchQueue.forEach(update => update());
|
|
13
|
+
batchQueue.clear();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function chunk(initialValue, middleware = []) {
|
|
18
|
+
if (initialValue === undefined || initialValue === null) {
|
|
19
|
+
throw new Error("Initial value cannot be undefined or null.");
|
|
20
|
+
}
|
|
2
21
|
let value = initialValue;
|
|
3
|
-
const subscribers =
|
|
22
|
+
const subscribers = new Set();
|
|
23
|
+
let isDirty = false;
|
|
4
24
|
const get = () => value;
|
|
25
|
+
const notifySubscribers = () => {
|
|
26
|
+
if (batchDepth > 0) {
|
|
27
|
+
if (!isDirty) {
|
|
28
|
+
isDirty = true;
|
|
29
|
+
batchQueue.add(() => {
|
|
30
|
+
if (isDirty) {
|
|
31
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
32
|
+
isDirty = false;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
5
41
|
const set = (newValue) => {
|
|
6
|
-
if (newValue
|
|
7
|
-
|
|
8
|
-
|
|
42
|
+
if (newValue === null || newValue === undefined) {
|
|
43
|
+
throw new Error("Value cannot be null or undefined.");
|
|
44
|
+
}
|
|
45
|
+
let currentValue = newValue;
|
|
46
|
+
let index = 0;
|
|
47
|
+
while (index < middleware.length) {
|
|
48
|
+
const currentMiddleware = middleware[index];
|
|
49
|
+
let nextCalled = false;
|
|
50
|
+
let nextValue = null;
|
|
51
|
+
currentMiddleware(currentValue, (val) => {
|
|
52
|
+
nextCalled = true;
|
|
53
|
+
nextValue = val;
|
|
54
|
+
});
|
|
55
|
+
if (!nextCalled)
|
|
56
|
+
break;
|
|
57
|
+
if (nextValue === null || nextValue === undefined) {
|
|
58
|
+
throw new Error("Value cannot be null or undefined.");
|
|
59
|
+
}
|
|
60
|
+
currentValue = nextValue;
|
|
61
|
+
index++;
|
|
62
|
+
}
|
|
63
|
+
if (currentValue !== value) {
|
|
64
|
+
value = currentValue;
|
|
65
|
+
notifySubscribers();
|
|
9
66
|
}
|
|
10
67
|
};
|
|
11
68
|
const subscribe = (callback) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
69
|
+
if (typeof callback !== "function") {
|
|
70
|
+
throw new Error("Callback must be a function.");
|
|
71
|
+
}
|
|
72
|
+
if (subscribers.has(callback)) {
|
|
73
|
+
console.warn("Callback is already subscribed. This may lead to duplicate updates.");
|
|
74
|
+
}
|
|
75
|
+
subscribers.add(callback);
|
|
76
|
+
callback(value);
|
|
15
77
|
return () => {
|
|
16
|
-
|
|
17
|
-
if (index >= 0) {
|
|
18
|
-
subscribers.splice(index, 1);
|
|
19
|
-
}
|
|
78
|
+
subscribers.delete(callback);
|
|
20
79
|
};
|
|
21
80
|
};
|
|
22
|
-
|
|
81
|
+
const reset = () => {
|
|
82
|
+
value = initialValue;
|
|
83
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
84
|
+
};
|
|
85
|
+
const destroy = () => {
|
|
86
|
+
if (subscribers.size > 0) {
|
|
87
|
+
console.warn("Destroying chunk with active subscribers. This may lead to memory leaks.");
|
|
88
|
+
}
|
|
89
|
+
// Just clear subscribers without calling unsubscribe
|
|
90
|
+
subscribers.clear();
|
|
91
|
+
value = initialValue;
|
|
92
|
+
};
|
|
93
|
+
const derive = (fn) => {
|
|
94
|
+
if (typeof fn !== "function") {
|
|
95
|
+
throw new Error("Derive function must be a function.");
|
|
96
|
+
}
|
|
97
|
+
const derivedValue = fn(value);
|
|
98
|
+
const derivedChunk = chunk(derivedValue);
|
|
99
|
+
subscribe(() => {
|
|
100
|
+
const newDerivedValue = fn(value);
|
|
101
|
+
derivedChunk.set(newDerivedValue);
|
|
102
|
+
});
|
|
103
|
+
return derivedChunk;
|
|
104
|
+
};
|
|
105
|
+
return { get, set, subscribe, derive, reset, destroy };
|
|
23
106
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
1
|
+
export { chunk } from './core';
|
|
2
|
+
export { logger } from "./middleware/logger";
|
|
3
|
+
export { nonNegativeValidator } from "./middleware/validator";
|
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stunk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
-
"test": "jest",
|
|
9
|
+
"test": "jest --runInBand",
|
|
10
10
|
"prepare": "npm run build"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|
|
13
13
|
"state-management",
|
|
14
14
|
"atomic-state",
|
|
15
15
|
"framework-agnostic",
|
|
16
|
-
"
|
|
16
|
+
"stunk",
|
|
17
|
+
"state",
|
|
18
|
+
"chunk"
|
|
17
19
|
],
|
|
18
20
|
"author": "AbdulAzeez",
|
|
19
21
|
"license": "MIT",
|
package/src/core.ts
CHANGED
|
@@ -1,36 +1,144 @@
|
|
|
1
1
|
export type Subscriber<T> = (newValue: T) => void;
|
|
2
|
+
export type Middleware<T> = (value: T, next: (newValue: T) => void) => void;
|
|
2
3
|
|
|
3
4
|
export interface Chunk<T> {
|
|
5
|
+
/** Get the current value of the chunk. */
|
|
4
6
|
get: () => T;
|
|
7
|
+
/** Set a new value for the chunk. */
|
|
5
8
|
set: (value: T) => void;
|
|
9
|
+
/** Subscribe to changes in the chunk. Returns an unsubscribe function. */
|
|
6
10
|
subscribe: (callback: Subscriber<T>) => () => void;
|
|
11
|
+
/** Create a derived chunk based on this chunk's value. */
|
|
12
|
+
derive: <D>(fn: (value: T) => D) => Chunk<D>;
|
|
13
|
+
/** Reset the chunk to its initial value. */
|
|
14
|
+
reset: () => void;
|
|
15
|
+
/** Destroy the chunk and all its subscribers. */
|
|
16
|
+
destroy: () => void;
|
|
7
17
|
}
|
|
8
18
|
|
|
9
|
-
|
|
19
|
+
let batchDepth = 0;
|
|
20
|
+
const batchQueue = new Set<() => void>();
|
|
21
|
+
|
|
22
|
+
export function batch(callback: () => void) {
|
|
23
|
+
batchDepth++;
|
|
24
|
+
try {
|
|
25
|
+
callback();
|
|
26
|
+
} finally {
|
|
27
|
+
batchDepth--;
|
|
28
|
+
if (batchDepth === 0) {
|
|
29
|
+
// Execute all queued updates
|
|
30
|
+
batchQueue.forEach(update => update());
|
|
31
|
+
batchQueue.clear();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export function chunk<T>(initialValue: T, middleware: Middleware<T>[] = []): Chunk<T> {
|
|
38
|
+
if (initialValue === undefined || initialValue === null) {
|
|
39
|
+
throw new Error("Initial value cannot be undefined or null.");
|
|
40
|
+
}
|
|
41
|
+
|
|
10
42
|
let value = initialValue;
|
|
11
|
-
const subscribers
|
|
43
|
+
const subscribers = new Set<Subscriber<T>>();
|
|
44
|
+
let isDirty = false;
|
|
12
45
|
|
|
13
46
|
const get = () => value;
|
|
14
47
|
|
|
48
|
+
const notifySubscribers = () => {
|
|
49
|
+
if (batchDepth > 0) {
|
|
50
|
+
if (!isDirty) {
|
|
51
|
+
isDirty = true;
|
|
52
|
+
batchQueue.add(() => {
|
|
53
|
+
if (isDirty) {
|
|
54
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
55
|
+
isDirty = false;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
15
64
|
const set = (newValue: T) => {
|
|
16
|
-
if (newValue
|
|
17
|
-
|
|
18
|
-
|
|
65
|
+
if (newValue === null || newValue === undefined) {
|
|
66
|
+
throw new Error("Value cannot be null or undefined.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let currentValue = newValue;
|
|
70
|
+
let index = 0;
|
|
71
|
+
|
|
72
|
+
while (index < middleware.length) {
|
|
73
|
+
const currentMiddleware = middleware[index];
|
|
74
|
+
let nextCalled = false;
|
|
75
|
+
let nextValue: T | null = null;
|
|
76
|
+
|
|
77
|
+
currentMiddleware(currentValue, (val) => {
|
|
78
|
+
nextCalled = true;
|
|
79
|
+
nextValue = val;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!nextCalled) break;
|
|
83
|
+
|
|
84
|
+
if (nextValue === null || nextValue === undefined) {
|
|
85
|
+
throw new Error("Value cannot be null or undefined.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
currentValue = nextValue;
|
|
89
|
+
index++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (currentValue !== value) {
|
|
93
|
+
value = currentValue;
|
|
94
|
+
notifySubscribers();
|
|
19
95
|
}
|
|
20
96
|
};
|
|
21
97
|
|
|
22
98
|
const subscribe = (callback: Subscriber<T>) => {
|
|
23
|
-
|
|
24
|
-
|
|
99
|
+
if (typeof callback !== "function") {
|
|
100
|
+
throw new Error("Callback must be a function.");
|
|
101
|
+
}
|
|
102
|
+
if (subscribers.has(callback)) {
|
|
103
|
+
console.warn("Callback is already subscribed. This may lead to duplicate updates.");
|
|
104
|
+
}
|
|
105
|
+
subscribers.add(callback);
|
|
106
|
+
callback(value);
|
|
25
107
|
|
|
26
|
-
// Return unsubscribe function
|
|
27
108
|
return () => {
|
|
28
|
-
|
|
29
|
-
if (index >= 0) {
|
|
30
|
-
subscribers.splice(index, 1);
|
|
31
|
-
}
|
|
109
|
+
subscribers.delete(callback);
|
|
32
110
|
};
|
|
33
111
|
};
|
|
34
112
|
|
|
35
|
-
|
|
113
|
+
const reset = () => {
|
|
114
|
+
value = initialValue;
|
|
115
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const destroy = () => {
|
|
119
|
+
if (subscribers.size > 0) {
|
|
120
|
+
console.warn("Destroying chunk with active subscribers. This may lead to memory leaks.");
|
|
121
|
+
}
|
|
122
|
+
// Just clear subscribers without calling unsubscribe
|
|
123
|
+
subscribers.clear();
|
|
124
|
+
value = initialValue;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
const derive = <D>(fn: (value: T) => D) => {
|
|
129
|
+
if (typeof fn !== "function") {
|
|
130
|
+
throw new Error("Derive function must be a function.");
|
|
131
|
+
}
|
|
132
|
+
const derivedValue = fn(value);
|
|
133
|
+
const derivedChunk = chunk(derivedValue);
|
|
134
|
+
|
|
135
|
+
subscribe(() => {
|
|
136
|
+
const newDerivedValue = fn(value);
|
|
137
|
+
derivedChunk.set(newDerivedValue);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return derivedChunk;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return { get, set, subscribe, derive, reset, destroy };
|
|
36
144
|
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { chunk, batch } from '../src/core';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
describe('Chunk batch updates', () => {
|
|
5
|
+
it('should batch multiple updates into a single notification', () => {
|
|
6
|
+
const countChunk = chunk(0);
|
|
7
|
+
const callback = jest.fn();
|
|
8
|
+
|
|
9
|
+
countChunk.subscribe(callback);
|
|
10
|
+
callback.mockClear(); // Clear initial subscription call
|
|
11
|
+
|
|
12
|
+
batch(() => {
|
|
13
|
+
countChunk.set(1);
|
|
14
|
+
countChunk.set(2);
|
|
15
|
+
countChunk.set(3); // Should only notify once
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
19
|
+
expect(callback).toHaveBeenLastCalledWith(3);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should handle nested batch calls', () => {
|
|
23
|
+
const countChunk = chunk(0);
|
|
24
|
+
const callback = jest.fn();
|
|
25
|
+
|
|
26
|
+
countChunk.subscribe(callback);
|
|
27
|
+
callback.mockClear();
|
|
28
|
+
|
|
29
|
+
batch(() => {
|
|
30
|
+
countChunk.set(1); // Should not notify yet
|
|
31
|
+
batch(() => {
|
|
32
|
+
countChunk.set(2);
|
|
33
|
+
countChunk.set(3);
|
|
34
|
+
});
|
|
35
|
+
countChunk.set(4);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
39
|
+
expect(callback).toHaveBeenLastCalledWith(4);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle errors in batch without breaking state', () => {
|
|
43
|
+
const countChunk = chunk(0);
|
|
44
|
+
const callback = jest.fn();
|
|
45
|
+
|
|
46
|
+
countChunk.subscribe(callback);
|
|
47
|
+
callback.mockClear();
|
|
48
|
+
|
|
49
|
+
expect(() => {
|
|
50
|
+
batch(() => {
|
|
51
|
+
countChunk.set(1);
|
|
52
|
+
throw new Error('Test error');
|
|
53
|
+
// countChunk.set(2);
|
|
54
|
+
});
|
|
55
|
+
}).toThrow('Test error');
|
|
56
|
+
|
|
57
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(callback).toHaveBeenLastCalledWith(1);
|
|
59
|
+
expect(countChunk.get()).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should work with multiple chunks in the same batch', () => {
|
|
63
|
+
const chunk1 = chunk(0);
|
|
64
|
+
const chunk2 = chunk(0);
|
|
65
|
+
const callback1 = jest.fn();
|
|
66
|
+
const callback2 = jest.fn();
|
|
67
|
+
|
|
68
|
+
chunk1.subscribe(callback1);
|
|
69
|
+
chunk2.subscribe(callback2);
|
|
70
|
+
callback1.mockClear();
|
|
71
|
+
callback2.mockClear();
|
|
72
|
+
|
|
73
|
+
batch(() => {
|
|
74
|
+
chunk1.set(1);
|
|
75
|
+
chunk2.set(1);
|
|
76
|
+
chunk1.set(2);
|
|
77
|
+
chunk2.set(2);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(callback1).toHaveBeenLastCalledWith(2);
|
|
83
|
+
expect(callback2).toHaveBeenLastCalledWith(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle derived chunks in batch updates', () => {
|
|
87
|
+
const sourceChunk = chunk(0);
|
|
88
|
+
const derivedChunk = sourceChunk.derive(x => x * 2);
|
|
89
|
+
const sourceCallback = jest.fn();
|
|
90
|
+
const derivedCallback = jest.fn();
|
|
91
|
+
|
|
92
|
+
sourceChunk.subscribe(sourceCallback);
|
|
93
|
+
derivedChunk.subscribe(derivedCallback);
|
|
94
|
+
sourceCallback.mockClear();
|
|
95
|
+
derivedCallback.mockClear();
|
|
96
|
+
|
|
97
|
+
batch(() => {
|
|
98
|
+
sourceChunk.set(1);
|
|
99
|
+
sourceChunk.set(2);
|
|
100
|
+
sourceChunk.set(3);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(sourceCallback).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(derivedCallback).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(sourceCallback).toHaveBeenLastCalledWith(3);
|
|
106
|
+
expect(derivedCallback).toHaveBeenLastCalledWith(6);
|
|
107
|
+
});
|
|
108
|
+
});
|
package/tests/chunk.test.ts
CHANGED
|
@@ -1,35 +1,187 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { createChunk } from "../src/core";
|
|
1
|
+
import { chunk } from "../src/core";
|
|
4
2
|
|
|
5
3
|
test("Chunk should get and set values correctly", () => {
|
|
6
|
-
const
|
|
7
|
-
expect(
|
|
8
|
-
|
|
9
|
-
expect(
|
|
4
|
+
const chunky = chunk<number>(0);
|
|
5
|
+
expect(chunky.get()).toBe(0);
|
|
6
|
+
chunky.set(10);
|
|
7
|
+
expect(chunky.get()).toBe(10);
|
|
10
8
|
});
|
|
11
9
|
|
|
12
10
|
test("Chunk should notify subscribers on value change", () => {
|
|
13
|
-
const
|
|
11
|
+
const chunky = chunk<number>(0);
|
|
14
12
|
const callback = jest.fn();
|
|
15
|
-
|
|
13
|
+
const unsubscribe = chunky.subscribe(callback); // Store unsubscribe function
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
chunky.set(5);
|
|
18
16
|
expect(callback).toHaveBeenCalledWith(5);
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
chunky.set(10);
|
|
21
19
|
expect(callback).toHaveBeenCalledWith(10);
|
|
20
|
+
|
|
21
|
+
unsubscribe(); // Ensure cleanup after test
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
test("Chunk should notify multiple subscribers correctly", () => {
|
|
25
|
+
const chunky = chunk<number>(0);
|
|
26
|
+
const callback1 = jest.fn();
|
|
27
|
+
const callback2 = jest.fn();
|
|
28
|
+
|
|
29
|
+
const unsubscribe1 = chunky.subscribe(callback1);
|
|
30
|
+
const unsubscribe2 = chunky.subscribe(callback2);
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
chunky.set(10);
|
|
34
|
+
|
|
35
|
+
expect(callback1).toHaveBeenCalledWith(10);
|
|
36
|
+
expect(callback2).toHaveBeenCalledWith(10);
|
|
37
|
+
|
|
38
|
+
unsubscribe1();
|
|
39
|
+
unsubscribe2();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
|
|
24
43
|
test("Chunk should allow unsubscribing from updates", () => {
|
|
25
|
-
const
|
|
44
|
+
const chunky = chunk<number>(0);
|
|
26
45
|
const callback = jest.fn();
|
|
27
|
-
const unsubscribe =
|
|
46
|
+
const unsubscribe = chunky.subscribe(callback);
|
|
47
|
+
|
|
48
|
+
// Initial subscription call
|
|
49
|
+
expect(callback).toHaveBeenCalledWith(0);
|
|
50
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
28
51
|
|
|
29
|
-
|
|
52
|
+
chunky.set(5);
|
|
30
53
|
expect(callback).toHaveBeenCalledWith(5);
|
|
54
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
31
55
|
|
|
32
56
|
unsubscribe();
|
|
33
|
-
|
|
34
|
-
expect(callback).toHaveBeenCalledTimes(
|
|
57
|
+
chunky.set(10);
|
|
58
|
+
expect(callback).toHaveBeenCalledTimes(2); // Still called only twice
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("Chunk Derivation", () => {
|
|
62
|
+
it("should create a derived chunk and update it when the original chunk changes", () => {
|
|
63
|
+
const count = chunk(5);
|
|
64
|
+
const doubleCount = count.derive((value) => value * 2);
|
|
65
|
+
|
|
66
|
+
const countSpy = jest.fn();
|
|
67
|
+
const doubleCountSpy = jest.fn();
|
|
68
|
+
|
|
69
|
+
// Subscribe to both chunks
|
|
70
|
+
count.subscribe(countSpy);
|
|
71
|
+
doubleCount.subscribe(doubleCountSpy);
|
|
72
|
+
|
|
73
|
+
// Initial values
|
|
74
|
+
expect(count.get()).toBe(5);
|
|
75
|
+
expect(doubleCount.get()).toBe(10);
|
|
76
|
+
expect(countSpy).toHaveBeenCalledWith(5);
|
|
77
|
+
expect(doubleCountSpy).toHaveBeenCalledWith(10);
|
|
78
|
+
|
|
79
|
+
// Update count and verify updates
|
|
80
|
+
count.set(10);
|
|
81
|
+
expect(count.get()).toBe(10);
|
|
82
|
+
expect(doubleCount.get()).toBe(20);
|
|
83
|
+
expect(countSpy).toHaveBeenCalledWith(10);
|
|
84
|
+
expect(doubleCountSpy).toHaveBeenCalledWith(20);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should not update the derived chunk if the original chunk value does not change", () => {
|
|
88
|
+
const count = chunk(5);
|
|
89
|
+
const doubleCount = count.derive((value) => value * 2);
|
|
90
|
+
|
|
91
|
+
const doubleCountSpy = jest.fn();
|
|
92
|
+
|
|
93
|
+
// Subscribe to the derived chunk
|
|
94
|
+
doubleCount.subscribe(doubleCountSpy);
|
|
95
|
+
|
|
96
|
+
// Setting the same value
|
|
97
|
+
count.set(5);
|
|
98
|
+
expect(doubleCount.get()).toBe(10); // Derived value should remain the same
|
|
99
|
+
expect(doubleCountSpy).toHaveBeenCalledTimes(1); // Only initial value
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
test("Chunk should reset to initial value", () => {
|
|
105
|
+
const count = chunk(5);
|
|
106
|
+
count.set(10);
|
|
107
|
+
expect(count.get()).toBe(10);
|
|
108
|
+
count.reset();
|
|
109
|
+
expect(count.get()).toBe(5);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
describe('Chunk destroy', () => {
|
|
114
|
+
const countChunk = chunk(0);
|
|
115
|
+
const anotherChunk = chunk(0);
|
|
116
|
+
const countCallback = jest.fn();
|
|
117
|
+
const anotherCallback = jest.fn();
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
// Reset the mocks
|
|
121
|
+
countCallback.mockClear();
|
|
122
|
+
anotherCallback.mockClear();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should stop notifying subscribers after destroy is called', () => {
|
|
126
|
+
// Subscribe to the chunks
|
|
127
|
+
const countUnsubscribe = countChunk.subscribe(countCallback);
|
|
128
|
+
const anotherUnsubscribe = anotherChunk.subscribe(anotherCallback);
|
|
129
|
+
|
|
130
|
+
// Verify initial subscription calls
|
|
131
|
+
expect(countCallback).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(countCallback).toHaveBeenCalledWith(0);
|
|
133
|
+
expect(anotherCallback).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(anotherCallback).toHaveBeenCalledWith(0);
|
|
135
|
+
|
|
136
|
+
// Clear the mocks to start fresh
|
|
137
|
+
countCallback.mockClear();
|
|
138
|
+
anotherCallback.mockClear();
|
|
139
|
+
|
|
140
|
+
// Cleanup subscriptions before destroy
|
|
141
|
+
countUnsubscribe();
|
|
142
|
+
anotherUnsubscribe();
|
|
143
|
+
|
|
144
|
+
// Now destroy the chunks - no warning should appear
|
|
145
|
+
countChunk.destroy();
|
|
146
|
+
anotherChunk.destroy();
|
|
147
|
+
|
|
148
|
+
// Try setting new values after destruction
|
|
149
|
+
countChunk.set(30);
|
|
150
|
+
anotherChunk.set(40);
|
|
151
|
+
|
|
152
|
+
// Ensure that the subscribers were not notified after destroy
|
|
153
|
+
expect(countCallback).toHaveBeenCalledTimes(0);
|
|
154
|
+
expect(anotherCallback).toHaveBeenCalledTimes(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should reset to initial value after destroy', () => {
|
|
158
|
+
// Set some values
|
|
159
|
+
countChunk.set(10);
|
|
160
|
+
anotherChunk.set(20);
|
|
161
|
+
|
|
162
|
+
// Destroy the chunks (no subscribers at this point, so no warning)
|
|
163
|
+
countChunk.destroy();
|
|
164
|
+
anotherChunk.destroy();
|
|
165
|
+
|
|
166
|
+
// Subscribe new callbacks after destroy
|
|
167
|
+
const newCountCallback = jest.fn();
|
|
168
|
+
const newAnotherCallback = jest.fn();
|
|
169
|
+
|
|
170
|
+
const newCountUnsubscribe = countChunk.subscribe(newCountCallback);
|
|
171
|
+
const newAnotherUnsubscribe = anotherChunk.subscribe(newAnotherCallback);
|
|
172
|
+
|
|
173
|
+
// Should receive initial values
|
|
174
|
+
expect(newCountCallback).toHaveBeenCalledWith(0);
|
|
175
|
+
expect(newAnotherCallback).toHaveBeenCalledWith(0);
|
|
176
|
+
|
|
177
|
+
// Cleanup
|
|
178
|
+
newCountUnsubscribe();
|
|
179
|
+
newAnotherUnsubscribe();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Clean up after all tests
|
|
183
|
+
afterAll(() => {
|
|
184
|
+
countChunk.destroy();
|
|
185
|
+
anotherChunk.destroy();
|
|
186
|
+
});
|
|
35
187
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { chunk } from "../src/core";
|
|
2
|
+
import { logger } from "../src/middleware/logger";
|
|
3
|
+
import { nonNegativeValidator } from "../src/middleware/validator";
|
|
4
|
+
|
|
5
|
+
describe("Middleware Tests", () => {
|
|
6
|
+
let consoleSpy: jest.SpyInstance;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
consoleSpy = jest.spyOn(console, "log").mockImplementation();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
jest.restoreAllMocks(); // Restores all spies
|
|
14
|
+
jest.clearAllTimers(); // Clears any lingering timers
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("Logger middleware should log updates", () => {
|
|
18
|
+
const count = chunk(0, [logger]);
|
|
19
|
+
|
|
20
|
+
const unsubscribe = count.subscribe(() => { }); // Subscribe to capture updates
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
count.set(5); // Should log: "Setting value: 5"
|
|
24
|
+
expect(consoleSpy).toHaveBeenCalledWith("Setting value:", 5);
|
|
25
|
+
} finally {
|
|
26
|
+
unsubscribe(); // Ensure cleanup after test
|
|
27
|
+
consoleSpy.mockRestore();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("Non-negative validator middleware should prevent negative values", () => {
|
|
32
|
+
const count = chunk(0, [nonNegativeValidator]);
|
|
33
|
+
|
|
34
|
+
expect(() => count.set(-5)).toThrow("Value must be non-negative!");
|
|
35
|
+
expect(count.get()).toBe(0); // Value should remain unchanged
|
|
36
|
+
});
|
|
37
|
+
});
|
package/types/stunk.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ declare module 'stunk' {
|
|
|
5
5
|
get: () => T;
|
|
6
6
|
set: (value: T) => void;
|
|
7
7
|
subscribe: (callback: Subscriber<T>) => () => void;
|
|
8
|
+
derive?: <D>(fn: (value: T) => D) => Chunk<D>;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export function
|
|
11
|
+
export function chunk<T>(initialValue: T): Chunk<T>;
|
|
11
12
|
}
|