@jucie.io/state-history 1.0.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.
- package/README.md +320 -0
- package/dist/main.js +2 -0
- package/dist/main.js.map +7 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# @jucio.io/state/history
|
|
2
|
+
|
|
3
|
+
History management plugin for @jucio.io/state that provides undo/redo functionality with markers, batching, and commit listeners.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ⏪ **Undo/Redo**: Full undo and redo support with automatic change tracking
|
|
8
|
+
- 🏷️ **Markers**: Add descriptive markers to create logical undo/redo boundaries
|
|
9
|
+
- 📦 **Batching**: Group multiple changes into a single undo/redo step
|
|
10
|
+
- 🔔 **Commit Listeners**: React to history commits
|
|
11
|
+
- 🎯 **Smart Change Consolidation**: Automatically merges changes to the same path
|
|
12
|
+
- 📏 **Configurable History Size**: Limit the number of stored changes
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @jucio.io/state
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Note:** History plugin is included in the main package.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import { createState } from '@jucio.io/state';
|
|
26
|
+
import { HistoryManager } from '@jucio.io/state/history';
|
|
27
|
+
|
|
28
|
+
// Create state and install history plugin
|
|
29
|
+
const state = createState({
|
|
30
|
+
data: { count: 0 }
|
|
31
|
+
});
|
|
32
|
+
state.install(HistoryManager);
|
|
33
|
+
|
|
34
|
+
// Make some changes
|
|
35
|
+
state.set(['data', 'count'], 1);
|
|
36
|
+
state.set(['data', 'count'], 2);
|
|
37
|
+
state.set(['data', 'count'], 3);
|
|
38
|
+
|
|
39
|
+
console.log(state.get(['data', 'count'])); // 3
|
|
40
|
+
|
|
41
|
+
// Undo the changes
|
|
42
|
+
state.history.undo();
|
|
43
|
+
console.log(state.get(['data', 'count'])); // 2
|
|
44
|
+
|
|
45
|
+
state.history.undo();
|
|
46
|
+
console.log(state.get(['data', 'count'])); // 1
|
|
47
|
+
|
|
48
|
+
// Redo
|
|
49
|
+
state.history.redo();
|
|
50
|
+
console.log(state.get(['data', 'count'])); // 2
|
|
51
|
+
|
|
52
|
+
// Check if undo/redo is available
|
|
53
|
+
console.log(state.history.canUndo()); // true
|
|
54
|
+
console.log(state.history.canRedo()); // true
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### Actions
|
|
60
|
+
|
|
61
|
+
#### `undo(callback?)`
|
|
62
|
+
|
|
63
|
+
Undo the last committed change(s).
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
state.history.undo(() => {
|
|
67
|
+
console.log('Undo completed');
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Returns:** `boolean` - `true` if undo was successful, `false` if no changes to undo
|
|
72
|
+
|
|
73
|
+
#### `redo(callback?)`
|
|
74
|
+
|
|
75
|
+
Redo the next change(s).
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
state.history.redo(() => {
|
|
79
|
+
console.log('Redo completed');
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Returns:** `boolean` - `true` if redo was successful, `false` if no changes to redo
|
|
84
|
+
|
|
85
|
+
#### `canUndo()`
|
|
86
|
+
|
|
87
|
+
Check if undo is available.
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
if (state.history.canUndo()) {
|
|
91
|
+
state.history.undo();
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Returns:** `boolean`
|
|
96
|
+
|
|
97
|
+
#### `canRedo()`
|
|
98
|
+
|
|
99
|
+
Check if redo is available.
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
if (state.history.canRedo()) {
|
|
103
|
+
state.history.redo();
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Returns:** `boolean`
|
|
108
|
+
|
|
109
|
+
### Batching
|
|
110
|
+
|
|
111
|
+
#### `batch()`
|
|
112
|
+
|
|
113
|
+
Start a batch to group multiple changes into a single undo/redo step.
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const endBatch = state.history.batch();
|
|
117
|
+
|
|
118
|
+
state.set(['user', 'name'], 'Alice');
|
|
119
|
+
state.set(['user', 'age'], 30);
|
|
120
|
+
state.set(['user', 'email'], 'alice@example.com');
|
|
121
|
+
|
|
122
|
+
endBatch(); // All three changes are now a single undo/redo step
|
|
123
|
+
|
|
124
|
+
state.history.undo(); // Undoes all three changes at once
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Returns:** `Function` - Call the returned function to end the batch
|
|
128
|
+
|
|
129
|
+
#### `commit()`
|
|
130
|
+
|
|
131
|
+
Manually commit pending changes and end the current batch.
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
state.history.batch();
|
|
135
|
+
state.set(['data', 'value'], 1);
|
|
136
|
+
state.history.commit(); // Forces commit
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Returns:** `HistoryManager` instance (for chaining)
|
|
140
|
+
|
|
141
|
+
### Markers
|
|
142
|
+
|
|
143
|
+
#### `addMarker(description)`
|
|
144
|
+
|
|
145
|
+
Add a descriptive marker to create a logical undo/redo boundary.
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
state.set(['user', 'name'], 'Alice');
|
|
149
|
+
state.history.addMarker('Set user name');
|
|
150
|
+
|
|
151
|
+
state.set(['user', 'age'], 30);
|
|
152
|
+
state.history.addMarker('Set user age');
|
|
153
|
+
|
|
154
|
+
// Now each undo will stop at the marker
|
|
155
|
+
state.history.undo(); // Undoes age change
|
|
156
|
+
state.history.undo(); // Undoes name change
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Parameters:**
|
|
160
|
+
- `description` (string): Optional description for the marker
|
|
161
|
+
|
|
162
|
+
### Commit Listeners
|
|
163
|
+
|
|
164
|
+
#### `onCommit(callback)`
|
|
165
|
+
|
|
166
|
+
Listen for history commits.
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
const unsubscribe = state.history.onCommit((changes) => {
|
|
170
|
+
console.log('Changes committed:', changes);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
state.set(['data', 'value'], 1);
|
|
174
|
+
// Console: "Changes committed: [...]"
|
|
175
|
+
|
|
176
|
+
// Remove listener when done
|
|
177
|
+
unsubscribe();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Parameters:**
|
|
181
|
+
- `callback` (Function): Called with an array of changes when committed
|
|
182
|
+
|
|
183
|
+
**Returns:** `Function` - Call to remove the listener
|
|
184
|
+
|
|
185
|
+
### Info
|
|
186
|
+
|
|
187
|
+
#### `size()`
|
|
188
|
+
|
|
189
|
+
Get the current number of items in the history.
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
const historySize = state.history.size();
|
|
193
|
+
console.log(`History contains ${historySize} items`);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Returns:** `number`
|
|
197
|
+
|
|
198
|
+
## Configuration
|
|
199
|
+
|
|
200
|
+
Configure the history plugin using the `configure()` method:
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
import { createState } from '@jucio.io/state';
|
|
204
|
+
import { HistoryManager } from '@jucio.io/state/history';
|
|
205
|
+
|
|
206
|
+
const state = createState({
|
|
207
|
+
data: { count: 0 }
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Install with custom configuration
|
|
211
|
+
state.install(HistoryManager.configure({
|
|
212
|
+
maxSize: 200 // Limit to 200 history items (default: 100)
|
|
213
|
+
}));
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Options
|
|
217
|
+
|
|
218
|
+
- **`maxSize`** (number): Maximum number of history items to keep. Default: `100`
|
|
219
|
+
|
|
220
|
+
## Advanced Usage
|
|
221
|
+
|
|
222
|
+
### Complex Batching with Markers
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
// Start a complex operation
|
|
226
|
+
state.history.batch();
|
|
227
|
+
state.history.addMarker('Start user registration');
|
|
228
|
+
|
|
229
|
+
state.set(['user', 'name'], 'Alice');
|
|
230
|
+
state.set(['user', 'email'], 'alice@example.com');
|
|
231
|
+
state.set(['user', 'preferences'], { theme: 'dark' });
|
|
232
|
+
|
|
233
|
+
state.history.commit();
|
|
234
|
+
|
|
235
|
+
// All changes are now a single undo step with a descriptive marker
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Pause and Resume Recording
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
// Temporarily pause history recording (internal API)
|
|
242
|
+
state.plugins.history.pause();
|
|
243
|
+
|
|
244
|
+
state.set(['temp', 'data'], 'not recorded');
|
|
245
|
+
|
|
246
|
+
state.plugins.history.resume();
|
|
247
|
+
|
|
248
|
+
state.set(['tracked', 'data'], 'recorded'); // This will be recorded
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Reset History
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
// Clear all history (internal API)
|
|
255
|
+
state.plugins.history.reset();
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## How It Works
|
|
259
|
+
|
|
260
|
+
1. **Change Tracking**: The plugin automatically tracks all state changes
|
|
261
|
+
2. **Consolidation**: Multiple changes to the same path are consolidated
|
|
262
|
+
3. **Deferred Commits**: Changes are committed asynchronously for performance
|
|
263
|
+
4. **Markers**: Markers create logical boundaries for undo/redo operations
|
|
264
|
+
5. **Inversion**: Changes are inverted for undo operations
|
|
265
|
+
|
|
266
|
+
## Common Patterns
|
|
267
|
+
|
|
268
|
+
### Form Editing with Undo/Redo
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
// Track form edits
|
|
272
|
+
function handleFieldChange(field, value) {
|
|
273
|
+
const endBatch = state.history.batch();
|
|
274
|
+
state.set(['form', field], value);
|
|
275
|
+
state.history.addMarker(`Update ${field}`);
|
|
276
|
+
endBatch();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Implement undo/redo buttons
|
|
280
|
+
function handleUndo() {
|
|
281
|
+
if (state.history.canUndo()) {
|
|
282
|
+
state.history.undo(() => {
|
|
283
|
+
updateUI();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Multi-Step Operations
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
function performComplexOperation() {
|
|
293
|
+
const endBatch = state.history.batch();
|
|
294
|
+
|
|
295
|
+
// Step 1: Update user
|
|
296
|
+
state.set(['user', 'status'], 'processing');
|
|
297
|
+
|
|
298
|
+
// Step 2: Create records
|
|
299
|
+
state.set(['records'], [{ id: 1, status: 'new' }]);
|
|
300
|
+
|
|
301
|
+
// Step 3: Update timestamp
|
|
302
|
+
state.set(['lastUpdate'], Date.now());
|
|
303
|
+
|
|
304
|
+
endBatch();
|
|
305
|
+
state.history.addMarker('Complex operation completed');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// All steps undo/redo as one operation
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## License
|
|
312
|
+
|
|
313
|
+
See the root [LICENSE](../../LICENSE) file for license information.
|
|
314
|
+
|
|
315
|
+
## Related Packages
|
|
316
|
+
|
|
317
|
+
- [@jucio.io/state](../../core) - Core state management system
|
|
318
|
+
- [@jucio.io/state/matcher](../matcher) - Path pattern matching
|
|
319
|
+
- [@jucio.io/state/on-change](../on-change) - Change listeners
|
|
320
|
+
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var w=Symbol("STATE_CONTEXT"),x=Symbol("MATCHER"),a="CREATED",c="DELETED",d="UPDATED";var A={[a]:c,[c]:a,[d]:d};function E(i){let{address:t,path:e,method:n,from:s,to:o,operation:f}=i;return{address:t,path:e,method:n,to:s,from:o,operation:g(f)}}function g(i){return A[i]}function h(i,t){let e=null,n=0;return typeof i=="function"?(e=i,typeof t=="number"&&(n=t)):typeof i=="number"&&(n=i),(...s)=>new Promise((o,f)=>{setTimeout(()=>{try{let l=e?e(...s):void 0;o(l)}catch(l){f(l)}},n)})}var u=class{static name=null;static options={};static configure(t){return t={...this.options,...t},{install:e=>this.install(e,t),name:this.name,options:t}}static install(t,e){e={...this.options,...e};let n=new this(t,e);return Object.defineProperty(n,"state",{value:t,writable:!1,configurable:!1}),Object.defineProperty(n,"options",{value:e,writable:!1,configurable:!1}),n}};var r=class{constructor(t=""){this.isMarker=!0,this.description=t}},p=class extends u{static name="history";static options={maxSize:100};#t=[];#u=!1;#o=new Set;#e=0;#n=0;#s=new Map;#i=!0;#r=!1;onUndoRedo=null;initialize(t){this.onUndoRedo=e=>t.apply(e)}#f(){this.#t=[new r("History Start")],this.#e=0,this.#u=!0,this.#s.clear(),this.#n=0,this.#r=!1}actions(t){return{undo:e=>this.undo(e),redo:e=>this.redo(e),canUndo:()=>this.canUndo(),canRedo:()=>this.canRedo(),batch:()=>this.batch(),commit:()=>this.commit(),onCommit:e=>this.onCommit(e),addMarker:e=>this.addMarker(e),size:()=>this.size(),start:()=>this.#f()}}#l=h(()=>{this.#h(),this.#r=!1});onStateChange(t,e,n){if(!this.#u||!this.#i||e.method==="apply")return;let s=this.#s.get(t.address),o=s?{...e,from:s.from}:e;this.#s.set(t.address,o),!n&&this.#n===0&&this.#a()}addMarker(t=""){this.#h(t),this.#e=this.#t.length-1}undo=h(t=>{if(!this.onUndoRedo)throw new Error("Undo/redo handler is required for undo operation");if(this.#h(),this.#e<=0)return!1;if(this.#i=!1,!(this.#t[this.#e]instanceof r))return console.warn("Expected to be at a marker for undo operation"),!1;let e=new Set,n=this.#e-1;for(;n>0;){let s=this.#t[n];if(s instanceof r)break;e.add(E(s)),n--}return this.#e=n,this.onUndoRedo(Array.from(e)),t&&typeof t=="function"&&t(),this.#i=!0,!0});redo=h(t=>{if(!this.onUndoRedo)throw new Error("Undo/redo handler is required for redo operation");if(this.#e>=this.#t.length-1)return!1;if(this.#i=!1,!(this.#t[this.#e]instanceof r))return console.warn("Expected to be at a marker for redo operation"),!1;let e=new Set;for(this.#e++;this.#e<this.#t.length;){let n=this.#t[this.#e];if(n instanceof r)break;e.add(n),this.#e++}return this.onUndoRedo(e),t&&typeof t=="function"&&t(),this.#i=!0,!0});canUndo(){return this.#e>0}canRedo(){return this.#e<this.#t.length-1}size(){return this.#t.length}batch(){return this.#n++,()=>{this.#n--}}commit(){return this.#n=this.#n-1<0?0:this.#n-1,this.#n===0&&this.#h(),this}pause(){this.#i=!1}resume(){this.#i=!0}onCommit(t){return this.#o.add(t),()=>{this.#o.delete(t)}}reset(){this.#t=[new r("History Start")],this.#o=new Set,this.#e=0,this.#n=0,this.#s=new Map,this.#i=!0,this.#r=!1,this.#u=!1,this.onUndoRedo=null}#a(){this.#r||(this.#r=!0,this.#l())}#h(t=""){if(this.#s.size===0)return;this.#e<this.#t.length-1&&this.#t.splice(this.#e+1);let e=Array.from(this.#s.values());for(this.#t.push(...e),this.#o.forEach(s=>s(e)),this.#t[this.#t.length-1]instanceof r||this.#t.push(new r(t||Date.now().toString()));this.#t.length>this.options.maxSize;)this.#t.shift();this.#s.clear(),this.#e=this.#t.length-1}};export{p as HistoryManager};
|
|
2
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../core/src/lib/TOKENS.js", "../../../core/src/lib/change.js", "../../../core/src/utils/defer.js", "../../../core/src/Plugin.js", "../src/HistoryManager.js"],
|
|
4
|
+
"sourcesContent": ["export const GLOBAL_TAG = '*';\nexport const STATE_CONTEXT = Symbol('STATE_CONTEXT');\nexport const MATCHER = Symbol('MATCHER');\nexport const CREATED = 'CREATED';\nexport const DELETED = 'DELETED';\nexport const UPDATED = 'UPDATED';\n\n// Marker type bitflags\nexport const MARKER_GLOBAL = 1; // 0b001\nexport const MARKER_SINGLE = 2; // 0b010\nexport const MARKER_MANY = 4; // 0b100\nexport const MARKER_EPHEMERAL = 8; // 0b1000\n\n// Comparison result constants\nexport const MATCH_EXACT = 0; // Markers are identical\nexport const MATCH_PARENT = 1; // controlMarker is child of comparedMarker (parent changed)\nexport const MATCH_CHILD = 2; // comparedMarker is child of controlMarker (child changed)\nexport const MATCH_NONE = -1; // No relationship", "import { GLOBAL_TAG, CREATED, DELETED, UPDATED } from './TOKENS.js';\nimport { dispatch } from './marker.js';\n\nexport const OPERATION_INVERSES = {\n [CREATED]: DELETED,\n [DELETED]: CREATED,\n [UPDATED]: UPDATED,\n}\n\n/**\n * Determines the operation type based on from/to values\n * \n * @private\n * @param {*} from - Previous value\n * @param {*} to - New value\n * @returns {'created'|'deleted'|'updated'} Operation type\n */\nfunction determineOperation(to, from) {\n if (from === undefined) return CREATED;\n if (to === undefined) return DELETED;\n return UPDATED;\n}\n\n/**\n * Creates a change object with the given parameters\n * \n * @param {string} method - Method that triggered the change\n * @param {*} from - Previous value\n * @param {*} to - New value\n * @returns {Object} Change object\n * @example\n * createChange('set', undefined, 'new value') \n * // => { method: 'set', operation: 'created', from: undefined, to: 'new value' }\n */\nexport function createChange(marker, method, to, from) {\n return dispatch(marker, {\n global: () => ({\n path: GLOBAL_TAG,\n method,\n operation: determineOperation(to, from),\n from,\n to\n }),\n path: (marker) => ({\n path: marker.path,\n method,\n operation: determineOperation(to, from),\n from,\n to\n }),\n });\n}\n\n/**\n * Inverts a change object for undo operations\n * \n * @param {Object} change - Change object to invert\n * @param {string} change.method - Original method\n * @param {string} change.operation - Original operation\n * @param {*} change.from - Original from value\n * @param {*} change.to - Original to value\n * @returns {Object} Inverted change object\n * @example\n * invertChange({ method: 'set', operation: 'created', from: undefined, to: 'value' })\n * // => { method: 'set', operation: 'deleted', from: 'value', to: undefined }\n */\nexport function invertChange(change) {\n const {address, path, method, from, to, operation } = change;\n return {\n address,\n path,\n method,\n to: from,\n from: to,\n operation: invertOperation(operation),\n }\n}\n\nexport function invertOperation(operation) {\n return OPERATION_INVERSES[operation];\n}\n\n/**\n * Checks if a change needs to be inverted based on the method\n * \n * @param {string} method - Method to check\n * @returns {boolean} True if the change should be inverted\n * @example\n * shouldInvertChange('undo') // => true\n * shouldInvertChange('set') // => false\n */\nexport function shouldInvertChange(method) {\n return ['undo', 'redo', 'stepBackward', 'stepForward'].includes(method);\n}", "export function defer(arg1, arg2) {\n let callback = null;\n let delay = 0;\n\n if (typeof arg1 === 'function') {\n callback = arg1;\n if (typeof arg2 === 'number') {\n delay = arg2;\n }\n } else if (typeof arg1 === 'number') {\n delay = arg1;\n }\n \n return (...args) => new Promise((resolve, reject) => {\n setTimeout(() => {\n try {\n const result = callback ? callback(...args) : undefined;\n resolve(result);\n } catch (error) {\n reject(error);\n }\n }, delay);\n });\n}\n", "export class Plugin {\n\n static name = null;\n static options = {};\n\n static configure(options) {\n options = {...this.options, ...options};\n return {\n install: (state) => this.install(state, options),\n name: this.name,\n options\n }\n }\n\n static install(state, options) {\n options = {...this.options, ...options};\n const pluginInstance = new this(state, options);\n\n Object.defineProperty(pluginInstance, 'state', {\n value: state,\n writable: false,\n configurable: false\n });\n \n Object.defineProperty(pluginInstance, 'options', {\n value: options,\n writable: false,\n configurable: false\n });\n\n \n return pluginInstance;\n }\n}\n\nexport default Plugin;", "import { invertChange } from \"@jucie-state/core/lib/change.js\";\nimport { defer } from \"@jucie-state/core/utils/defer.js\";\nimport { Plugin } from \"@jucie-state/core/Plugin\";\n\nclass Marker {\n constructor(description = '') {\n this.isMarker = true;\n this.description = description;\n }\n}\n\nexport class HistoryManager extends Plugin {\n static name = 'history';\n static options = {\n maxSize: 100\n };\n\n #changes = [];\n #started = false;\n #commitListeners = new Set();\n #cursor = 0;\n #batchDepth = 0;\n #pendingChanges = new Map();\n #isRecording = true;\n #commitScheduled = false;\n onUndoRedo = null;\n\n initialize(state) {\n this.onUndoRedo = (changes) => state.apply(changes);\n }\n\n #start() {\n // Reset history and set baseline at current state\n this.#changes = [new Marker('History Start')];\n this.#cursor = 0;\n this.#started = true;\n this.#pendingChanges.clear();\n this.#batchDepth = 0;\n this.#commitScheduled = false;\n }\n\n actions (state) {\n return {\n undo: (cb) => this.undo(cb),\n redo: (cb) => this.redo(cb),\n canUndo: () => this.canUndo(),\n canRedo: () => this.canRedo(),\n batch: () => this.batch(),\n commit: () => this.commit(),\n onCommit: (callback) => this.onCommit(callback),\n addMarker: (description) => this.addMarker(description),\n size: () => this.size(),\n start: () => this.#start()\n }\n }\n\n #deferredCommit = defer(() => {\n this.#commitPendingChanges();\n this.#commitScheduled = false;\n });\n\n onStateChange(marker, change, batching) {\n // Ignore all changes if history hasn't been started yet\n if (!this.#started) return;\n if (!this.#isRecording || change.method === 'apply') return;\n \n const existingChange = this.#pendingChanges.get(marker.address);\n const recordedChange = existingChange \n ? { ...change, from: existingChange.from } // Need to preserve original 'from'\n : change; // Can use original object directly\n this.#pendingChanges.set(marker.address, recordedChange);\n\n if (!batching && this.#batchDepth === 0) {\n this.#scheduleCommit();\n }\n }\n\n addMarker(description = '') {\n this.#commitPendingChanges(description);\n this.#cursor = this.#changes.length - 1;\n }\n\n undo = defer((cb) => {\n if (!this.onUndoRedo) {\n throw new Error('Undo/redo handler is required for undo operation');\n }\n\n this.#commitPendingChanges();\n\n // We should always have at least the initial marker\n if (this.#cursor <= 0) return false;\n this.#isRecording = false;\n \n // We should be at a marker\n if (!(this.#changes[this.#cursor] instanceof Marker)) {\n console.warn('Expected to be at a marker for undo operation');\n return false;\n }\n \n const changesToUndo = new Set();\n let tempCursor = this.#cursor - 1; // Don't modify the actual cursor yet\n \n // Collect changes until we hit the next marker\n while (tempCursor > 0) { // Changed from >= 0 to > 0 to preserve initial marker\n const changeEntry = this.#changes[tempCursor];\n \n if (changeEntry instanceof Marker) {\n break;\n }\n \n changesToUndo.add(invertChange(changeEntry));\n tempCursor--;\n }\n \n // Only update cursor after successfully collecting changes\n this.#cursor = tempCursor;\n \n // Apply the changes (already in correct order)\n this.onUndoRedo(Array.from(changesToUndo));\n \n if (cb && typeof cb === 'function') {\n cb();\n }\n\n this.#isRecording = true;\n return true;\n });\n\n redo = defer((cb) => {\n if (!this.onUndoRedo) {\n throw new Error('Undo/redo handler is required for redo operation');\n }\n\n if (this.#cursor >= this.#changes.length - 1) return false;\n this.#isRecording = false;\n \n // We should be at a marker\n if (!(this.#changes[this.#cursor] instanceof Marker)) {\n console.warn('Expected to be at a marker for redo operation');\n return false;\n }\n \n const changesToRedo = new Set();\n this.#cursor++; // Move past current marker\n \n // Collect changes until we hit the next marker\n while (this.#cursor < this.#changes.length) {\n const change = this.#changes[this.#cursor];\n \n if (change instanceof Marker) {\n break;\n }\n \n changesToRedo.add(change);\n this.#cursor++;\n }\n\n // Apply the changes in original order\n this.onUndoRedo(changesToRedo);\n \n if (cb && typeof cb === 'function') {\n cb();\n }\n\n this.#isRecording = true;\n return true;\n });\n\n canUndo() {\n return this.#cursor > 0;\n }\n\n canRedo() {\n return this.#cursor < this.#changes.length - 1;\n }\n\n size() {\n return this.#changes.length;\n }\n\n\n batch() {\n this.#batchDepth++;\n\n return () => {\n this.#batchDepth--;\n };\n }\n\n commit() {\n this.#batchDepth = this.#batchDepth - 1 < 0 ? 0 : this.#batchDepth - 1;\n\n if (this.#batchDepth === 0) {\n this.#commitPendingChanges();\n }\n \n return this;\n }\n\n pause() {\n this.#isRecording = false;\n }\n\n resume() {\n this.#isRecording = true;\n }\n\n onCommit(callback) {\n this.#commitListeners.add(callback);\n\n return () => {\n this.#commitListeners.delete(callback);\n };\n }\n\n reset() {\n this.#changes = [new Marker('History Start')];\n this.#commitListeners = new Set();\n this.#cursor = 0;\n this.#batchDepth = 0;\n this.#pendingChanges = new Map();\n this.#isRecording = true;\n this.#commitScheduled = false;\n this.#started = false;\n this.onUndoRedo = null;\n }\n\n #scheduleCommit() {\n if (!this.#commitScheduled) {\n this.#commitScheduled = true;\n this.#deferredCommit();\n }\n }\n\n #commitPendingChanges(description = '') {\n if (this.#pendingChanges.size === 0) return;\n\n // Truncate future changes if we're not at the end\n if (this.#cursor < this.#changes.length - 1) {\n this.#changes.splice(this.#cursor + 1);\n }\n\n // Add all pending changes individually\n const changes = Array.from(this.#pendingChanges.values());\n this.#changes.push(...changes);\n\n // Notify listeners\n this.#commitListeners.forEach(listener => listener(changes));\n \n // Add a marker automatically after the changes, but only if the last item isn't already a marker\n const lastItem = this.#changes[this.#changes.length - 1];\n \n if (!(lastItem instanceof Marker)) {\n this.#changes.push(new Marker(description || Date.now().toString()));\n }\n\n // Ensure the changes do not exceed maxSize\n while (this.#changes.length > this.options.maxSize) {\n this.#changes.shift(); // Remove the oldest change\n }\n\n // Clear pending changes\n this.#pendingChanges.clear();\n\n // Update current index\n this.#cursor = this.#changes.length - 1;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["STATE_CONTEXT", "MATCHER", "CREATED", "DELETED", "UPDATED", "OPERATION_INVERSES", "CREATED", "DELETED", "UPDATED", "invertChange", "change", "address", "path", "method", "from", "to", "operation", "invertOperation", "OPERATION_INVERSES", "defer", "arg1", "arg2", "callback", "delay", "args", "resolve", "reject", "result", "error", "Plugin", "options", "state", "pluginInstance", "Marker", "description", "HistoryManager", "Plugin", "#changes", "#started", "#commitListeners", "#cursor", "#batchDepth", "#pendingChanges", "#isRecording", "#commitScheduled", "state", "changes", "#start", "cb", "callback", "#deferredCommit", "defer", "#commitPendingChanges", "marker", "change", "batching", "existingChange", "recordedChange", "#scheduleCommit", "changesToUndo", "tempCursor", "changeEntry", "invertChange", "changesToRedo", "listener"]
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jucie.io/state-history",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "History management plugin for @jucie.io/state with undo/redo and change tracking",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/main.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/main.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "Tests run from root workspace"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"state",
|
|
19
|
+
"history",
|
|
20
|
+
"undo",
|
|
21
|
+
"redo",
|
|
22
|
+
"plugin"
|
|
23
|
+
],
|
|
24
|
+
"author": "Adrian Miller",
|
|
25
|
+
"license": "SEE LICENSE IN ../../../LICENSE",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/adrianjonmiller/state",
|
|
32
|
+
"directory": "state/plugins/history"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@jucie.io/state": "^1.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|