doit-lib 1.2.0 → 1.2.1

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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/package.json +8 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 khalif
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,318 @@
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/khalifmv/doit/main/doit.png" width="100px" style="background-color: #505050ff; padding: 10px; border-radius: 10px;"/>
3
+ <p>A lightweight & framework-agnostic, path-based undo-redo state management library for JavaScript/TypeScript applications.</p>
4
+ <div style="display: inline-block;">
5
+ <a href="https://www.npmjs.com/package/doit-lib" target="_blank"><img src="https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=fff" alt="npm"></a>
6
+ <a href="https://github.com/khalifmv/doit-lib/blob/main/LICENSE" target="_blank">
7
+ <img src="https://img.shields.io/npm/l/doit-lib" alt="license">
8
+ <img src="https://img.shields.io/badge/platform-browser-blue" />
9
+ <img alt="gzip size" src="https://img.shields.io/bundlejs/size/doit-lib">
10
+
11
+ </a>
12
+
13
+ </div>
14
+ </div>
15
+
16
+ ## Concept
17
+
18
+ `doit-lib` provides a centralized store where every mutation is recorded as an operation (`set`, `delete`). It allows you to modify deeply nested state using a string-based path syntax, automatically generating the corresponding inverse operations for undo capabilities.
19
+
20
+ Key features:
21
+ - **Path-based mutations**: Modify deep state easily (e.g., `users[0].name`).
22
+ - **Array Filtering**: Target array items by properties (e.g., `todos[id:123].completed`).
23
+ - **Automatic History**: Every `set` generates an undo/redo stack.
24
+ - **Framework Agnostic**: Works with Vanilla JS, React, Vue, Svelte, etc.
25
+ - **TypeScript Support**: Full type inference and autocompletion.
26
+ - **Persistence**: Auto-save to localStorage or sessionStorage.
27
+ - **Batch Operations**: Group multiple `set` operations into a single undo/redo entry.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install doit-lib
33
+ ```
34
+
35
+ ## Basic Usage
36
+
37
+ ```typescript
38
+ import { DoIt } from 'doit-lib';
39
+
40
+ // Initialize store
41
+ const store = new DoIt({
42
+ user: { name: 'Alice', score: 10 },
43
+ todos: []
44
+ });
45
+
46
+ // Subscribe to changes
47
+ store.subscribe((state, history) => {
48
+ console.log('New State:', state);
49
+ console.log('History:', history);
50
+ });
51
+
52
+ // Update state
53
+ store.set('user.score', 20);
54
+ store.set('todos[0]', { id: 1, task: 'Buy milk', done: false });
55
+
56
+ // Undo change
57
+ store.undo(); // user.score reverts to 10
58
+ ```
59
+
60
+ ## Path Syntax
61
+
62
+ The library uses a powerful path syntax to traverse and modify objects:
63
+
64
+ - **Dot Notation**: `user.name` targets the `name` property of `user`.
65
+ - **Array Index**: `items[0]` targets the first element of `items`.
66
+ - **Filter Selector**: `users[id:42]` targets the element in the `users` array where `id` equals `42`.
67
+ - If the item doesn't exist during a `set` operation, it can verify existence or handle it based on logic (currently, it might attempt to create it if implied, or strictly find it). *Note: The current implementation attempts to find existing items.*
68
+
69
+ ## API
70
+
71
+ ### Constructor
72
+
73
+ ```typescript
74
+ new DoIt(initialState?, options?)
75
+ ```
76
+
77
+ - **initialState** (optional): The initial state object. Default: `{}`
78
+ - **options** (optional):
79
+ - `maxHistory`: Maximum number of history entries to keep. Default: `100`
80
+
81
+ **Example:**
82
+ ```typescript
83
+ const store = new DoIt({ count: 0 }, { maxHistory: 50 });
84
+ ```
85
+
86
+ ---
87
+
88
+ ### Methods
89
+
90
+ #### `set(path: string, value: any): void`
91
+ Sets a value at the specified path and records the operation in history.
92
+
93
+ ```typescript
94
+ store.set('user.name', 'Alice');
95
+ store.set('todos[0].completed', true);
96
+ store.set('items[id:42].quantity', 10);
97
+ ```
98
+
99
+ > **Note:** If the value is identical to the current value (using deep equality), the operation is skipped and no history entry is created.
100
+
101
+ #### `getState(): any`
102
+ Returns the current state object.
103
+
104
+ ```typescript
105
+ const currentState = store.getState();
106
+ ```
107
+
108
+ #### `getHistory(): HistoryInfo`
109
+ Returns information about the undo/redo history.
110
+
111
+ ```typescript
112
+ const history = store.getHistory();
113
+ // { undo: 5, redo: 2, canUndo: true, canRedo: true }
114
+ ```
115
+
116
+ #### `undo(): boolean`
117
+ Undoes the last operation. Returns `true` if successful, `false` if there's nothing to undo.
118
+
119
+ ```typescript
120
+ const success = store.undo();
121
+ ```
122
+
123
+ #### `redo(): boolean`
124
+ Redoes the last undone operation. Returns `true` if successful, `false` if there's nothing to redo.
125
+
126
+ ```typescript
127
+ const success = store.redo();
128
+ ```
129
+
130
+ #### `clearHistory(): void`
131
+ Clears all undo/redo history (does not modify the current state).
132
+
133
+ ```typescript
134
+ store.clearHistory();
135
+ ```
136
+
137
+ #### `subscribe(callback: (state: any, history: any) => void): () => void`
138
+ Subscribes to state changes. Returns an unsubscribe function.
139
+
140
+ ```typescript
141
+ const unsubscribe = store.subscribe((state, history) => {
142
+ console.log('State changed:', state);
143
+ console.log('Can undo:', history.canUndo);
144
+ });
145
+
146
+ // Later: unsubscribe()
147
+ ```
148
+
149
+ #### `batch(callback: (batch) => void): void`
150
+ Groups multiple `set` operations into a single history entry. All operations within the batch are undone/redone together.
151
+
152
+ ```typescript
153
+ store.batch((b) => {
154
+ b.set('user.name', 'Alice');
155
+ b.set('user.age', 25);
156
+ b.set('user.city', 'NYC');
157
+ });
158
+
159
+ // Only 1 history entry created for all 3 operations
160
+ // Undo will revert all 3 changes at once
161
+ ```
162
+
163
+ **Use cases:**
164
+ - Form submissions with multiple field updates
165
+ - Bulk data imports
166
+ - Complex state transitions that should be atomic
167
+
168
+ > **Note:** Duplicate values are still skipped within batches.
169
+
170
+ #### `persist(options: PersistOptions): DoIt`
171
+ Enables automatic persistence to localStorage or sessionStorage. Returns the store instance for chaining.
172
+
173
+ ```typescript
174
+ store.persist({ to: localStorage });
175
+ store.persist({ to: sessionStorage, key: 'my-app-state' });
176
+ ```
177
+
178
+ - **options&#46;to**: Storage adapter (localStorage or sessionStorage)
179
+ - **options.key** (optional): Storage key. Default: `'doit-state'`
180
+
181
+ **Features:**
182
+ - Auto-saves state and history on every change
183
+ - Auto-restores state and history on initialization
184
+ - Handles serialization errors gracefully
185
+
186
+ #### `unpersist(): void`
187
+ Disables persistence (does not clear existing stored data).
188
+
189
+ ```typescript
190
+ store.unpersist();
191
+ ```
192
+
193
+ ---
194
+
195
+ ### Types
196
+
197
+ ```typescript
198
+ type PathToken =
199
+ | { type: 'key'; value: string }
200
+ | { type: 'index'; value: number }
201
+ | { type: 'filter'; key: string; value: any };
202
+
203
+ type Operation =
204
+ | { op: 'set'; path: string; value: any }
205
+ | { op: 'delete'; path: string }
206
+ | { op: 'batch'; ops: Operation[] };
207
+
208
+ interface StorageAdapter {
209
+ getItem(key: string): string | null;
210
+ setItem(key: string, value: string): void;
211
+ removeItem(key: string): void;
212
+ }
213
+ ```
214
+
215
+
216
+ ## Framework Integration
217
+
218
+ ### React Example
219
+
220
+ You can create a simple hook to connect the store to your components.
221
+
222
+ ```tsx
223
+ import { useEffect, useState } from 'react';
224
+ import { DoIt } from 'doit-lib';
225
+
226
+ const store = new DoIt({ count: 0 });
227
+
228
+ export function useDoIt(store) {
229
+ const [state, setState] = useState(store.getState());
230
+
231
+ useEffect(() => {
232
+ return store.subscribe((newState) => {
233
+ setState({ ...newState }); // Trigger re-render
234
+ });
235
+ }, [store]);
236
+
237
+ return state;
238
+ }
239
+
240
+ // Component
241
+ export const Counter = () => {
242
+ const state = useDoIt(store);
243
+
244
+ return (
245
+ <div>
246
+ <h1>{state.count}</h1>
247
+ <button onClick={() => store.set('count', state.count + 1)}>Increment</button>
248
+ <button onClick={() => store.undo()}>Undo</button>
249
+ </div>
250
+ );
251
+ };
252
+ ```
253
+
254
+ ### Vue Example
255
+
256
+ Using Vue's Composition API.
257
+
258
+ ```vue
259
+ <script setup>
260
+ import { ref, onMounted, onUnmounted } from 'vue';
261
+ import { DoIt } from 'doit-lib';
262
+
263
+ const store = new DoIt({ count: 0 });
264
+ const state = ref(store.getState());
265
+
266
+ let unsubscribe;
267
+
268
+ onMounted(() => {
269
+ unsubscribe = store.subscribe((newState) => {
270
+ state.value = { ...newState }; // Update reactive ref
271
+ });
272
+ });
273
+
274
+ onUnmounted(() => {
275
+ if (unsubscribe) unsubscribe();
276
+ });
277
+
278
+ const inc = () => store.set('count', state.value.count + 1);
279
+ const undo = () => store.undo();
280
+ </script>
281
+
282
+ <template>
283
+ <div>
284
+ <h1>{{ state.count }}</h1>
285
+ <button @click="inc">Increment</button>
286
+ <button @click="undo">Undo</button>
287
+ </div>
288
+ </template>
289
+ ```
290
+
291
+ ## Running the Demo
292
+
293
+ This repository is set up as a monorepo. To run the included Svelte-based demo:
294
+
295
+ 1. Navigate to the demo package:
296
+ ```bash
297
+ cd packages/demo
298
+ ```
299
+ 2. Install dependencies (if not done at root):
300
+ ```bash
301
+ npm install
302
+ ```
303
+ 3. Start the development server:
304
+ ```bash
305
+ npm run dev
306
+ ```
307
+
308
+ ## Limitations
309
+
310
+ 1. **JSON-serializable State**: The library is best suited for serializable data structures (objects, arrays, primitives). Complex class instances or cyclic references may not work as expected or preserve their prototype chain during history traversal.
311
+ 2. **Filter Performance**: Using filter selectors (e.g., `list[id:123]`) involves a linear search over the array. For very large arrays, this might impact performance.
312
+
313
+ ## TODO
314
+
315
+ * [x] Publish to NPM
316
+ * [x] Add support for persistent history (e.g., localStorage)
317
+ * [x] Add support for batch operations
318
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doit-lib",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -8,6 +8,12 @@
8
8
  "test": "vitest run",
9
9
  "build": "tsc --emitDeclarationOnly && esbuild src/index.ts --bundle --minify --format=esm --outfile=dist/index.js && gzip -k -f dist/index.js"
10
10
  },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
11
17
  "keywords": [
12
18
  "undo-redo",
13
19
  "state-management",
@@ -16,11 +22,10 @@
16
22
  ],
17
23
  "author": {
18
24
  "name": "Khalif",
19
- "email": "",
20
25
  "url": "https://github.com/khalifmv"
21
26
  },
22
27
  "license": "MIT",
23
- "description": "",
28
+ "description": "Framework-agnostic undo/redo state management library",
24
29
  "files": [
25
30
  "dist",
26
31
  "README.md",