stunk 0.8.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 +191 -127
- package/dist/core/asyncChunk.js +46 -1
- package/dist/core/computed.js +63 -0
- package/dist/core/core.js +16 -26
- package/dist/core/types.js +1 -0
- package/dist/index.js +2 -1
- package/dist/middleware/index.js +3 -0
- package/dist/middleware/persistence.js +25 -0
- package/dist/utils.js +64 -0
- package/package.json +8 -2
- package/src/core/asyncChunk.ts +83 -0
- package/src/core/computed.ts +75 -0
- package/src/core/core.ts +22 -32
- package/src/core/types.ts +17 -0
- package/src/index.ts +3 -2
- package/src/middleware/history.ts +18 -0
- package/src/middleware/index.ts +4 -0
- package/src/middleware/persistence.ts +45 -0
- package/src/utils.ts +88 -0
- package/tests/async-chunk.test.ts +215 -0
- package/tests/persist.test.ts +57 -0
- package/tests/update.test.ts +70 -0
package/README.md
CHANGED
|
@@ -1,92 +1,62 @@
|
|
|
1
1
|
# Stunk
|
|
2
2
|
|
|
3
|
-
A lightweight,
|
|
4
|
-
|
|
5
|
-
## Pronunciation and Meaning
|
|
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
|
|
|
7
5
|
- **Pronunciation**: _Stunk_ (A playful blend of "state" and "chunk")
|
|
8
|
-
- **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
|
-
|
|
10
|
-
## What is Stunk?
|
|
11
|
-
|
|
12
|
-
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.
|
|
13
6
|
|
|
14
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.
|
|
15
8
|
|
|
16
9
|
## Features
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
- 🎯
|
|
22
|
-
-
|
|
23
|
-
- 🔄
|
|
24
|
-
-
|
|
25
|
-
-
|
|
11
|
+
- 🚀 **Lightweight and Fast**: No dependencies, minimal overhead
|
|
12
|
+
- 🔄 **Reactive**: Automatic updates when state changes
|
|
13
|
+
- 📦 **Batch Updates**: Group multiple state updates together
|
|
14
|
+
- 🎯 **Atomic State Management**: Break down state into manageable chunks
|
|
15
|
+
- 🎭 **State Selection**: Select and derive specific parts of the state
|
|
16
|
+
- 🔄 **Async Support**: Handle async state with built-in loading and error states
|
|
17
|
+
- 🔌 **Middleware Support**: Extend functionality with custom middleware
|
|
18
|
+
- ⏱️ **Time Travel**: Undo/redo state changes
|
|
19
|
+
- 🔍 **Type-Safe**: Written in TypeScript with full type inference
|
|
26
20
|
|
|
27
21
|
## Installation
|
|
28
22
|
|
|
29
23
|
```bash
|
|
30
24
|
npm install stunk
|
|
25
|
+
# or
|
|
26
|
+
yarn add stunk
|
|
27
|
+
# or
|
|
28
|
+
pnpm install stunk
|
|
31
29
|
```
|
|
32
30
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
import { chunk, select, batch } from "stunk";
|
|
37
|
-
|
|
38
|
-
// Create a state chunk
|
|
39
|
-
const counter = chunk(0);
|
|
40
|
-
const userChunk = chunk({ name: "Olamide", age: 26 });
|
|
41
|
-
|
|
42
|
-
// Select specific state - Selector
|
|
43
|
-
const nameSelector = select(userChunk, (user) => user.name);
|
|
44
|
-
|
|
45
|
-
// Subscribe to changes
|
|
46
|
-
nameSelector.subscribe((name) => console.log("Name:", name));
|
|
47
|
-
counter.subscribe((count) => console.log("Counter", counter));
|
|
48
|
-
|
|
49
|
-
// Batch updates
|
|
50
|
-
batch(() => {
|
|
51
|
-
userChunk.set({ name: "Olalekan", age: 27 }); // Doesn't log yet
|
|
52
|
-
counter.set(5); // Doesn't log yet
|
|
53
|
-
}); // All logs happen here at once
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Core Concepts
|
|
57
|
-
|
|
58
|
-
### Chunks
|
|
31
|
+
## Basic Usage
|
|
59
32
|
|
|
60
|
-
|
|
33
|
+
A **chunk** is a small container of state. It holds a value, and you can do some stuffs with it:
|
|
61
34
|
|
|
62
35
|
```typescript
|
|
63
|
-
|
|
64
|
-
counter.subscribe((value) => console.log(value));
|
|
65
|
-
counter.set(5);
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Unsubscribing
|
|
69
|
-
|
|
70
|
-
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..**
|
|
36
|
+
import { chunk } from "stunk";
|
|
71
37
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
const count = chunk(0);
|
|
76
|
-
const callback = (newValue: number) => console.log("Updated value:", newValue);
|
|
38
|
+
// Create a simple counter
|
|
39
|
+
const counterChunk = chunk(0);
|
|
77
40
|
|
|
78
|
-
|
|
41
|
+
// Subscribe to changes
|
|
42
|
+
counterChunk.subscribe((value) => {
|
|
43
|
+
console.log("Counter changed:", value);
|
|
44
|
+
});
|
|
79
45
|
|
|
80
|
-
|
|
46
|
+
// Update the value
|
|
47
|
+
counterChunk.set(1);
|
|
81
48
|
|
|
82
|
-
|
|
49
|
+
// Get current value
|
|
50
|
+
const value = counterChunk.get(); // 1
|
|
83
51
|
|
|
84
|
-
|
|
52
|
+
// Reset to initial value
|
|
53
|
+
counterChunk.reset();
|
|
85
54
|
```
|
|
86
55
|
|
|
87
|
-
|
|
56
|
+
## Deriving New Chunks
|
|
88
57
|
|
|
89
|
-
With Stunk
|
|
58
|
+
With **Stunk**, you can create **derived chunks**. This means you can create a new **chunk** based on the value of another **chunk**.
|
|
59
|
+
When the original **chunk** changes, the **derived chunk** will automatically update.
|
|
90
60
|
|
|
91
61
|
```typescript
|
|
92
62
|
const count = chunk(5);
|
|
@@ -103,34 +73,51 @@ count.set(10);
|
|
|
103
73
|
// "Double count: 20"
|
|
104
74
|
```
|
|
105
75
|
|
|
106
|
-
|
|
76
|
+
## Batch Updates
|
|
107
77
|
|
|
108
|
-
|
|
78
|
+
Batch Update 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.
|
|
109
79
|
|
|
110
80
|
```typescript
|
|
81
|
+
import { chunk, batch } from "stunk";
|
|
82
|
+
|
|
83
|
+
const nameChunk = chunk("Olamide");
|
|
84
|
+
const ageChunk = chunk(30);
|
|
85
|
+
|
|
111
86
|
batch(() => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}); //
|
|
87
|
+
nameChunk.set("AbdulAzeez");
|
|
88
|
+
ageChunk.set(31);
|
|
89
|
+
}); // Only one notification will be sent to subscribers
|
|
115
90
|
|
|
116
91
|
// Nested batches are also supported
|
|
117
92
|
batch(() => {
|
|
118
|
-
|
|
93
|
+
firstName.set("Olanrewaju");
|
|
119
94
|
batch(() => {
|
|
120
|
-
|
|
95
|
+
age.set(29);
|
|
121
96
|
});
|
|
122
|
-
});
|
|
97
|
+
}); // Only one notification will be sent to subscribers
|
|
123
98
|
```
|
|
124
99
|
|
|
125
|
-
|
|
100
|
+
## State Selection
|
|
126
101
|
|
|
127
102
|
Efficiently access and react to specific state parts:
|
|
128
103
|
|
|
129
104
|
```typescript
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
105
|
+
import { chunk, select } from "stunk";
|
|
106
|
+
|
|
107
|
+
const userChunk = chunk({
|
|
108
|
+
name: "Olamide",
|
|
109
|
+
age: 30,
|
|
110
|
+
email: "olamide@example.com",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Select specific properties -readonly
|
|
114
|
+
const nameChunk = select(userChunk, (state) => state.name);
|
|
115
|
+
const ageChunk = select(userChunk, (state) => state.age);
|
|
116
|
+
|
|
117
|
+
nameChunk.subscribe((name) => console.log("Name changed:", name));
|
|
118
|
+
// will only re-render if the selected part change.
|
|
119
|
+
|
|
120
|
+
nameChunk.set("Olamide"); // ❌ this will throw an error, because it is a readonly.
|
|
134
121
|
```
|
|
135
122
|
|
|
136
123
|
## Middleware
|
|
@@ -138,46 +125,135 @@ const scoreSelector = select(userChunk, (u) => u.score);
|
|
|
138
125
|
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.
|
|
139
126
|
|
|
140
127
|
```typescript
|
|
141
|
-
|
|
128
|
+
import { chunk } from "stunk";
|
|
129
|
+
import { logger, nonNegativeValidator } from "stunk/middleware";
|
|
130
|
+
|
|
131
|
+
// You can also create yours and pass it chunk as the second param
|
|
142
132
|
|
|
143
133
|
// Use middleware for logging and validation
|
|
144
134
|
const age = chunk(25, [logger, nonNegativeValidator]);
|
|
145
135
|
|
|
146
136
|
age.set(30); // Logs: "Setting value: 30"
|
|
147
|
-
age.set(-5); // Throws an error: "Value must be non-negative!"
|
|
137
|
+
age.set(-5); // ❌ Throws an error: "Value must be non-negative!"
|
|
148
138
|
```
|
|
149
139
|
|
|
150
|
-
|
|
140
|
+
## Time Travel (Middleware)
|
|
151
141
|
|
|
152
142
|
```typescript
|
|
153
|
-
|
|
143
|
+
import { chunk } from "stunk";
|
|
144
|
+
import { withHistory } from "stunk/midddleware";
|
|
145
|
+
|
|
146
|
+
const counterChunk = withHistory(chunk(0));
|
|
147
|
+
|
|
148
|
+
counterChunk.set(1);
|
|
149
|
+
counterChunk.set(2);
|
|
154
150
|
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
counterChunk.undo(); // Goes back to 1
|
|
152
|
+
counterChunk.undo(); // Goes back to 0
|
|
157
153
|
|
|
158
|
-
|
|
154
|
+
counterChunk.redo(); // Goes forward to 1
|
|
159
155
|
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
counterChunk.canUndo(); // Returns `true` if there is a previous state to revert to..
|
|
157
|
+
counterChunk.canRedo(); // Returns `true` if there is a next state to move to.
|
|
162
158
|
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
counterChunk.getHistory(); // Returns an array of all the values in the history.
|
|
160
|
+
|
|
161
|
+
counterChunk.clearHistory(); // Clears the history, keeping only the current value.
|
|
165
162
|
```
|
|
166
163
|
|
|
167
164
|
**Example: Limiting History Size (Optional)**
|
|
168
165
|
You can specify a max history size to prevent excessive memory usage.
|
|
169
166
|
|
|
170
167
|
```ts
|
|
171
|
-
const counter = withHistory(chunk(0), { maxHistory: 5 });
|
|
168
|
+
const counter = withHistory(chunk(0), { maxHistory: 5 });
|
|
169
|
+
// Only keeps the last 5 changes -- default is 100.
|
|
172
170
|
```
|
|
173
171
|
|
|
174
172
|
This prevents the history from growing indefinitely and ensures efficient memory usage.
|
|
175
173
|
|
|
174
|
+
## State Persistence
|
|
175
|
+
|
|
176
|
+
Stunk provides a persistence middleware to automatically save state changes to storage (localStorage, sessionStorage, etc).
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { chunk } from "stunk";
|
|
180
|
+
import { withPersistence } from "stunk/middleware";
|
|
181
|
+
|
|
182
|
+
const counterChunk = withPersistence(chunk({ count: 0 }), {
|
|
183
|
+
key: "counter-state",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// State automatically persists to localStorage
|
|
187
|
+
counterChunk.set({ count: 1 });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Async State
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { asyncChunk } from "stunk";
|
|
194
|
+
|
|
195
|
+
type User = {
|
|
196
|
+
id: number;
|
|
197
|
+
name: string;
|
|
198
|
+
email: string;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const user = asyncChunk<User>(async () => {
|
|
202
|
+
const response = await fetch("/api/user");
|
|
203
|
+
return response.json(); // TypeScript expects this to return User;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Now userChunk is typed as AsyncChunk<User>, which means:
|
|
207
|
+
user.subscribe((state) => {
|
|
208
|
+
if (state.data) {
|
|
209
|
+
// state.data is typed as User | null
|
|
210
|
+
console.log(state.data.name); // TypeScript knows 'name' exists
|
|
211
|
+
console.log(state.data.age); // ❌ TypeScript Error: Property 'age' does not exist
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
user.subscribe(({ loading, error, data }) => {
|
|
216
|
+
if (loading) console.log("Loading...");
|
|
217
|
+
if (error) console.log("Error:", error);
|
|
218
|
+
if (data) console.log("User:", data);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Reload data
|
|
222
|
+
await user.reload();
|
|
223
|
+
|
|
224
|
+
// Optimistic update
|
|
225
|
+
user.mutate((currentData) => ({
|
|
226
|
+
...currentData,
|
|
227
|
+
name: "Fola",
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
// The mutate function also enforces the User type
|
|
231
|
+
user.mutate(currentUser => ({
|
|
232
|
+
id: currentUser?.id ?? 0,
|
|
233
|
+
name: "Olamide",
|
|
234
|
+
email: "olamide@gmail.com"
|
|
235
|
+
age: 70 // ❌ TypeScript Error: Object literal may only specify known properties
|
|
236
|
+
}));
|
|
237
|
+
```
|
|
238
|
+
|
|
176
239
|
## API Reference
|
|
177
240
|
|
|
178
|
-
###
|
|
241
|
+
### Core
|
|
242
|
+
|
|
243
|
+
- `chunk<T>(initialValue: T): Chunk<T>`
|
|
244
|
+
- `batch(fn: () => void): void`
|
|
245
|
+
- `select<T, S>(sourceChunk: Chunk<T>, selector: (state: T) => S): Chunk<S>`
|
|
246
|
+
<!-- - `asyncChunk<T>(fetcher: () => Promise<T>, options?): AsyncChunk<T>` -->
|
|
247
|
+
|
|
248
|
+
### History
|
|
249
|
+
|
|
250
|
+
- `withHistory<T>(chunk: Chunk<T>, options: { maxHistory?: number }): ChunkWithHistory<T>`
|
|
251
|
+
|
|
252
|
+
### Persistance
|
|
253
|
+
|
|
254
|
+
- `withPersistence<T>(baseChunk: Chunk<T>,options: PersistOptions<T>): Chunk<T>`
|
|
179
255
|
|
|
180
|
-
|
|
256
|
+
### Types
|
|
181
257
|
|
|
182
258
|
```typescript
|
|
183
259
|
interface Chunk<T> {
|
|
@@ -185,62 +261,50 @@ interface Chunk<T> {
|
|
|
185
261
|
set(value: T): void;
|
|
186
262
|
subscribe(callback: (value: T) => void): () => void;
|
|
187
263
|
derive<D>(fn: (value: T) => D): Chunk<D>;
|
|
264
|
+
reset(): void;
|
|
188
265
|
destroy(): void;
|
|
189
266
|
}
|
|
190
267
|
```
|
|
191
268
|
|
|
192
|
-
### `select<T, S>(sourceChunk: Chunk<T>, selector: (value: T) => S)`
|
|
193
|
-
|
|
194
|
-
Creates an optimized selector.
|
|
195
|
-
|
|
196
269
|
```typescript
|
|
197
|
-
|
|
198
|
-
|
|
270
|
+
interface AsyncState<T> {
|
|
271
|
+
loading: boolean;
|
|
272
|
+
error: Error | null;
|
|
273
|
+
data: T | null;
|
|
274
|
+
}
|
|
199
275
|
```
|
|
200
276
|
|
|
201
|
-
### `batch(callback: () => void)`
|
|
202
|
-
|
|
203
|
-
Batches multiple updates.
|
|
204
|
-
|
|
205
277
|
```typescript
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
batch(() => {
|
|
211
|
-
// Multiple updates here
|
|
212
|
-
batch(() => {
|
|
213
|
-
// Nested upddates here
|
|
214
|
-
});
|
|
215
|
-
});
|
|
278
|
+
interface AsyncChunk<T> extends Chunk<AsyncState<T>> {
|
|
279
|
+
reload(): Promise<void>;
|
|
280
|
+
mutate(mutator: (currentData: T | null) => T): void;
|
|
281
|
+
}
|
|
216
282
|
```
|
|
217
283
|
|
|
218
|
-
### `withHistory<T>(chunk: Chunk<T>, options?: { maxHistory?: number })`
|
|
219
|
-
|
|
220
|
-
Adds undo/redo capabilities.
|
|
221
|
-
|
|
222
284
|
```typescript
|
|
223
285
|
interface ChunkWithHistory<T> extends Chunk<T> {
|
|
224
|
-
undo()
|
|
225
|
-
redo()
|
|
226
|
-
canUndo()
|
|
227
|
-
canRedo()
|
|
228
|
-
getHistory()
|
|
229
|
-
clearHistory()
|
|
286
|
+
undo: () => void;
|
|
287
|
+
redo: () => void;
|
|
288
|
+
canUndo: () => boolean;
|
|
289
|
+
canRedo: () => boolean;
|
|
290
|
+
getHistory: () => T[];
|
|
291
|
+
clearHistory: () => void;
|
|
230
292
|
}
|
|
231
293
|
```
|
|
232
294
|
|
|
233
|
-
### `Middleware<T>`
|
|
234
|
-
|
|
235
|
-
Custom state processing:
|
|
236
|
-
|
|
237
295
|
```typescript
|
|
238
|
-
|
|
296
|
+
interface PersistOptions<T> {
|
|
297
|
+
key: string; // Storage key
|
|
298
|
+
storage?: Storage; // Storage mechanism (default: localStorage)
|
|
299
|
+
serialize?: (value: T) => string; // Custom serializer
|
|
300
|
+
deserialize?: (value: string) => T; // Custom deserializer
|
|
301
|
+
}
|
|
239
302
|
```
|
|
240
303
|
|
|
241
|
-
|
|
242
|
-
|
|
304
|
+
## Contributing
|
|
305
|
+
|
|
306
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
243
307
|
|
|
244
308
|
## License
|
|
245
309
|
|
|
246
|
-
MIT
|
|
310
|
+
This is licence under MIT
|
package/dist/core/asyncChunk.js
CHANGED
|
@@ -1 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
import { chunk } from "./core";
|
|
2
|
+
export function asyncChunk(fetcher, options = {}) {
|
|
3
|
+
const { initialData = null, onError, retryCount = 0, retryDelay = 1000, } = options;
|
|
4
|
+
const initialState = {
|
|
5
|
+
loading: true,
|
|
6
|
+
error: null,
|
|
7
|
+
data: initialData,
|
|
8
|
+
};
|
|
9
|
+
const baseChunk = chunk(initialState);
|
|
10
|
+
const fetchData = async (retries = retryCount) => {
|
|
11
|
+
baseChunk.set({ ...baseChunk.get(), loading: true, error: null });
|
|
12
|
+
try {
|
|
13
|
+
const data = await fetcher();
|
|
14
|
+
baseChunk.set({ loading: false, error: null, data });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (retries > 0) {
|
|
18
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
19
|
+
return fetchData(retries - 1);
|
|
20
|
+
}
|
|
21
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
22
|
+
baseChunk.set({ loading: false, error: errorObj, data: baseChunk.get().data });
|
|
23
|
+
if (onError) {
|
|
24
|
+
onError(errorObj);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
// Initial fetch
|
|
29
|
+
fetchData();
|
|
30
|
+
const asyncChunkInstance = {
|
|
31
|
+
...baseChunk,
|
|
32
|
+
reload: async () => {
|
|
33
|
+
await fetchData();
|
|
34
|
+
},
|
|
35
|
+
mutate: (mutator) => {
|
|
36
|
+
const currentState = baseChunk.get();
|
|
37
|
+
const newData = mutator(currentState.data);
|
|
38
|
+
baseChunk.set({ ...currentState, data: newData });
|
|
39
|
+
},
|
|
40
|
+
reset: () => {
|
|
41
|
+
baseChunk.set(initialState);
|
|
42
|
+
fetchData();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return asyncChunkInstance;
|
|
46
|
+
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
CHANGED
package/dist/middleware/index.js
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function withPersistence(baseChunk, options) {
|
|
2
|
+
const { key, storage = localStorage, serialize = JSON.stringify, deserialize = JSON.parse, } = options;
|
|
3
|
+
// Try to load initial state from storage
|
|
4
|
+
try {
|
|
5
|
+
const savedChunk = storage.getItem(key);
|
|
6
|
+
if (savedChunk) {
|
|
7
|
+
const parsed = deserialize(savedChunk);
|
|
8
|
+
baseChunk.set(parsed);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.error('Failed to load persisted state:', error);
|
|
13
|
+
}
|
|
14
|
+
// Save to storage
|
|
15
|
+
baseChunk.subscribe((newValue) => {
|
|
16
|
+
try {
|
|
17
|
+
const serialized = serialize(newValue);
|
|
18
|
+
storage.setItem(key, serialized);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.log('Failed to persist chunk', error);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return baseChunk;
|
|
25
|
+
}
|