stunk 1.4.8 → 2.1.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 +5 -496
- package/dist/core/asyncChunk.d.ts +4 -4
- package/dist/core/asyncChunk.js +2 -3
- package/dist/core/computed.js +36 -24
- package/dist/core/core.d.ts +2 -4
- package/dist/core/core.js +10 -12
- package/dist/core/selector.js +3 -17
- package/dist/core/types.d.ts +3 -3
- package/dist/middleware/history.js +13 -3
- package/dist/use-react/hooks/useAsyncChunk.d.ts +3 -3
- package/dist/use-react/hooks/useAsyncChunk.js +1 -1
- package/dist/use-react/hooks/useChunk.d.ts +1 -1
- package/dist/use-react/hooks/useChunk.js +3 -6
- package/package.json +26 -14
- package/jest.config.js +0 -4
- package/src/core/asyncChunk.ts +0 -83
- package/src/core/computed.ts +0 -65
- package/src/core/core.ts +0 -134
- package/src/core/selector.ts +0 -27
- package/src/core/types.ts +0 -17
- package/src/index.ts +0 -10
- package/src/middleware/history.ts +0 -104
- package/src/middleware/index.ts +0 -7
- package/src/middleware/logger.ts +0 -6
- package/src/middleware/persistence.ts +0 -45
- package/src/middleware/validator.ts +0 -8
- package/src/use-react/hooks/useAsyncChunk.ts +0 -38
- package/src/use-react/hooks/useChunk.ts +0 -47
- package/src/use-react/hooks/useChunkProperty.ts +0 -21
- package/src/use-react/hooks/useChunkValue.ts +0 -15
- package/src/use-react/hooks/useChunkValues.ts +0 -35
- package/src/use-react/hooks/useComputed.ts +0 -34
- package/src/use-react/hooks/useDerive.ts +0 -15
- package/src/use-react/index.ts +0 -9
- package/src/utils.ts +0 -103
- package/tests/async-chunk.test.ts +0 -215
- package/tests/batch-chunk.test.ts +0 -108
- package/tests/chunk.test.ts +0 -187
- package/tests/computed.test.ts +0 -93
- package/tests/history.test.ts +0 -99
- package/tests/middleware.test.ts +0 -37
- package/tests/persist.test.ts +0 -57
- package/tests/select-chunk.test.ts +0 -133
- package/tests/update.test.ts +0 -70
- package/tsconfig.json +0 -23
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Stunk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Stunk is a lightweight, framework-agnostic state management library built on atomic state principles. It simplifies state management by breaking state into manageable "chunks", ensuring efficient updates and reactivity.
|
|
4
4
|
|
|
5
5
|
- **Pronunciation**: _Stunk_ (A playful blend of "state" and "chunk")
|
|
6
6
|
|
|
@@ -28,507 +28,16 @@ yarn add stunk
|
|
|
28
28
|
pnpm install stunk
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Read Docs:
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
import { chunk } from "stunk";
|
|
37
|
-
|
|
38
|
-
// Create a simple counter
|
|
39
|
-
const counterChunk = chunk(0);
|
|
40
|
-
|
|
41
|
-
// Subscribe to changes
|
|
42
|
-
counterChunk.subscribe((value) => {
|
|
43
|
-
console.log("Counter changed:", value);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Update the value
|
|
47
|
-
counterChunk.set(1);
|
|
48
|
-
|
|
49
|
-
// Get current value
|
|
50
|
-
const value = counterChunk.get(); // 1
|
|
51
|
-
|
|
52
|
-
// Reset to initial value
|
|
53
|
-
counterChunk.reset();
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Deriving New Chunks
|
|
57
|
-
|
|
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.
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
const count = chunk(5);
|
|
63
|
-
|
|
64
|
-
// Create a derived chunk that doubles the count
|
|
65
|
-
const doubleCount = count.derive((value) => value * 2);
|
|
66
|
-
|
|
67
|
-
count.subscribe((newValue) => console.log("Count:", newValue));
|
|
68
|
-
doubleCount.subscribe((newValue) => console.log("Double count:", newValue));
|
|
69
|
-
|
|
70
|
-
count.set(10);
|
|
71
|
-
// Will log:
|
|
72
|
-
// "Count: 10"
|
|
73
|
-
// "Double count: 20"
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## State Selection
|
|
77
|
-
|
|
78
|
-
Efficiently access and react to specific state parts:
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
import { chunk, select } from "stunk";
|
|
82
|
-
|
|
83
|
-
const userChunk = chunk({
|
|
84
|
-
name: "Olamide",
|
|
85
|
-
age: 30,
|
|
86
|
-
email: "olamide@example.com",
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// Select specific properties -readonly
|
|
90
|
-
const nameChunk = select(userChunk, (state) => state.name);
|
|
91
|
-
const ageChunk = select(userChunk, (state) => state.age);
|
|
92
|
-
|
|
93
|
-
nameChunk.subscribe((name) => console.log("Name changed:", name));
|
|
94
|
-
// will only re-render if the selected part change.
|
|
95
|
-
|
|
96
|
-
nameChunk.set("Olamide"); // ❌ this will throw an error, because it is a readonly.
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Batch Updates
|
|
100
|
-
|
|
101
|
-
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.
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
import { chunk, batch } from "stunk";
|
|
105
|
-
|
|
106
|
-
const nameChunk = chunk("Olamide");
|
|
107
|
-
const ageChunk = chunk(30);
|
|
108
|
-
|
|
109
|
-
batch(() => {
|
|
110
|
-
nameChunk.set("AbdulAzeez");
|
|
111
|
-
ageChunk.set(31);
|
|
112
|
-
}); // Only one notification will be sent to subscribers
|
|
113
|
-
|
|
114
|
-
// Nested batches are also supported
|
|
115
|
-
batch(() => {
|
|
116
|
-
firstName.set("Olanrewaju");
|
|
117
|
-
batch(() => {
|
|
118
|
-
age.set(29);
|
|
119
|
-
});
|
|
120
|
-
}); // Only one notification will be sent to subscribers
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## Computed
|
|
124
|
-
|
|
125
|
-
Computed Chunks in Stunk allow you to create state derived from other chunks in a reactive way. Unlike derived chunks, computed chunks can depend on multiple sources, and they automatically recalculate when any of the source chunks change.
|
|
126
|
-
|
|
127
|
-
- Multiple Dependencies: Can depend on multiple chunks.
|
|
128
|
-
- Memoization: Only recalculates when dependencies change.
|
|
129
|
-
- Type-Safe: Fully typed in TypeScript for safe data handling.
|
|
130
|
-
- Reactive: Automatically updates subscribers when any dependency changes.
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
import { chunk, computed } from "stunk";
|
|
134
|
-
|
|
135
|
-
const firstNameChunk = chunk("John");
|
|
136
|
-
const lastNameChunk = chunk("Doe");
|
|
137
|
-
const ageChunk = chunk(30);
|
|
138
|
-
// Create a computed chunk that depends on multiple sources
|
|
139
|
-
|
|
140
|
-
const fullInfoChunk = computed(
|
|
141
|
-
[firstNameChunk, lastNameChunk, ageChunk],
|
|
142
|
-
(firstName, lastName, age) => ({
|
|
143
|
-
fullName: `${firstName} ${lastName}`,
|
|
144
|
-
isAdult: age >= 18,
|
|
145
|
-
})
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
firstNameChunk.set("Ola");
|
|
149
|
-
ageChunk.set(10);
|
|
150
|
-
|
|
151
|
-
console.log(fullInfoChunk.get());
|
|
152
|
-
// ✅ { fullName: "Ola Doe", isAdult: true }
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
`computed` chunks are ideal for scenarios where state depends on multiple sources or needs complex calculations. They ensure your application remains performant and maintainable.
|
|
156
|
-
|
|
157
|
-
## Advanced Examples
|
|
158
|
-
|
|
159
|
-
Form Validation Example
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
// With derive - single field validation
|
|
163
|
-
const emailChunk = chunk("user@example.com");
|
|
164
|
-
const isValidEmailChunk = emailChunk.derive((email) =>
|
|
165
|
-
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// With computed - full form validation
|
|
169
|
-
const usernameChunk = chunk("john");
|
|
170
|
-
const emailChunk = chunk("user@example.com");
|
|
171
|
-
const passwordChunk = chunk("pass123");
|
|
172
|
-
const confirmPasswordChunk = chunk("pass123");
|
|
173
|
-
|
|
174
|
-
const formValidationChunk = computed(
|
|
175
|
-
[usernameChunk, emailChunk, passwordChunk, confirmPasswordChunk],
|
|
176
|
-
(username, email, password, confirmPass) => ({
|
|
177
|
-
isUsernameValid: username.length >= 3,
|
|
178
|
-
isEmailValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
|
|
179
|
-
isPasswordValid: password.length >= 6,
|
|
180
|
-
doPasswordsMatch: password === confirmPass,
|
|
181
|
-
isFormValid:
|
|
182
|
-
username.length >= 3 &&
|
|
183
|
-
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) &&
|
|
184
|
-
password.length >= 6 &&
|
|
185
|
-
password === confirmPass,
|
|
186
|
-
})
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
console.log(formValidationChunk.get());
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
Data Filtering Example
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
// With derive - simple filter
|
|
196
|
-
const postsChunk = chunk([
|
|
197
|
-
{ id: 1, title: "Post 1", published: true },
|
|
198
|
-
{ id: 2, title: "Post 2", published: false },
|
|
199
|
-
]);
|
|
200
|
-
|
|
201
|
-
const publishedPostsChunk = postsChunk.derive((posts) =>
|
|
202
|
-
posts.filter((post) => post.published)
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
// With computed - complex filtering with multiple conditions
|
|
206
|
-
const postsChunk = chunk([
|
|
207
|
-
{ id: 1, title: "Post 1", category: "tech", date: "2024-01-01" },
|
|
208
|
-
]);
|
|
209
|
-
const categoryFilterChunk = chunk("tech");
|
|
210
|
-
const dateRangeChunk = chunk({ start: "2024-01-01", end: "2024-02-01" });
|
|
211
|
-
const searchTermChunk = chunk("");
|
|
212
|
-
|
|
213
|
-
const filteredPostsChunk = computed(
|
|
214
|
-
[postsChunk, categoryFilterChunk, dateRangeChunk, searchTermChunk],
|
|
215
|
-
(posts, category, dateRange, searchTerm) =>
|
|
216
|
-
posts.filter(
|
|
217
|
-
(post) =>
|
|
218
|
-
(!category || post.category === category) &&
|
|
219
|
-
(!dateRange ||
|
|
220
|
-
(post.date >= dateRange.start && post.date <= dateRange.end)) &&
|
|
221
|
-
(!searchTerm ||
|
|
222
|
-
post.title.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
223
|
-
)
|
|
224
|
-
);
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
## Middleware
|
|
228
|
-
|
|
229
|
-
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.
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
import { chunk } from "stunk";
|
|
233
|
-
import { logger, nonNegativeValidator } from "stunk/middleware";
|
|
234
|
-
|
|
235
|
-
// You can also create yours and pass it chunk as the second param
|
|
236
|
-
|
|
237
|
-
// Use middleware for logging and validation
|
|
238
|
-
const age = chunk(25, [logger, nonNegativeValidator]);
|
|
239
|
-
|
|
240
|
-
age.set(30); // Logs: "Setting value: 30"
|
|
241
|
-
age.set(-5); // ❌ Throws an error: "Value must be non-negative!"
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
## Time Travel (Middleware)
|
|
245
|
-
|
|
246
|
-
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.
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
import { chunk } from "stunk";
|
|
250
|
-
import { withHistory } from "stunk/midddleware";
|
|
251
|
-
|
|
252
|
-
const counterChunk = withHistory(chunk(0));
|
|
253
|
-
|
|
254
|
-
counterChunk.set(1);
|
|
255
|
-
counterChunk.set(2);
|
|
256
|
-
|
|
257
|
-
counterChunk.undo(); // Goes back to 1
|
|
258
|
-
counterChunk.undo(); // Goes back to 0
|
|
259
|
-
|
|
260
|
-
counterChunk.redo(); // Goes forward to 1
|
|
261
|
-
|
|
262
|
-
counterChunk.canUndo(); // Returns `true` if there is a previous state to revert to..
|
|
263
|
-
counterChunk.canRedo(); // Returns `true` if there is a next state to move to.
|
|
264
|
-
|
|
265
|
-
counterChunk.getHistory(); // Returns an array of all the values in the history.
|
|
266
|
-
|
|
267
|
-
counterChunk.clearHistory(); // Clears the history, keeping only the current value.
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
**Example: Limiting History Size (Optional)**
|
|
271
|
-
You can specify a max history size to prevent excessive memory usage.
|
|
272
|
-
|
|
273
|
-
```ts
|
|
274
|
-
const counter = withHistory(chunk(0), { maxHistory: 5 });
|
|
275
|
-
// Only keeps the last 5 changes -- default is 100.
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
This prevents the history from growing indefinitely and ensures efficient memory usage.
|
|
279
|
-
|
|
280
|
-
## State Persistence (Middleware)
|
|
281
|
-
|
|
282
|
-
Stunk provides a persistence middleware to automatically save state changes to storage (localStorage, sessionStorage, etc).
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
import { chunk } from "stunk";
|
|
286
|
-
import { withPersistence } from "stunk/middleware";
|
|
287
|
-
|
|
288
|
-
const counterChunk = withPersistence(chunk({ count: 0 }), {
|
|
289
|
-
key: "counter-state",
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// State automatically persists to localStorage
|
|
293
|
-
counterChunk.set({ count: 1 });
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
Using Different Storage
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
// Use sessionStorage instead of localStorage
|
|
300
|
-
const sessionStorageChunk = withPersistence(baseChunk, {
|
|
301
|
-
key: "counter",
|
|
302
|
-
storage: sessionStorage,
|
|
303
|
-
});
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
Custom Serialization
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
// Add custom serialization/deserialization
|
|
310
|
-
const encryptedChunk = withPersistence(baseChunk, {
|
|
311
|
-
key: "encrypted-data",
|
|
312
|
-
serialize: (value) => encrypt(JSON.stringify(value)),
|
|
313
|
-
deserialize: (value) => JSON.parse(decrypt(value)),
|
|
314
|
-
});
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
## Once
|
|
318
|
-
|
|
319
|
-
`Once` utility is a function that ensures a given piece of code or a function is executed only once, no matter how many times it's called. It's typically used to optimize performance by preventing redundant calculations or event handlers from running multiple times.
|
|
320
|
-
|
|
321
|
-
How It Works:
|
|
322
|
-
|
|
323
|
-
- It wraps a function and tracks whether it has been called.
|
|
324
|
-
- On the first call, it executes the function and saves the result.
|
|
325
|
-
- On subsequent calls, it simply returns the saved result without executing the function again.
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
const numbersChunk = chunk([1, 2, 3, 4, 5]);
|
|
329
|
-
|
|
330
|
-
const expensiveCalculation = once(() => {
|
|
331
|
-
console.log("Expensive calculation running...");
|
|
332
|
-
return numbersChunk.get().reduce((sum, num) => sum + num, 0);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// Derived chunk using the once utility
|
|
336
|
-
const totalChunk = numbersChunk.derive(() => expensiveCalculation());
|
|
337
|
-
|
|
338
|
-
totalChunk.subscribe((total) => {
|
|
339
|
-
console.log("Total:", total);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// Even if numbersChunk updates, the expensive calculation runs only once
|
|
343
|
-
numbersChunk.set([10, 20, 30, 40, 50]);
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
## Async State
|
|
347
|
-
|
|
348
|
-
Async Chunks in Stunk are designed to manage asynchronous state seamlessly. They handle loading, error, and data states automatically, making it easier to work with APIs and other asynchronous operations.
|
|
349
|
-
|
|
350
|
-
Key Features
|
|
351
|
-
|
|
352
|
-
- Built-in Loading and Error States: Automatically manages loading, error, and data properties.
|
|
353
|
-
|
|
354
|
-
- Type-Safe: Fully typed in TypeScript, ensuring safe data handling.
|
|
355
|
-
|
|
356
|
-
- Optimistic Updates: Update state optimistically and revert if needed.
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
import { asyncChunk } from "stunk";
|
|
360
|
-
|
|
361
|
-
type User = {
|
|
362
|
-
id: number;
|
|
363
|
-
name: string;
|
|
364
|
-
email: string;
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// Create an Async Chunk
|
|
368
|
-
const user = asyncChunk<User>(async () => {
|
|
369
|
-
const response = await fetch("/api/user");
|
|
370
|
-
return response.json(); // TypeScript expects this to return User;
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// Now userChunk is typed as AsyncChunk<User>, which means:
|
|
374
|
-
user.subscribe((state) => {
|
|
375
|
-
if (state.data) {
|
|
376
|
-
// state.data is typed as User | null
|
|
377
|
-
console.log(state.data.name); // TypeScript knows 'name' exists
|
|
378
|
-
console.log(state.data.age); // ❌ TypeScript Error: Property 'age' does not exist
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// Subscribe to state changes
|
|
383
|
-
user.subscribe(({ loading, error, data }) => {
|
|
384
|
-
if (loading) console.log("Loading...");
|
|
385
|
-
if (error) console.log("Error:", error);
|
|
386
|
-
if (data) console.log("User:", data);
|
|
387
|
-
});
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
**Reloading Data**
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
// Reload data
|
|
394
|
-
await user.reload();
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
**Optimistic Updates**
|
|
398
|
-
|
|
399
|
-
```typescript
|
|
400
|
-
|
|
401
|
-
// Optimistic update
|
|
402
|
-
user.mutate((currentData) => ({
|
|
403
|
-
...currentData,
|
|
404
|
-
name: "Fola",
|
|
405
|
-
}));
|
|
406
|
-
|
|
407
|
-
// The mutate function also enforces the User type
|
|
408
|
-
user.mutate(currentUser => ({
|
|
409
|
-
id: currentUser?.id ?? 0,
|
|
410
|
-
name: "Olamide",
|
|
411
|
-
email: "olamide@gmail.com"
|
|
412
|
-
age: 70 // ❌ TypeScript Error: Object literal may only specify known properties
|
|
413
|
-
}));
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
## Combine Async Chunk
|
|
417
|
-
|
|
418
|
-
`combineAsyncChunks` utility is used for managing multiple related async chunks.
|
|
419
|
-
|
|
420
|
-
- Maintains reactivity through the entire chain
|
|
421
|
-
- Preserves previous data during reloading
|
|
422
|
-
- Proper error propagation
|
|
423
|
-
|
|
424
|
-
```typescript
|
|
425
|
-
// Basic fetch
|
|
426
|
-
const userChunk = asyncChunk(async () => {
|
|
427
|
-
const response = await fetch("/api/user");
|
|
428
|
-
return response.json();
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// With options
|
|
432
|
-
const postsChunk = asyncChunk(
|
|
433
|
-
async () => {
|
|
434
|
-
const response = await fetch("/api/posts");
|
|
435
|
-
return response.json();
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
initialData: [],
|
|
439
|
-
retryCount: 3,
|
|
440
|
-
retryDelay: 2000,
|
|
441
|
-
onError: (error) => console.error("Failed to fetch posts:", error),
|
|
442
|
-
}
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
// Combining chunks
|
|
446
|
-
const profileChunk = combineAsyncChunks({
|
|
447
|
-
user: userChunk,
|
|
448
|
-
posts: postsChunk,
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
// Reactive updates
|
|
452
|
-
profileChunk.subscribe(({ loading, error, data }) => {
|
|
453
|
-
if (loading) {
|
|
454
|
-
showLoadingSpinner();
|
|
455
|
-
} else if (error) {
|
|
456
|
-
showError(error);
|
|
457
|
-
} else {
|
|
458
|
-
updateUI(data);
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
## API Reference
|
|
464
|
-
|
|
465
|
-
### Core
|
|
466
|
-
|
|
467
|
-
- `chunk<T>(initialValue: T): Chunk<T>`
|
|
468
|
-
- `batch(fn: () => void): void`
|
|
469
|
-
- `select<T, S>(sourceChunk: Chunk<T>, selector: (state: T) => S): Chunk<S>`
|
|
470
|
-
<!-- - `asyncChunk<T>(fetcher: () => Promise<T>, options?): AsyncChunk<T>` -->
|
|
471
|
-
|
|
472
|
-
### History
|
|
473
|
-
|
|
474
|
-
- `withHistory<T>(chunk: Chunk<T>, options: { maxHistory?: number }): ChunkWithHistory<T>`
|
|
475
|
-
|
|
476
|
-
### Persistance
|
|
477
|
-
|
|
478
|
-
- `withPersistence<T>(baseChunk: Chunk<T>,options: PersistOptions<T>): Chunk<T>`
|
|
479
|
-
|
|
480
|
-
### Types
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
interface Chunk<T> {
|
|
484
|
-
get(): T;
|
|
485
|
-
set(value: T): void;
|
|
486
|
-
subscribe(callback: (value: T) => void): () => void;
|
|
487
|
-
derive<D>(fn: (value: T) => D): Chunk<D>;
|
|
488
|
-
reset(): void;
|
|
489
|
-
destroy(): void;
|
|
490
|
-
}
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
interface AsyncState<T> {
|
|
495
|
-
loading: boolean;
|
|
496
|
-
error: Error | null;
|
|
497
|
-
data: T | null;
|
|
498
|
-
}
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
```typescript
|
|
502
|
-
interface AsyncChunk<T> extends Chunk<AsyncState<T>> {
|
|
503
|
-
reload(): Promise<void>;
|
|
504
|
-
mutate(mutator: (currentData: T | null) => T): void;
|
|
505
|
-
}
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
```typescript
|
|
509
|
-
interface ChunkWithHistory<T> extends Chunk<T> {
|
|
510
|
-
undo: () => void;
|
|
511
|
-
redo: () => void;
|
|
512
|
-
canUndo: () => boolean;
|
|
513
|
-
canRedo: () => boolean;
|
|
514
|
-
getHistory: () => T[];
|
|
515
|
-
clearHistory: () => void;
|
|
516
|
-
}
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
```typescript
|
|
520
|
-
interface PersistOptions<T> {
|
|
521
|
-
key: string; // Storage key
|
|
522
|
-
storage?: Storage; // Storage mechanism (default: localStorage)
|
|
523
|
-
serialize?: (value: T) => string; // Custom serializer
|
|
524
|
-
deserialize?: (value: string) => T; // Custom deserializer
|
|
525
|
-
}
|
|
526
|
-
```
|
|
33
|
+
[Stunk](https://stunk.vercel.app/)
|
|
527
34
|
|
|
528
35
|
## Contributing
|
|
529
36
|
|
|
530
37
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
531
38
|
|
|
39
|
+
[Pull Request](https://github.com/I-am-abdulazeez/stunk/pulls)
|
|
40
|
+
|
|
532
41
|
## License
|
|
533
42
|
|
|
534
43
|
This is licence under MIT
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Chunk } from "./core";
|
|
2
2
|
import { AsyncChunkOpt } from "./types";
|
|
3
|
-
export interface AsyncState<T> {
|
|
3
|
+
export interface AsyncState<T, E extends Error> {
|
|
4
4
|
loading: boolean;
|
|
5
|
-
error:
|
|
5
|
+
error: E | null;
|
|
6
6
|
data: T | null;
|
|
7
7
|
}
|
|
8
|
-
export interface AsyncChunk<T> extends Chunk<AsyncState<T>> {
|
|
8
|
+
export interface AsyncChunk<T, E extends Error = Error> extends Chunk<AsyncState<T, E>> {
|
|
9
9
|
/**
|
|
10
10
|
* Reload the data from the source.
|
|
11
11
|
*/
|
|
@@ -19,4 +19,4 @@ export interface AsyncChunk<T> extends Chunk<AsyncState<T>> {
|
|
|
19
19
|
*/
|
|
20
20
|
reset: () => void;
|
|
21
21
|
}
|
|
22
|
-
export declare function asyncChunk<T>(fetcher: () => Promise<T>, options?: AsyncChunkOpt<T>): AsyncChunk<T>;
|
|
22
|
+
export declare function asyncChunk<T, E extends Error = Error>(fetcher: () => Promise<T>, options?: AsyncChunkOpt<T, E>): AsyncChunk<T, E>;
|
package/dist/core/asyncChunk.js
CHANGED
|
@@ -18,10 +18,9 @@ export function asyncChunk(fetcher, options = {}) {
|
|
|
18
18
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
19
19
|
return fetchData(retries - 1);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
baseChunk.set({ loading: false, error: errorObj, data: baseChunk.get().data });
|
|
21
|
+
baseChunk.set({ loading: false, error: error, data: baseChunk.get().data });
|
|
23
22
|
if (onError) {
|
|
24
|
-
onError(
|
|
23
|
+
onError(error);
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
26
|
};
|
package/dist/core/computed.js
CHANGED
|
@@ -1,42 +1,54 @@
|
|
|
1
1
|
import { chunk } from "./core";
|
|
2
2
|
export function computed(dependencies, computeFn) {
|
|
3
|
-
|
|
4
|
-
let cachedValue = computeFn(...
|
|
3
|
+
const initialValues = dependencies.map(dep => dep.get());
|
|
4
|
+
let cachedValue = computeFn(...initialValues);
|
|
5
|
+
let isDirty = false;
|
|
6
|
+
let lastDependencyValues = [...initialValues];
|
|
7
|
+
const computedChunk = chunk(cachedValue);
|
|
8
|
+
const originalSet = computedChunk.set;
|
|
5
9
|
const recalculate = () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
if (!isDirty)
|
|
11
|
+
return;
|
|
12
|
+
const currentValues = dependencies.map(dep => dep.get());
|
|
13
|
+
const hasChanges = currentValues.some((val, i) => val !== lastDependencyValues[i]);
|
|
14
|
+
if (hasChanges) {
|
|
15
|
+
lastDependencyValues = [...currentValues];
|
|
16
|
+
const newValue = computeFn(...currentValues);
|
|
17
|
+
if (newValue !== cachedValue) {
|
|
18
|
+
cachedValue = newValue;
|
|
19
|
+
originalSet(newValue);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Always clear the dirty flag after recalculation
|
|
23
|
+
isDirty = false;
|
|
9
24
|
};
|
|
10
|
-
const computedChunk = chunk(cachedValue);
|
|
11
|
-
const originalGet = computedChunk.get;
|
|
12
25
|
computedChunk.get = () => {
|
|
13
26
|
if (isDirty) {
|
|
14
27
|
recalculate();
|
|
15
|
-
computedChunk.set(cachedValue); // Update the chunk value after recomputation
|
|
16
28
|
}
|
|
17
|
-
return cachedValue;
|
|
29
|
+
return cachedValue;
|
|
18
30
|
};
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
isDirty = true;
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
});
|
|
31
|
+
const unsub = dependencies.map(dep => dep.subscribe(() => {
|
|
32
|
+
if (!isDirty) {
|
|
33
|
+
isDirty = true;
|
|
34
|
+
recalculate();
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
29
37
|
return {
|
|
30
38
|
...computedChunk,
|
|
31
39
|
isDirty: () => isDirty,
|
|
32
40
|
recompute: () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
computedChunk.set(cachedValue); // Update the chunk value after manual recomputation
|
|
36
|
-
}
|
|
41
|
+
isDirty = true;
|
|
42
|
+
recalculate();
|
|
37
43
|
},
|
|
38
44
|
set: () => {
|
|
39
|
-
throw new Error('Cannot directly
|
|
45
|
+
throw new Error('Cannot set values directly on computed. Modify the source chunk instead.');
|
|
46
|
+
},
|
|
47
|
+
destroy: () => {
|
|
48
|
+
unsub.forEach(cleanup => cleanup());
|
|
49
|
+
if (computedChunk.destroy) {
|
|
50
|
+
computedChunk.destroy();
|
|
51
|
+
}
|
|
40
52
|
}
|
|
41
53
|
};
|
|
42
54
|
}
|
package/dist/core/core.d.ts
CHANGED
|
@@ -3,10 +3,8 @@ export type Middleware<T> = (value: T, next: (newValue: T) => void) => void;
|
|
|
3
3
|
export interface Chunk<T> {
|
|
4
4
|
/** Get the current value of the chunk. */
|
|
5
5
|
get: () => T;
|
|
6
|
-
/** Set a new value for the chunk. */
|
|
7
|
-
set: (
|
|
8
|
-
/** Update existing value efficiently */
|
|
9
|
-
update: (updater: (currentValue: T) => T) => void;
|
|
6
|
+
/** Set a new value for the chunk & Update existing value efficiently. */
|
|
7
|
+
set: (newValueOrUpdater: T | ((currentValue: T) => T)) => void;
|
|
10
8
|
/** Subscribe to changes in the chunk. Returns an unsubscribe function. */
|
|
11
9
|
subscribe: (callback: Subscriber<T>) => () => void;
|
|
12
10
|
/** Create a derived chunk based on this chunk's value. */
|
package/dist/core/core.js
CHANGED
|
@@ -39,19 +39,17 @@ export function chunk(initialValue, middleware = []) {
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
const get = () => value;
|
|
42
|
-
const set = (
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const set = (newValueOrUpdater) => {
|
|
43
|
+
let newValue;
|
|
44
|
+
if (typeof newValueOrUpdater === 'function') {
|
|
45
|
+
// Handle updater function
|
|
46
|
+
newValue = newValueOrUpdater(value);
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
throw new Error("Updater must be a function");
|
|
48
|
+
else {
|
|
49
|
+
// Handle direct value assignment
|
|
50
|
+
newValue = newValueOrUpdater;
|
|
52
51
|
}
|
|
53
|
-
const
|
|
54
|
-
const processedValue = processMiddleware(newValue);
|
|
52
|
+
const processedValue = processMiddleware(newValue, middleware);
|
|
55
53
|
if (processedValue !== value) {
|
|
56
54
|
value = processedValue;
|
|
57
55
|
notifySubscribers();
|
|
@@ -92,5 +90,5 @@ export function chunk(initialValue, middleware = []) {
|
|
|
92
90
|
});
|
|
93
91
|
return derivedChunk;
|
|
94
92
|
};
|
|
95
|
-
return { get, set,
|
|
93
|
+
return { get, set, subscribe, derive, reset, destroy };
|
|
96
94
|
}
|