stunk 0.6.0 → 0.8.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 CHANGED
@@ -1,8 +1,6 @@
1
1
  # Stunk
2
2
 
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
- ---
3
+ A lightweight, framework-agnostic state management library using atomic state principles. Stunk breaks down state into manageable "chunks" for easy updates and subscriptions.
6
4
 
7
5
  ## Pronunciation and Meaning
8
6
 
@@ -15,49 +13,61 @@ Think of your application's state as a big jar of data. In traditional state man
15
13
 
16
14
  **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
15
 
18
- ### Why Stunk
16
+ ## Features
19
17
 
20
- - lightweight, framework-agnostic state management.
21
- - granular control over state updates and subscriptions.
22
- - modular and composable state architecture.
18
+ <!-- - 🎯 Framework agnostic -->
23
19
 
24
- ---
20
+ - 🔄 Reactive updates with efficient subscription system
21
+ - 🎯 Granular state selection
22
+ - ⏳ Built-in undo/redo/getHistory/clearHistory
23
+ - 🔄 Batch updates for performance and nested batch updates
24
+ - 🛠️ Extensible middleware
25
+ - 🔍 Full TypeScript support
25
26
 
26
27
  ## Installation
27
28
 
28
- You can install **Stunk** from NPM:
29
-
30
29
  ```bash
31
30
  npm install stunk
32
31
  ```
33
32
 
34
- ## 1 Features
33
+ ## Quick Start
35
34
 
36
- ### 1.0 **Chunks**
35
+ ```typescript
36
+ import { chunk, select, batch } from "stunk";
37
37
 
38
- A **chunk** is a small container of state. It holds a value, and you can do three things with it:
38
+ // Create a state chunk
39
+ const counter = chunk(0);
40
+ const userChunk = chunk({ name: "Olamide", age: 26 });
39
41
 
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
42
+ // Select specific state - Selector
43
+ const nameSelector = select(userChunk, (user) => user.name);
43
44
 
44
- ### Usage:
45
+ // Subscribe to changes
46
+ nameSelector.subscribe((name) => console.log("Name:", name));
47
+ counter.subscribe((count) => console.log("Counter", counter));
45
48
 
46
- ```ts
47
- import { chunk } from "stunk";
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
+ ```
48
55
 
49
- const count = chunk(0);
56
+ ## Core Concepts
50
57
 
51
- console.log(count.get()); // 0
58
+ ### Chunks
52
59
 
53
- count.set(5);
60
+ Basic unit of state with get/set/subscribe functionality:
54
61
 
55
- console.log(count.get()); // 5
62
+ ```typescript
63
+ const counter = chunk(0);
64
+ counter.subscribe((value) => console.log(value));
65
+ counter.set(5);
56
66
  ```
57
67
 
58
- ### 1.1. **Subscription**
68
+ ## Unsubscribing
59
69
 
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.
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..**
61
71
 
62
72
  ### Usage
63
73
 
@@ -65,18 +75,20 @@ You can **subscribe** to a **chunk**. This means you get notified whenever the v
65
75
  const count = chunk(0);
66
76
  const callback = (newValue: number) => console.log("Updated value:", newValue);
67
77
 
68
- count.subscribe(callback);
78
+ const unsubscribe = count.subscribe(callback);
69
79
 
70
80
  count.set(10); // Will log: "Updated value: 10"
81
+
82
+ unsubscribe(); // Unsubscribe
83
+
84
+ count.set(20); // Nothing will happen now, because you unsubscribed
71
85
  ```
72
86
 
73
- ### 1.2. **Deriving New Chunks**
87
+ ### Deriving New Chunks
74
88
 
75
89
  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
90
 
77
- ### Usage
78
-
79
- ```ts
91
+ ```typescript
80
92
  const count = chunk(5);
81
93
 
82
94
  // Create a derived chunk that doubles the count
@@ -91,113 +103,144 @@ count.set(10);
91
103
  // "Double count: 20"
92
104
  ```
93
105
 
94
- ### 1.3. **Unsubscribing**
106
+ ### Batch Updates
95
107
 
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? 😂
108
+ Group multiple updates:
97
109
 
98
- ### Usage
99
-
100
- ```ts
101
- const count = chunk(0);
102
- const callback = (newValue: number) => console.log("Updated value:", newValue);
110
+ ```typescript
111
+ batch(() => {
112
+ chunk1.set(newValue1);
113
+ chunk2.set(newValue2);
114
+ }); // Single notification
103
115
 
104
- const unsubscribe = count.subscribe(callback);
116
+ // Nested batches are also supported
117
+ batch(() => {
118
+ chunk1.set("Tunde");
119
+ batch(() => {
120
+ chunk1.set(26);
121
+ });
122
+ });
123
+ ```
105
124
 
106
- count.set(10); // Will log: "Updated value: 10"
125
+ ### Selectors
107
126
 
108
- unsubscribe(); // Unsubscribe
127
+ Efficiently access and react to specific state parts:
109
128
 
110
- count.set(20); // Nothing will happen now, because you unsubscribed
129
+ ```typescript
130
+ // With selector - more specific, read-only
131
+ const userChunk = chunk({ name: "Olamide", score: 100 });
132
+ const scoreSelector = select(userChunk, (u) => u.score);
133
+ // scoreSelector.set(200); // This would throw an error
111
134
  ```
112
135
 
113
- ### 1.4. **Batch Updates**
136
+ ## Middleware
114
137
 
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.
138
+ 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.
116
139
 
117
- ### Usage
140
+ ```typescript
141
+ // You can also create yours and pass it []
118
142
 
119
- ```ts
120
- import { chunk, batch } from "stunk";
143
+ // Use middleware for logging and validation
144
+ const age = chunk(25, [logger, nonNegativeValidator]);
121
145
 
122
- const firstName = chunk("John");
123
- const lastName = chunk("Doe");
124
- const age = chunk(25);
146
+ age.set(30); // Logs: "Setting value: 30"
147
+ age.set(-5); // Throws an error: "Value must be non-negative!"
148
+ ```
125
149
 
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));
150
+ ### History (Undo/Redo) - Time Travel
130
151
 
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
152
+ ```typescript
153
+ const counter = withHistory(chunk(0));
135
154
 
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
155
+ counter.set(10);
156
+ counter.set(20);
142
157
 
143
- // Nested batches are also supported
144
- batch(() => {
145
- firstName.set("Jane");
146
- batch(() => {
147
- lastName.set("Smith");
148
- age.set(26);
149
- });
150
- });
158
+ console.log(counter.get()); // 20
159
+
160
+ counter.undo(); // Go back one step
161
+ console.log(counter.get()); // 10
162
+
163
+ counter.redo(); // Go forward one step
164
+ console.log(counter.get()); // 20
151
165
  ```
152
166
 
153
- Looks intresting right? There's more!
167
+ **Example: Limiting History Size (Optional)**
168
+ You can specify a max history size to prevent excessive memory usage.
154
169
 
155
- Batching is particularly useful when:
170
+ ```ts
171
+ const counter = withHistory(chunk(0), { maxHistory: 5 }); // Only keeps the last 5 changes -- default is 100.
172
+ ```
156
173
 
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
174
+ This prevents the history from growing indefinitely and ensures efficient memory usage.
161
175
 
162
- The batch function ensures that:
176
+ ## API Reference
163
177
 
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)
178
+ ### `chunk<T>(initialValue: T, middleware?: Middleware<T>[])`
168
179
 
169
- ### 1.5. **Middleware**
180
+ Creates a new state chunk.
170
181
 
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.
182
+ ```typescript
183
+ interface Chunk<T> {
184
+ get(): T;
185
+ set(value: T): void;
186
+ subscribe(callback: (value: T) => void): () => void;
187
+ derive<D>(fn: (value: T) => D): Chunk<D>;
188
+ destroy(): void;
189
+ }
190
+ ```
172
191
 
173
- A middleware is a function with the following structure:
192
+ ### `select<T, S>(sourceChunk: Chunk<T>, selector: (value: T) => S)`
174
193
 
175
- ```ts
176
- export type Middleware<T> = (value: T, next: (newValue: T) => void) => void;
194
+ Creates an optimized selector.
195
+
196
+ ```typescript
197
+ // Returns a read-only chunk that updates only when selected value changes
198
+ const selector = select(userChunk, (user) => user.name);
177
199
  ```
178
200
 
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.
201
+ ### `batch(callback: () => void)`
181
202
 
182
- ### Usage
203
+ Batches multiple updates.
183
204
 
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 []
205
+ ```typescript
206
+ batch(() => {
207
+ // Multiple updates here
208
+ });
189
209
 
190
- // Use middleware for logging and validation
191
- const age = chunk(25, [logger, nonNegativeValidator]);
210
+ batch(() => {
211
+ // Multiple updates here
212
+ batch(() => {
213
+ // Nested upddates here
214
+ });
215
+ });
216
+ ```
192
217
 
193
- age.set(30); // Logs: "Setting value: 30"
194
- age.set(-5); // Throws an error: "Value must be non-negative!"
218
+ ### `withHistory<T>(chunk: Chunk<T>, options?: { maxHistory?: number })`
219
+
220
+ Adds undo/redo capabilities.
221
+
222
+ ```typescript
223
+ interface ChunkWithHistory<T> extends Chunk<T> {
224
+ undo(): void; // Reverts to the previous state (if available).
225
+ redo(): void; // Moves forward to the next state (if available).
226
+ canUndo(): boolean; // Returns `true` if there are past states available.
227
+ canRedo(): boolean; // Returns `true` if there are future states available.
228
+ getHistory(): T[]; // Returns an `array` of all past states.
229
+ clearHistory(): void; // Clears all stored history and keeps only the current state.
230
+ }
231
+ ```
232
+
233
+ ### `Middleware<T>`
234
+
235
+ Custom state processing:
236
+
237
+ ```typescript
238
+ type Middleware<T> = (value: T, next: (newValue: T) => void) => void;
195
239
  ```
196
240
 
197
- ## 2. **Atomic State Technique**
241
+ - value: The value that is about to be set to the chunk.
242
+ - 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.
198
243
 
199
- The **Atomic State** technique is all about breaking down your state into small, manageable chunks. This allows you to:
244
+ ## License
200
245
 
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
246
+ MIT
@@ -0,0 +1 @@
1
+ "use strict";
@@ -14,6 +14,28 @@ export function batch(callback) {
14
14
  }
15
15
  }
16
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
+ }
17
39
  export function chunk(initialValue, middleware = []) {
18
40
  if (initialValue === undefined || initialValue === null) {
19
41
  throw new Error("Initial value cannot be undefined or null.");
package/dist/index.js CHANGED
@@ -1,3 +1,2 @@
1
- export { chunk } from './core';
2
- export { logger } from "./middleware/logger";
3
- export { nonNegativeValidator } from "./middleware/validator";
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
+ }
@@ -0,0 +1,3 @@
1
+ export { logger } from "./logger";
2
+ export { nonNegativeValidator } from "./validator";
3
+ export { withHistory } from "./history";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stunk",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",
File without changes
@@ -34,6 +34,32 @@ export function batch(callback: () => void) {
34
34
  }
35
35
 
36
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
+
37
63
  export function chunk<T>(initialValue: T, middleware: Middleware<T>[] = []): Chunk<T> {
38
64
  if (initialValue === undefined || initialValue === null) {
39
65
  throw new Error("Initial value cannot be undefined or null.");
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
- export { chunk } from './core';
2
- export type { Chunk } from './core';
1
+ export { chunk } from './core/core';
2
+ export type { Chunk } from './core/core';
3
3
 
4
- export { logger } from "./middleware/logger";
5
- export { nonNegativeValidator } from "./middleware/validator";
4
+ export * from "./middleware";
@@ -0,0 +1,86 @@
1
+ import { Chunk } from "../core/core";
2
+
3
+ export interface ChunkWithHistory<T> extends Chunk<T> {
4
+ undo: () => void;
5
+ redo: () => void;
6
+ canUndo: () => boolean;
7
+ canRedo: () => boolean;
8
+ getHistory: () => T[];
9
+ clearHistory: () => void;
10
+ }
11
+
12
+ export function withHistory<T>(
13
+ baseChunk: Chunk<T>,
14
+ options: { maxHistory?: number } = {}
15
+ ): ChunkWithHistory<T> {
16
+ const { maxHistory = 100 } = options;
17
+ const history: T[] = [baseChunk.get()];
18
+ let currentIndex = 0;
19
+ let isHistoryAction = false;
20
+
21
+ const historyChunk: ChunkWithHistory<T> = {
22
+ ...baseChunk,
23
+
24
+ set: (newValue: T) => {
25
+ if (isHistoryAction) {
26
+ baseChunk.set(newValue);
27
+ return;
28
+ }
29
+
30
+ // Remove any future history when setting a new value
31
+ history.splice(currentIndex + 1);
32
+ history.push(newValue);
33
+
34
+ // Limit history size
35
+ if (history.length > maxHistory) {
36
+ console.warn("History limit reached. Removing oldest entries.");
37
+ const removeCount = history.length - maxHistory;
38
+ history.splice(0, removeCount);
39
+ currentIndex = Math.max(0, currentIndex - removeCount);
40
+ }
41
+
42
+ currentIndex = history.length - 1;
43
+ baseChunk.set(newValue);
44
+ },
45
+
46
+ undo: () => {
47
+ if (!historyChunk.canUndo()) return;
48
+
49
+ isHistoryAction = true;
50
+ currentIndex--;
51
+ historyChunk.set(history[currentIndex]);
52
+ isHistoryAction = false;
53
+ },
54
+
55
+ redo: () => {
56
+ if (!historyChunk.canRedo()) return;
57
+
58
+ isHistoryAction = true;
59
+ currentIndex++;
60
+ historyChunk.set(history[currentIndex]);
61
+ isHistoryAction = false;
62
+ },
63
+
64
+ canUndo: () => currentIndex > 0,
65
+
66
+ canRedo: () => currentIndex < history.length - 1,
67
+
68
+ getHistory: () => [...history],
69
+
70
+ clearHistory: () => {
71
+ const currentValue = baseChunk.get();
72
+ history.length = 0;
73
+ history.push(currentValue);
74
+ currentIndex = 0;
75
+ },
76
+
77
+ // Override destroy to clean up history
78
+ destroy: () => {
79
+ history.length = 0;
80
+ baseChunk.destroy();
81
+ }
82
+ }
83
+
84
+ return historyChunk;
85
+
86
+ }
@@ -0,0 +1,3 @@
1
+ export { logger } from "./logger";
2
+ export { nonNegativeValidator } from "./validator";
3
+ export { withHistory } from "./history";
@@ -1,4 +1,4 @@
1
- import { Middleware } from "../core";
1
+ import { Middleware } from "../core/core";
2
2
 
3
3
  export const logger: Middleware<any> = (value, next) => {
4
4
  console.log("Setting value:", value);
@@ -1,4 +1,4 @@
1
- import { Middleware } from "../core";
1
+ import { Middleware } from "../core/core";
2
2
 
3
3
  export const nonNegativeValidator: Middleware<number> = (value, next) => {
4
4
  if (value < 0) {
@@ -1,4 +1,4 @@
1
- import { chunk, batch } from '../src/core';
1
+ import { chunk, batch } from '../src/core/core';
2
2
 
3
3
 
4
4
  describe('Chunk batch updates', () => {
@@ -1,4 +1,4 @@
1
- import { chunk } from "../src/core";
1
+ import { chunk } from "../src/core/core";
2
2
 
3
3
  test("Chunk should get and set values correctly", () => {
4
4
  const chunky = chunk<number>(0);
@@ -0,0 +1,99 @@
1
+ import { batch, chunk } from "../src/core/core";
2
+ import { withHistory } from "../src/middleware/history";
3
+
4
+
5
+ describe('Chunk with History', () => {
6
+ it('should maintain history of changes', () => {
7
+ const baseChunk = chunk(0);
8
+ const historyChunk = withHistory(baseChunk);
9
+
10
+ historyChunk.set(1);
11
+ historyChunk.set(2);
12
+ historyChunk.set(3);
13
+
14
+ expect(historyChunk.getHistory()).toEqual([0, 1, 2, 3]);
15
+ });
16
+
17
+ it('should handle undo and redo operations', () => {
18
+ const baseChunk = chunk(0);
19
+ const historyChunk = withHistory(baseChunk);
20
+ const callback = jest.fn();
21
+
22
+ historyChunk.subscribe(callback);
23
+ callback.mockClear(); // Clear initial subscription call
24
+
25
+ historyChunk.set(1);
26
+ historyChunk.set(2);
27
+
28
+ expect(historyChunk.get()).toBe(2);
29
+
30
+ historyChunk.undo();
31
+ expect(historyChunk.get()).toBe(1);
32
+
33
+ historyChunk.undo();
34
+ expect(historyChunk.get()).toBe(0);
35
+
36
+ historyChunk.redo();
37
+ expect(historyChunk.get()).toBe(1);
38
+
39
+ historyChunk.redo();
40
+ expect(historyChunk.get()).toBe(2);
41
+
42
+ expect(callback).toHaveBeenCalledTimes(6); // 2 sets + 2 undos + 2 redos
43
+ });
44
+
45
+ it('should handle branching history', () => {
46
+ const baseChunk = chunk(0);
47
+ const historyChunk = withHistory(baseChunk);
48
+
49
+ historyChunk.set(1);
50
+ historyChunk.set(2);
51
+ historyChunk.undo();
52
+ historyChunk.set(3); // This should create a new branch
53
+
54
+ expect(historyChunk.getHistory()).toEqual([0, 1, 3]);
55
+ expect(historyChunk.get()).toBe(3);
56
+ });
57
+
58
+ it('should respect maxHistory limit', () => {
59
+ const baseChunk = chunk(0);
60
+ const historyChunk = withHistory(baseChunk, { maxHistory: 3 });
61
+
62
+ historyChunk.set(1);
63
+ historyChunk.set(2);
64
+ historyChunk.set(3);
65
+ historyChunk.set(4);
66
+
67
+ expect(historyChunk.getHistory()).toEqual([2, 3, 4]);
68
+ });
69
+
70
+ it('should handle canUndo and canRedo correctly', () => {
71
+ const baseChunk = chunk(0);
72
+ const historyChunk = withHistory(baseChunk);
73
+
74
+ expect(historyChunk.canUndo()).toBe(false);
75
+ expect(historyChunk.canRedo()).toBe(false);
76
+
77
+ historyChunk.set(1);
78
+ expect(historyChunk.canUndo()).toBe(true);
79
+ expect(historyChunk.canRedo()).toBe(false);
80
+
81
+ historyChunk.undo();
82
+ expect(historyChunk.canUndo()).toBe(false);
83
+ expect(historyChunk.canRedo()).toBe(true);
84
+ });
85
+
86
+ it('should clear history properly', () => {
87
+ const baseChunk = chunk(0);
88
+ const historyChunk = withHistory(baseChunk);
89
+
90
+ historyChunk.set(1);
91
+ historyChunk.set(2);
92
+
93
+ historyChunk.clearHistory();
94
+
95
+ expect(historyChunk.getHistory()).toEqual([2]);
96
+ expect(historyChunk.canUndo()).toBe(false);
97
+ expect(historyChunk.canRedo()).toBe(false);
98
+ });
99
+ });
@@ -1,4 +1,4 @@
1
- import { chunk } from "../src/core";
1
+ import { chunk } from "../src/core/core";
2
2
  import { logger } from "../src/middleware/logger";
3
3
  import { nonNegativeValidator } from "../src/middleware/validator";
4
4
 
@@ -0,0 +1,132 @@
1
+ import { chunk, select } from '../src/core/core';
2
+
3
+ describe('select', () => {
4
+ it('should create a selector that initially returns the correct value', () => {
5
+ const source = chunk({ name: 'John', age: 25 });
6
+ const nameSelector = select(source, user => user.name);
7
+
8
+ expect(nameSelector.get()).toBe('John');
9
+ });
10
+
11
+ it('should update when selected value changes', () => {
12
+ const source = chunk({ name: 'John', age: 25 });
13
+ const nameSelector = select(source, user => user.name);
14
+
15
+ source.set({ name: 'Jane', age: 25 });
16
+ expect(nameSelector.get()).toBe('Jane');
17
+ });
18
+
19
+ it('should not notify subscribers when non-selected values change', () => {
20
+ const source = chunk({ name: 'John', age: 25 });
21
+ const nameSelector = select(source, user => user.name);
22
+
23
+ const subscriber = jest.fn();
24
+ nameSelector.subscribe(subscriber);
25
+
26
+ // Reset the mock to ignore initial call
27
+ subscriber.mockReset();
28
+
29
+ // Update age only
30
+ source.set({ name: 'John', age: 26 });
31
+
32
+ expect(subscriber).not.toHaveBeenCalled();
33
+ });
34
+
35
+ it('should notify subscribers when selected value changes', () => {
36
+ const source = chunk({ name: 'John', age: 25 });
37
+ const nameSelector = select(source, user => user.name);
38
+
39
+ const subscriber = jest.fn();
40
+ nameSelector.subscribe(subscriber);
41
+
42
+ // Reset the mock to ignore initial call
43
+ subscriber.mockReset();
44
+
45
+ source.set({ name: 'Jane', age: 25 });
46
+
47
+ expect(subscriber).toHaveBeenCalledTimes(1);
48
+ expect(subscriber).toHaveBeenCalledWith('Jane');
49
+ });
50
+
51
+ it('should prevent direct modifications to selector', () => {
52
+ const source = chunk({ name: 'John', age: 25 });
53
+ const nameSelector = select(source, user => user.name);
54
+
55
+ expect(() => {
56
+ nameSelector.set('Jane');
57
+ }).toThrow('Cannot set values directly on a selector');
58
+ });
59
+
60
+ it('should work with complex selectors', () => {
61
+ const source = chunk({ user: { profile: { name: 'John' } } });
62
+ const nameSelector = select(source, state => state.user.profile.name);
63
+
64
+ expect(nameSelector.get()).toBe('John');
65
+
66
+ source.set({ user: { profile: { name: 'Jane' } } });
67
+ expect(nameSelector.get()).toBe('Jane');
68
+ });
69
+
70
+ it('should handle array selectors', () => {
71
+ const source = chunk({ items: [1, 2, 3] });
72
+ const firstItemSelector = select(source, state => state.items[0]);
73
+
74
+ expect(firstItemSelector.get()).toBe(1);
75
+
76
+ source.set({ items: [4, 2, 3] });
77
+ expect(firstItemSelector.get()).toBe(4);
78
+ });
79
+
80
+ it('should work with computed values', () => {
81
+ const source = chunk({ numbers: [1, 2, 3, 4, 5] });
82
+ const sumSelector = select(source, state =>
83
+ state.numbers.reduce((sum, num) => sum + num, 0)
84
+ );
85
+
86
+ expect(sumSelector.get()).toBe(15);
87
+
88
+ source.set({ numbers: [1, 2, 3] });
89
+ expect(sumSelector.get()).toBe(6);
90
+ });
91
+
92
+ it('should properly clean up subscriptions on destroy', () => {
93
+ const source = chunk({ name: 'John', age: 25 });
94
+ const nameSelector = select(source, user => user.name);
95
+
96
+ const subscriber = jest.fn();
97
+ const unsubscribe = nameSelector.subscribe(subscriber);
98
+
99
+ // Reset mock to ignore initial call
100
+ subscriber.mockReset();
101
+
102
+ unsubscribe();
103
+ nameSelector.destroy();
104
+ source.set({ name: 'Jane', age: 25 });
105
+
106
+ expect(subscriber).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it('should work with multiple independent selectors', () => {
110
+ const source = chunk({ name: 'John', age: 25 });
111
+ const nameSelector = select(source, user => user.name);
112
+ const ageSelector = select(source, user => user.age);
113
+
114
+ const nameSubscriber = jest.fn();
115
+ const ageSubscriber = jest.fn();
116
+
117
+ nameSelector.subscribe(nameSubscriber);
118
+ ageSelector.subscribe(ageSubscriber);
119
+
120
+ // Reset mocks to ignore initial calls
121
+ nameSubscriber.mockReset();
122
+ ageSubscriber.mockReset();
123
+
124
+ source.set({ name: 'John', age: 26 });
125
+ expect(nameSubscriber).not.toHaveBeenCalled();
126
+ expect(ageSubscriber).toHaveBeenCalledWith(26);
127
+
128
+ source.set({ name: 'Jane', age: 26 });
129
+ expect(nameSubscriber).toHaveBeenCalledWith('Jane');
130
+ expect(ageSubscriber).toHaveBeenCalledTimes(1); // Still from previous update
131
+ });
132
+ });