stunk 0.3.0 → 0.7.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 +230 -6
- package/dist/core/core.js +128 -0
- package/dist/index.js +2 -2
- package/dist/middleware/history.js +58 -0
- package/dist/middleware/index.js +3 -0
- package/dist/middleware/logger.js +4 -0
- package/dist/middleware/validator.js +6 -0
- package/package.json +5 -3
- package/src/core/core.ts +170 -0
- package/src/index.ts +3 -3
- package/src/middleware/history.ts +86 -0
- package/src/middleware/index.ts +3 -0
- 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/history.test.ts +99 -0
- package/tests/middleware.test.ts +37 -0
- package/tests/select-chunk.test.ts +132 -0
- package/types/stunk.d.ts +2 -1
- package/dist/core.js +0 -23
- package/src/core.ts +0 -36
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.
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
18
|
+
### Why Stunk
|
|
19
|
+
|
|
20
|
+
- lightweight, framework-agnostic state management.
|
|
21
|
+
- granular control over state updates and subscriptions.
|
|
22
|
+
- modular and composable state architecture.
|
|
23
|
+
|
|
24
|
+
---
|
|
16
25
|
|
|
17
26
|
## Installation
|
|
18
27
|
|
|
@@ -21,3 +30,218 @@ 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.1.0. **Unsubscribing**
|
|
74
|
+
|
|
75
|
+
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? 😂
|
|
76
|
+
|
|
77
|
+
### Usage
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const count = chunk(0);
|
|
81
|
+
const callback = (newValue: number) => console.log("Updated value:", newValue);
|
|
82
|
+
|
|
83
|
+
const unsubscribe = count.subscribe(callback);
|
|
84
|
+
|
|
85
|
+
count.set(10); // Will log: "Updated value: 10"
|
|
86
|
+
|
|
87
|
+
unsubscribe(); // Unsubscribe
|
|
88
|
+
|
|
89
|
+
count.set(20); // Nothing will happen now, because you unsubscribed
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 1.2. **Deriving New Chunks**
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
### Usage
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const count = chunk(5);
|
|
100
|
+
|
|
101
|
+
// Create a derived chunk that doubles the count
|
|
102
|
+
const doubleCount = count.derive((value) => value * 2);
|
|
103
|
+
|
|
104
|
+
count.subscribe((newValue) => console.log("Count:", newValue));
|
|
105
|
+
doubleCount.subscribe((newValue) => console.log("Double count:", newValue));
|
|
106
|
+
|
|
107
|
+
count.set(10);
|
|
108
|
+
// Will log:
|
|
109
|
+
// "Count: 10"
|
|
110
|
+
// "Double count: 20"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 1.3. **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.4. **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
|
+
### 1.4.1. Middleware: Undo & Redo (withHistory)
|
|
198
|
+
|
|
199
|
+
The **withHistory** middleware extends a chunk to support undo and redo functionality. This allows you to navigate back and forth between previous **states**, making it useful for implementing features like **undo/redo**, form history, and state time travel.
|
|
200
|
+
|
|
201
|
+
### Usage
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { chunk } from "stunk";
|
|
205
|
+
import { withHistory } from "stunk/middleware"; // Import the history middleware
|
|
206
|
+
|
|
207
|
+
const counter = withHistory(chunk(0));
|
|
208
|
+
|
|
209
|
+
counter.set(10);
|
|
210
|
+
counter.set(20);
|
|
211
|
+
|
|
212
|
+
console.log(counter.get()); // 20
|
|
213
|
+
|
|
214
|
+
counter.undo(); // Go back one step
|
|
215
|
+
console.log(counter.get()); // 10
|
|
216
|
+
|
|
217
|
+
counter.redo(); // Go forward one step
|
|
218
|
+
console.log(counter.get()); // 20
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Available Methods**
|
|
222
|
+
|
|
223
|
+
| Method | Description |
|
|
224
|
+
| ---------------- | ----------------------------------------------------------- |
|
|
225
|
+
| `undo()` | Reverts to the previous state (if available). |
|
|
226
|
+
| `redo()` | Moves forward to the next state (if available). |
|
|
227
|
+
| `canUndo()` | Returns `true` if there are past states available. |
|
|
228
|
+
| `canRedo()` | Returns `true` if there are future states available. |
|
|
229
|
+
| `getHistory()` | Returns an `array` of all past states. |
|
|
230
|
+
| `clearHistory()` | Clears all stored history and keeps only the current state. |
|
|
231
|
+
|
|
232
|
+
**Example: Limiting History Size (Optional)**
|
|
233
|
+
You can specify a max history size to prevent excessive memory usage.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const counter = withHistory(chunk(0), { maxHistory: 5 }); // Only keeps the last 5 changes -- default is 100.
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This prevents the history from growing indefinitely and ensures efficient memory usage.
|
|
240
|
+
|
|
241
|
+
## 2. **Atomic State Technique**
|
|
242
|
+
|
|
243
|
+
The **Atomic State** technique is all about breaking down your state into small, manageable chunks. This allows you to:
|
|
244
|
+
|
|
245
|
+
- Keep state changes focused and efficient
|
|
246
|
+
- Update only the parts of your app that need to change
|
|
247
|
+
- Easily manage and subscribe to state changes
|
|
@@ -0,0 +1,128 @@
|
|
|
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 select(sourceChunk, selector) {
|
|
18
|
+
const initialValue = selector(sourceChunk.get());
|
|
19
|
+
const selectedChunk = chunk(initialValue);
|
|
20
|
+
let previousSelected = initialValue;
|
|
21
|
+
// Subscribe to source changes with equality checking
|
|
22
|
+
sourceChunk.subscribe((newValue) => {
|
|
23
|
+
const newSelected = selector(newValue);
|
|
24
|
+
// Only update if the selected value actually changed
|
|
25
|
+
if (!Object.is(newSelected, previousSelected)) {
|
|
26
|
+
previousSelected = newSelected;
|
|
27
|
+
selectedChunk.set(newSelected);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// Return read-only version of the chunk
|
|
31
|
+
return {
|
|
32
|
+
...selectedChunk,
|
|
33
|
+
// Prevent setting values directly on the selector
|
|
34
|
+
set: () => {
|
|
35
|
+
throw new Error('Cannot set values directly on a selector. Modify the source chunk instead.');
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function chunk(initialValue, middleware = []) {
|
|
40
|
+
if (initialValue === undefined || initialValue === null) {
|
|
41
|
+
throw new Error("Initial value cannot be undefined or null.");
|
|
42
|
+
}
|
|
43
|
+
let value = initialValue;
|
|
44
|
+
const subscribers = new Set();
|
|
45
|
+
let isDirty = false;
|
|
46
|
+
const get = () => value;
|
|
47
|
+
const notifySubscribers = () => {
|
|
48
|
+
if (batchDepth > 0) {
|
|
49
|
+
if (!isDirty) {
|
|
50
|
+
isDirty = true;
|
|
51
|
+
batchQueue.add(() => {
|
|
52
|
+
if (isDirty) {
|
|
53
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
54
|
+
isDirty = false;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const set = (newValue) => {
|
|
64
|
+
if (newValue === null || newValue === undefined) {
|
|
65
|
+
throw new Error("Value cannot be null or undefined.");
|
|
66
|
+
}
|
|
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++;
|
|
84
|
+
}
|
|
85
|
+
if (currentValue !== value) {
|
|
86
|
+
value = currentValue;
|
|
87
|
+
notifySubscribers();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const subscribe = (callback) => {
|
|
91
|
+
if (typeof callback !== "function") {
|
|
92
|
+
throw new Error("Callback must be a function.");
|
|
93
|
+
}
|
|
94
|
+
if (subscribers.has(callback)) {
|
|
95
|
+
console.warn("Callback is already subscribed. This may lead to duplicate updates.");
|
|
96
|
+
}
|
|
97
|
+
subscribers.add(callback);
|
|
98
|
+
callback(value);
|
|
99
|
+
return () => {
|
|
100
|
+
subscribers.delete(callback);
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const reset = () => {
|
|
104
|
+
value = initialValue;
|
|
105
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
106
|
+
};
|
|
107
|
+
const destroy = () => {
|
|
108
|
+
if (subscribers.size > 0) {
|
|
109
|
+
console.warn("Destroying chunk with active subscribers. This may lead to memory leaks.");
|
|
110
|
+
}
|
|
111
|
+
// Just clear subscribers without calling unsubscribe
|
|
112
|
+
subscribers.clear();
|
|
113
|
+
value = initialValue;
|
|
114
|
+
};
|
|
115
|
+
const derive = (fn) => {
|
|
116
|
+
if (typeof fn !== "function") {
|
|
117
|
+
throw new Error("Derive function must be a function.");
|
|
118
|
+
}
|
|
119
|
+
const derivedValue = fn(value);
|
|
120
|
+
const derivedChunk = chunk(derivedValue);
|
|
121
|
+
subscribe(() => {
|
|
122
|
+
const newDerivedValue = fn(value);
|
|
123
|
+
derivedChunk.set(newDerivedValue);
|
|
124
|
+
});
|
|
125
|
+
return derivedChunk;
|
|
126
|
+
};
|
|
127
|
+
return { get, set, subscribe, derive, reset, destroy };
|
|
128
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
export { chunk } from './core/core';
|
|
2
|
+
export * from "./middleware";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function withHistory(baseChunk, options = {}) {
|
|
2
|
+
const { maxHistory = 100 } = options;
|
|
3
|
+
const history = [baseChunk.get()];
|
|
4
|
+
let currentIndex = 0;
|
|
5
|
+
let isHistoryAction = false;
|
|
6
|
+
const historyChunk = {
|
|
7
|
+
...baseChunk,
|
|
8
|
+
set: (newValue) => {
|
|
9
|
+
if (isHistoryAction) {
|
|
10
|
+
baseChunk.set(newValue);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// Remove any future history when setting a new value
|
|
14
|
+
history.splice(currentIndex + 1);
|
|
15
|
+
history.push(newValue);
|
|
16
|
+
// Limit history size
|
|
17
|
+
if (history.length > maxHistory) {
|
|
18
|
+
console.warn("History limit reached. Removing oldest entries.");
|
|
19
|
+
const removeCount = history.length - maxHistory;
|
|
20
|
+
history.splice(0, removeCount);
|
|
21
|
+
currentIndex = Math.max(0, currentIndex - removeCount);
|
|
22
|
+
}
|
|
23
|
+
currentIndex = history.length - 1;
|
|
24
|
+
baseChunk.set(newValue);
|
|
25
|
+
},
|
|
26
|
+
undo: () => {
|
|
27
|
+
if (!historyChunk.canUndo())
|
|
28
|
+
return;
|
|
29
|
+
isHistoryAction = true;
|
|
30
|
+
currentIndex--;
|
|
31
|
+
historyChunk.set(history[currentIndex]);
|
|
32
|
+
isHistoryAction = false;
|
|
33
|
+
},
|
|
34
|
+
redo: () => {
|
|
35
|
+
if (!historyChunk.canRedo())
|
|
36
|
+
return;
|
|
37
|
+
isHistoryAction = true;
|
|
38
|
+
currentIndex++;
|
|
39
|
+
historyChunk.set(history[currentIndex]);
|
|
40
|
+
isHistoryAction = false;
|
|
41
|
+
},
|
|
42
|
+
canUndo: () => currentIndex > 0,
|
|
43
|
+
canRedo: () => currentIndex < history.length - 1,
|
|
44
|
+
getHistory: () => [...history],
|
|
45
|
+
clearHistory: () => {
|
|
46
|
+
const currentValue = baseChunk.get();
|
|
47
|
+
history.length = 0;
|
|
48
|
+
history.push(currentValue);
|
|
49
|
+
currentIndex = 0;
|
|
50
|
+
},
|
|
51
|
+
// Override destroy to clean up history
|
|
52
|
+
destroy: () => {
|
|
53
|
+
history.length = 0;
|
|
54
|
+
baseChunk.destroy();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return historyChunk;
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stunk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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/core.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export type Subscriber<T> = (newValue: T) => void;
|
|
2
|
+
export type Middleware<T> = (value: T, next: (newValue: T) => void) => void;
|
|
3
|
+
|
|
4
|
+
export interface Chunk<T> {
|
|
5
|
+
/** Get the current value of the chunk. */
|
|
6
|
+
get: () => T;
|
|
7
|
+
/** Set a new value for the chunk. */
|
|
8
|
+
set: (value: T) => void;
|
|
9
|
+
/** Subscribe to changes in the chunk. Returns an unsubscribe function. */
|
|
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;
|
|
17
|
+
}
|
|
18
|
+
|
|
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 select<T, S>(sourceChunk: Chunk<T>, selector: (value: T) => S): Chunk<S> {
|
|
38
|
+
const initialValue = selector(sourceChunk.get());
|
|
39
|
+
const selectedChunk = chunk(initialValue);
|
|
40
|
+
let previousSelected = initialValue;
|
|
41
|
+
|
|
42
|
+
// Subscribe to source changes with equality checking
|
|
43
|
+
sourceChunk.subscribe((newValue) => {
|
|
44
|
+
const newSelected = selector(newValue);
|
|
45
|
+
|
|
46
|
+
// Only update if the selected value actually changed
|
|
47
|
+
if (!Object.is(newSelected, previousSelected)) {
|
|
48
|
+
previousSelected = newSelected;
|
|
49
|
+
selectedChunk.set(newSelected);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Return read-only version of the chunk
|
|
54
|
+
return {
|
|
55
|
+
...selectedChunk,
|
|
56
|
+
// Prevent setting values directly on the selector
|
|
57
|
+
set: () => {
|
|
58
|
+
throw new Error('Cannot set values directly on a selector. Modify the source chunk instead.');
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function chunk<T>(initialValue: T, middleware: Middleware<T>[] = []): Chunk<T> {
|
|
64
|
+
if (initialValue === undefined || initialValue === null) {
|
|
65
|
+
throw new Error("Initial value cannot be undefined or null.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let value = initialValue;
|
|
69
|
+
const subscribers = new Set<Subscriber<T>>();
|
|
70
|
+
let isDirty = false;
|
|
71
|
+
|
|
72
|
+
const get = () => value;
|
|
73
|
+
|
|
74
|
+
const notifySubscribers = () => {
|
|
75
|
+
if (batchDepth > 0) {
|
|
76
|
+
if (!isDirty) {
|
|
77
|
+
isDirty = true;
|
|
78
|
+
batchQueue.add(() => {
|
|
79
|
+
if (isDirty) {
|
|
80
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
81
|
+
isDirty = false;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
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
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!nextCalled) break;
|
|
109
|
+
|
|
110
|
+
if (nextValue === null || nextValue === undefined) {
|
|
111
|
+
throw new Error("Value cannot be null or undefined.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
currentValue = nextValue;
|
|
115
|
+
index++;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (currentValue !== value) {
|
|
119
|
+
value = currentValue;
|
|
120
|
+
notifySubscribers();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const subscribe = (callback: Subscriber<T>) => {
|
|
125
|
+
if (typeof callback !== "function") {
|
|
126
|
+
throw new Error("Callback must be a function.");
|
|
127
|
+
}
|
|
128
|
+
if (subscribers.has(callback)) {
|
|
129
|
+
console.warn("Callback is already subscribed. This may lead to duplicate updates.");
|
|
130
|
+
}
|
|
131
|
+
subscribers.add(callback);
|
|
132
|
+
callback(value);
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
subscribers.delete(callback);
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const reset = () => {
|
|
140
|
+
value = initialValue;
|
|
141
|
+
subscribers.forEach((subscriber) => subscriber(value));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const destroy = () => {
|
|
145
|
+
if (subscribers.size > 0) {
|
|
146
|
+
console.warn("Destroying chunk with active subscribers. This may lead to memory leaks.");
|
|
147
|
+
}
|
|
148
|
+
// Just clear subscribers without calling unsubscribe
|
|
149
|
+
subscribers.clear();
|
|
150
|
+
value = initialValue;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
const derive = <D>(fn: (value: T) => D) => {
|
|
155
|
+
if (typeof fn !== "function") {
|
|
156
|
+
throw new Error("Derive function must be a function.");
|
|
157
|
+
}
|
|
158
|
+
const derivedValue = fn(value);
|
|
159
|
+
const derivedChunk = chunk(derivedValue);
|
|
160
|
+
|
|
161
|
+
subscribe(() => {
|
|
162
|
+
const newDerivedValue = fn(value);
|
|
163
|
+
derivedChunk.set(newDerivedValue);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return derivedChunk;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return { get, set, subscribe, derive, reset, destroy };
|
|
170
|
+
}
|
package/src/index.ts
CHANGED