@jucie.io/state-history 1.0.13 → 1.0.14

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,12 +1,12 @@
1
1
  # @jucio.io/state/history
2
2
 
3
- History management plugin for @jucio.io/state that provides undo/redo functionality with markers, batching, and commit listeners.
3
+ History management plugin for @jucio.io/state that provides undo/redo functionality with markers, grouping, and commit listeners.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - ⏪ **Undo/Redo**: Full undo and redo support with automatic change tracking
8
8
  - 🏷️ **Markers**: Add descriptive markers to create logical undo/redo boundaries
9
- - 📦 **Batching**: Group multiple changes into a single undo/redo step
9
+ - 📦 **Grouping**: Group multiple changes into a single undo/redo step (can span multiple calls; commit when done)
10
10
  - 🔔 **Commit Listeners**: React to history commits
11
11
  - 🎯 **Smart Change Consolidation**: Automatically merges changes to the same path
12
12
  - 📏 **Configurable History Size**: Limit the number of stored changes
@@ -106,34 +106,42 @@ if (state.history.canRedo()) {
106
106
 
107
107
  **Returns:** `boolean`
108
108
 
109
- ### Batching
109
+ ### Grouping
110
110
 
111
- #### `batch()`
111
+ #### `group(callback?)`
112
112
 
113
- Start a batch to group multiple changes into a single undo/redo step.
113
+ Start grouping changes into a single undo/redo step. Grouping can span multiple calls; call `commit()` (or the returned function) when done. Pass a function to run in grouping mode and commit when it returns (quick single-block use).
114
114
 
115
115
  ```javascript
116
- const endBatch = state.history.batch();
117
-
116
+ // Span multiple calls
117
+ state.history.group();
118
118
  state.set(['user', 'name'], 'Alice');
119
119
  state.set(['user', 'age'], 30);
120
- state.set(['user', 'email'], 'alice@example.com');
120
+ state.history.commit(); // All changes are now a single undo/redo step
121
121
 
122
- endBatch(); // All three changes are now a single undo/redo step
122
+ // Or use the returned function
123
+ const endGroup = state.history.group();
124
+ state.set(['user', 'email'], 'alice@example.com');
125
+ endGroup();
123
126
 
124
- state.history.undo(); // Undoes all three changes at once
127
+ // Quick single-block: pass a callback
128
+ state.history.group(() => {
129
+ state.set(['user', 'name'], 'Alice');
130
+ state.set(['user', 'age'], 30);
131
+ });
132
+ state.history.undo(); // Undoes both changes at once
125
133
  ```
126
134
 
127
- **Returns:** `Function` - Call the returned function to end the batch
135
+ **Returns:** `HistoryManager` (for chaining) if a callback was provided; otherwise `Function` to call when done
128
136
 
129
- #### `commit()`
137
+ #### `commit(description?)`
130
138
 
131
- Manually commit pending changes and end the current batch.
139
+ Manually commit pending changes and end the current group.
132
140
 
133
141
  ```javascript
134
- state.history.batch();
142
+ state.history.group();
135
143
  state.set(['data', 'value'], 1);
136
- state.history.commit(); // Forces commit
144
+ state.history.commit('Initial value'); // Commits and ends grouping
137
145
  ```
138
146
 
139
147
  **Returns:** `HistoryManager` instance (for chaining)
@@ -219,11 +227,11 @@ state.install(HistoryManager.configure({
219
227
 
220
228
  ## Advanced Usage
221
229
 
222
- ### Complex Batching with Markers
230
+ ### Grouping with Markers
223
231
 
224
232
  ```javascript
225
- // Start a complex operation
226
- state.history.batch();
233
+ // Start a complex operation (grouping can span multiple calls)
234
+ state.history.group();
227
235
  state.history.addMarker('Start user registration');
228
236
 
229
237
  state.set(['user', 'name'], 'Alice');
@@ -270,10 +278,10 @@ state.plugins.history.reset();
270
278
  ```javascript
271
279
  // Track form edits
272
280
  function handleFieldChange(field, value) {
273
- const endBatch = state.history.batch();
281
+ state.history.group();
274
282
  state.set(['form', field], value);
275
283
  state.history.addMarker(`Update ${field}`);
276
- endBatch();
284
+ state.history.commit();
277
285
  }
278
286
 
279
287
  // Implement undo/redo buttons
@@ -290,7 +298,7 @@ function handleUndo() {
290
298
 
291
299
  ```javascript
292
300
  function performComplexOperation() {
293
- const endBatch = state.history.batch();
301
+ state.history.group();
294
302
 
295
303
  // Step 1: Update user
296
304
  state.set(['user', 'status'], 'processing');
@@ -301,7 +309,7 @@ function performComplexOperation() {
301
309
  // Step 3: Update timestamp
302
310
  state.set(['lastUpdate'], Date.now());
303
311
 
304
- endBatch();
312
+ state.history.commit();
305
313
  state.history.addMarker('Complex operation completed');
306
314
  }
307
315
 
package/dist/main.js CHANGED
@@ -1,2 +1,2 @@
1
- var f=Object.defineProperty;var e=(r,t)=>f(r,"name",{value:t,configurable:!0});import{invertChange as c}from"@jucie.io/state/lib/change.js";import{defer as o}from"@jucie.io/state/utils/defer.js";import{Plugin as u}from"@jucie.io/state/Plugin";var h=class{static{e(this,"Marker")}constructor(t=""){this.isMarker=!0,this.description=t}},a=class extends u{static name="history";static options={maxSize:100};#t=[];#a=!1;#r=new Set;#i=0;#e=0;#h=new Map;#s=!0;#n=!1;onUndoRedo=null;initialize(t){this.onUndoRedo=i=>t.apply(i)}#d(){this.#t=[new h("History Start")],this.#i=0,this.#a=!0,this.#h.clear(),this.#e=0,this.#n=!1}actions(t){return{undo:e(i=>this.undo(i),"undo"),redo:e(i=>this.redo(i),"redo"),canUndo:e(()=>this.canUndo(),"canUndo"),canRedo:e(()=>this.canRedo(),"canRedo"),batch:e(()=>this.batch(),"batch"),commit:e(()=>this.commit(),"commit"),onCommit:e(i=>this.onCommit(i),"onCommit"),addMarker:e(i=>this.addMarker(i),"addMarker"),size:e(()=>this.size(),"size"),start:e(()=>this.#d(),"start")}}#f=o(()=>{this.#o(),this.#n=!1});onStateChange(t,i,s){if(!this.#a||!this.#s||i.method==="apply")return;let n=this.#h.get(t.address),d=n?{...i,from:n.from}:i;this.#h.set(t.address,d),!s&&this.#e===0&&this.#c()}addMarker(t=""){this.#o(t),this.#i=this.#t.length-1}undo=o(t=>{if(!this.onUndoRedo)throw new Error("Undo/redo handler is required for undo operation");if(this.#o(),this.#i<=0)return!1;if(this.#s=!1,!(this.#t[this.#i]instanceof h))return console.warn("Expected to be at a marker for undo operation"),!1;let i=new Set,s=this.#i-1;for(;s>0;){let n=this.#t[s];if(n instanceof h)break;i.add(c(n)),s--}return this.#i=s,this.onUndoRedo(Array.from(i)),t&&typeof t=="function"&&t(),this.#s=!0,!0});redo=o(t=>{if(!this.onUndoRedo)throw new Error("Undo/redo handler is required for redo operation");if(this.#i>=this.#t.length-1)return!1;if(this.#s=!1,!(this.#t[this.#i]instanceof h))return console.warn("Expected to be at a marker for redo operation"),!1;let i=new Set;for(this.#i++;this.#i<this.#t.length;){let s=this.#t[this.#i];if(s instanceof h)break;i.add(s),this.#i++}return this.onUndoRedo(i),t&&typeof t=="function"&&t(),this.#s=!0,!0});canUndo(){return this.#i>0}canRedo(){return this.#i<this.#t.length-1}size(){return this.#t.length}batch(){return this.#e++,()=>{this.#e--}}commit(){return this.#e=this.#e-1<0?0:this.#e-1,this.#e===0&&this.#o(),this}pause(){this.#s=!1}resume(){this.#s=!0}onCommit(t){return this.#r.add(t),()=>{this.#r.delete(t)}}reset(){this.#t=[new h("History Start")],this.#r=new Set,this.#i=0,this.#e=0,this.#h=new Map,this.#s=!0,this.#n=!1,this.#a=!1,this.onUndoRedo=null}#c(){this.#n||(this.#n=!0,this.#f())}#o(t=""){if(this.#h.size===0)return;this.#i<this.#t.length-1&&this.#t.splice(this.#i+1);let i=Array.from(this.#h.values());for(this.#t.push(...i),this.#r.forEach(n=>n(i)),this.#t[this.#t.length-1]instanceof h||this.#t.push(new h(t||Date.now().toString()));this.#t.length>this.options.maxSize;)this.#t.shift();this.#h.clear(),this.#i=this.#t.length-1}};export{a as HistoryManager};
1
+ var f=Object.defineProperty;var i=(n,t)=>f(n,"name",{value:t,configurable:!0});import{invertChange as u}from"@jucie.io/state/lib/change.js";import{defer as h}from"@jucie.io/state/utils/defer.js";import{Plugin as l}from"@jucie.io/state/Plugin";var o=class{static{i(this,"Marker")}constructor(t=""){this.isMarker=!0,this.description=t}},a=class extends l{static name="history";static options={maxSize:100};#t=[];#a=!1;#n=new Set;#e=0;#o=!1;#s=new Map;#i=!0;#r=!1;onUndoRedo=null;initialize(t){this.onUndoRedo=e=>t.apply(e)}#d(){this.#t=[new o("History Start")],this.#e=0,this.#a=!0,this.#s.clear(),this.#o=!1,this.#r=!1}actions(t){return{undo:i(e=>this.undo(e),"undo"),redo:i(e=>this.redo(e),"redo"),canUndo:i(()=>this.canUndo(),"canUndo"),canRedo:i(()=>this.canRedo(),"canRedo"),group:i(e=>this.group(e),"group"),commit:i(e=>this.commit(e),"commit"),onCommit:i(e=>this.onCommit(e),"onCommit"),addMarker:i(e=>this.addMarker(e),"addMarker"),size:i(()=>this.size(),"size"),start:i(()=>this.#d(),"start")}}#f=h(()=>{this.#h(),this.#r=!1});onStateChange(t,e,s){if(!this.#a||!this.#i||e.method==="apply")return;let r=this.#s.get(t.address),d=r?{...e,from:r.from}:e;this.#s.set(t.address,d),!s&&!this.#o&&this.#u()}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 o))return console.warn("Expected to be at a marker for undo operation"),!1;let e=new Set,s=this.#e-1;for(;s>0;){let r=this.#t[s];if(r instanceof o)break;e.add(u(r)),s--}return this.#e=s,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 o))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 s=this.#t[this.#e];if(s instanceof o)break;e.add(s),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}group(t){return this.#o=!0,typeof t=="function"?(t(this),this.commit()):()=>this.commit()}commit(t=""){return this.#h(t),this.#o=!1,this}pause(){this.#i=!1}resume(){this.#i=!0}onCommit(t){return this.#n.add(t),()=>{this.#n.delete(t)}}reset(){this.#t=[new o("History Start")],this.#n=new Set,this.#e=0,this.#o=!1,this.#s=new Map,this.#i=!0,this.#r=!1,this.#a=!1,this.onUndoRedo=null}#u(){this.#r||(this.#r=!0,this.#f())}#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.#n.forEach(r=>r(e)),this.#t[this.#t.length-1]instanceof o||this.#t.push(new o(t||Date.now().toString()));this.#t.length>this.options.maxSize;)this.#t.shift();this.#s.clear(),this.#e=this.#t.length-1}};export{a as HistoryManager};
2
2
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/HistoryManager.js"],
4
- "sourcesContent": ["import { invertChange } from \"@jucie.io/state/lib/change.js\";\nimport { defer } from \"@jucie.io/state/utils/defer.js\";\nimport { Plugin } from \"@jucie.io/state/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": ["invertChange", "defer", "Plugin", "Marker", "__name", "description", "HistoryManager", "#changes", "#started", "#commitListeners", "#cursor", "#batchDepth", "#pendingChanges", "#isRecording", "#commitScheduled", "state", "changes", "#start", "cb", "callback", "#deferredCommit", "#commitPendingChanges", "marker", "change", "batching", "existingChange", "recordedChange", "#scheduleCommit", "changesToUndo", "tempCursor", "changeEntry", "changesToRedo", "listener"]
4
+ "sourcesContent": ["import { invertChange } from \"@jucie.io/state/lib/change.js\";\nimport { defer } from \"@jucie.io/state/utils/defer.js\";\nimport { Plugin } from \"@jucie.io/state/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 #grouping = false;\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.#grouping = false;\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 group: (fn) => this.group(fn),\n commit: (description) => this.commit(description),\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, grouping) {\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 (!grouping && !this.#grouping) {\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 * Start grouping changes into a single undo/redo step.\n * Grouping can span multiple calls; call commit() when done (or use the returned function).\n * Optional: pass a callback to run in grouping mode and commit when it returns (quick single-block use).\n */\n group(fn) {\n this.#grouping = true;\n\n if (typeof fn === 'function') {\n fn(this);\n return this.commit();\n }\n\n return () => this.commit();\n }\n\n commit(description = '') {\n this.#commitPendingChanges(description);\n this.#grouping = false;\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.#grouping = false;\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": ["invertChange", "defer", "Plugin", "Marker", "__name", "description", "HistoryManager", "#changes", "#started", "#commitListeners", "#cursor", "#grouping", "#pendingChanges", "#isRecording", "#commitScheduled", "state", "changes", "#start", "cb", "fn", "callback", "#deferredCommit", "#commitPendingChanges", "marker", "change", "grouping", "existingChange", "recordedChange", "#scheduleCommit", "changesToUndo", "tempCursor", "changeEntry", "changesToRedo", "listener"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jucie.io/state-history",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "History management plugin for @jucie.io/state with undo/redo and change tracking",
5
5
  "type": "module",
6
6
  "main": "./dist/main.js",