@weasel-js/history 1.0.4 → 1.2.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/dist/index.d.ts CHANGED
@@ -88,6 +88,8 @@ interface SerializedHistoryEntry {
88
88
  label: string;
89
89
  forwardOps: SerializedOp[];
90
90
  baseOps: SerializedOp[];
91
+ selectionBefore?: readonly string[];
92
+ selectionAfter?: readonly string[];
91
93
  }
92
94
  /** Snapshot of an entire `History` instance. Designed to live alongside the
93
95
  * scene snapshot in IDB so a reload restores the undo / redo stacks to
@@ -128,6 +130,10 @@ interface HistoryEntry {
128
130
  * contribute nothing. May be `undefined` for deserialized entries
129
131
  * restored from an older snapshot that predates this field. */
130
132
  touchedIds?: ReadonlySet<string>;
133
+ /** Selection restored when this entry is undone. */
134
+ selectionBefore?: readonly string[];
135
+ /** Selection restored when this entry is redone. */
136
+ selectionAfter?: readonly string[];
131
137
  }
132
138
  /** Op-batched undo/redo controller returned by `createHistory`. */
133
139
  interface History {
@@ -179,7 +185,7 @@ interface History {
179
185
  * Unlike `applyOps`, does NOT call `op.apply()`. Used by Journal.commit
180
186
  * to flush a session's net forward ops to the parent as one entry without
181
187
  * re-mutating the scene. */
182
- recordEntry(ops: Op[], label: string): void;
188
+ recordEntry(ops: Op[], label: string, options?: RecordEntryOptions): void;
183
189
  /** Concatenated forwardOps of every undo-stack entry, in order. Snapshot
184
190
  * of "what changes are currently applied via this history" — useful for
185
191
  * Journal.commit to flush to a parent, and for any caller that wants to
@@ -202,6 +208,22 @@ interface History {
202
208
  * to resume or discard before calling this. */
203
209
  resumeJournal(journal: Journal): void;
204
210
  }
211
+ /** Read/write access to whatever holds the caller's selection. The engine
212
+ * stores the ids it is handed and hands them back on the way past; it
213
+ * attaches no meaning to them and never records a selection change as an
214
+ * entry of its own. */
215
+ interface HistorySelection {
216
+ get(): readonly string[];
217
+ set(ids: readonly string[]): void;
218
+ }
219
+ /** Options for `recordEntry`. */
220
+ interface RecordEntryOptions {
221
+ /** Selection as of before the already-applied ops ran. `recordEntry` is
222
+ * called after the fact, so the live selection has moved on by then and
223
+ * the engine cannot sample it — a caller that wants undo to restore the
224
+ * selection captures it when the batch opens and passes it here. */
225
+ selectionBefore?: readonly string[];
226
+ }
205
227
  /** Options for `createHistory`. */
206
228
  interface CreateHistoryOptions {
207
229
  /** Window (ms) within which a new entry may merge into the previous one
@@ -214,6 +236,10 @@ interface CreateHistoryOptions {
214
236
  coalesceWindowMs?: number;
215
237
  /** Clock injection point for tests. Defaults to `Date.now`. */
216
238
  now?: () => number;
239
+ /** Where the caller's selection lives. Supplied, each entry records the
240
+ * selection around it and undo / redo / goto restore it; omitted, the
241
+ * engine never reads or writes selection at all. */
242
+ selection?: HistorySelection;
217
243
  /** Maximum undo-stack depth. When a push overflows the cap the oldest
218
244
  * entry is evicted (reported via `onEvict`) and can no longer be undone.
219
245
  * `0` disables the undo stack entirely — every push is evicted
@@ -255,4 +281,4 @@ interface HistoryLogger {
255
281
  /** Build an op-batched undo/redo `History`. The adapter is passed to each op's `apply`/`invert`. */
256
282
  declare function createHistory(adapter: unknown, options?: CreateHistoryOptions): History;
257
283
 
258
- export { type BeginJournalOptions, type CreateHistoryOptions, type EvictedEntry, type History, type HistoryEntry, type HistoryLogger, type Journal, type Op, type SerializedHistory, type SerializedHistoryEntry, type SerializedOp, createHistory };
284
+ export { type BeginJournalOptions, type CreateHistoryOptions, type EvictedEntry, type History, type HistoryEntry, type HistoryLogger, type HistorySelection, type Journal, type Op, type RecordEntryOptions, type SerializedHistory, type SerializedHistoryEntry, type SerializedOp, createHistory };
package/dist/index.js CHANGED
@@ -5,9 +5,10 @@ function _resumeJournalInternal(j) {
5
5
  if (!r) throw new Error("Journal is not resumable (already committed or cancelled)");
6
6
  r();
7
7
  }
8
- function createJournalInternal(parent, adapter, opts, onClose) {
9
- const inner = createHistory(adapter);
8
+ function createJournalInternal(parent, adapter, opts, onClose, selection) {
9
+ const inner = createHistory(adapter, selection ? { selection } : {});
10
10
  const forkedAtEntryId = parent.currentEntryId();
11
+ const selectionBefore = selection ? [...selection.get()] : void 0;
11
12
  let state = "active";
12
13
  const targetId = opts.targetId;
13
14
  const journal = {
@@ -38,7 +39,7 @@ function createJournalInternal(parent, adapter, opts, onClose) {
38
39
  if (state !== "active") throw new Error("Journal is not active");
39
40
  const netOps = inner.allForwardOps();
40
41
  if (netOps.length > 0) {
41
- parent.recordEntry(netOps, label);
42
+ parent.recordEntry(netOps, label, selectionBefore ? { selectionBefore } : void 0);
42
43
  }
43
44
  state = "closed";
44
45
  RESUMERS.delete(journal);
@@ -80,6 +81,7 @@ function createHistory(adapter, options = {}) {
80
81
  const historyLimit = Math.max(0, options.historyLimit ?? Infinity);
81
82
  const onEvict = options.onEvict;
82
83
  const customRebuild = options.rebuildOp;
84
+ const selection = options.selection;
83
85
  const logger = options.debug ?? SILENT;
84
86
  let nextEntryId = 1;
85
87
  let version = 0;
@@ -122,6 +124,21 @@ function createHistory(adapter, options = {}) {
122
124
  function invertEntry(entry) {
123
125
  return [...entry.baseOps].reverse().map((op) => op.invert());
124
126
  }
127
+ function readSelection() {
128
+ return selection ? [...selection.get()] : void 0;
129
+ }
130
+ function stepBack(entry) {
131
+ const leaving = readSelection();
132
+ if (leaving) entry.selectionAfter = leaving;
133
+ applyOps(invertEntry(entry));
134
+ if (selection && entry.selectionBefore) selection.set(entry.selectionBefore);
135
+ }
136
+ function stepForward(entry) {
137
+ const leaving = readSelection();
138
+ if (leaving) entry.selectionBefore = leaving;
139
+ applyOps(entry.forwardOps);
140
+ if (selection && entry.selectionAfter) selection.set(entry.selectionAfter);
141
+ }
125
142
  function canCoalesce(top, incoming) {
126
143
  if (coalesceWindowMs <= 0) return false;
127
144
  if (version !== coalesceAnchorVersion) return false;
@@ -145,6 +162,7 @@ function createHistory(adapter, options = {}) {
145
162
  }
146
163
  function pushOrCoalesce(ops, label) {
147
164
  if (ops.length === 0) return;
165
+ const selectionBefore = readSelection();
148
166
  const anyMutated = applyOpsAndDetectMutation(ops);
149
167
  if (!anyMutated) {
150
168
  logger.warn(
@@ -168,7 +186,15 @@ function createHistory(adapter, options = {}) {
168
186
  return;
169
187
  }
170
188
  logger.log(`push '${label}' (${ops.length} ops)`);
171
- undoStack.push({ id: nextEntryId++, forwardOps: ops, baseOps: ops, label, timestamp: now(), touchedIds: incoming });
189
+ undoStack.push({
190
+ id: nextEntryId++,
191
+ forwardOps: ops,
192
+ baseOps: ops,
193
+ label,
194
+ timestamp: now(),
195
+ touchedIds: incoming,
196
+ ...selectionBefore ? { selectionBefore } : {}
197
+ });
172
198
  dropRedo();
173
199
  enforceLimit();
174
200
  bump();
@@ -184,14 +210,14 @@ function createHistory(adapter, options = {}) {
184
210
  undo() {
185
211
  const entry = undoStack.pop();
186
212
  if (!entry) return;
187
- applyOps(invertEntry(entry));
213
+ stepBack(entry);
188
214
  redoStack.push(entry);
189
215
  bump();
190
216
  },
191
217
  redo() {
192
218
  const entry = redoStack.pop();
193
219
  if (!entry) return;
194
- applyOps(entry.forwardOps);
220
+ stepForward(entry);
195
221
  undoStack.push(entry);
196
222
  bump();
197
223
  },
@@ -206,7 +232,14 @@ function createHistory(adapter, options = {}) {
206
232
  if (had) bump();
207
233
  },
208
234
  entries() {
209
- const toView = (e) => ({ id: e.id, label: e.label, timestamp: e.timestamp, touchedIds: e.touchedIds });
235
+ const toView = (e) => ({
236
+ id: e.id,
237
+ label: e.label,
238
+ timestamp: e.timestamp,
239
+ touchedIds: e.touchedIds,
240
+ ...e.selectionBefore ? { selectionBefore: e.selectionBefore } : {},
241
+ ...e.selectionAfter ? { selectionAfter: e.selectionAfter } : {}
242
+ });
210
243
  return {
211
244
  undo: undoStack.map(toView),
212
245
  redo: [...redoStack].reverse().map(toView)
@@ -217,13 +250,13 @@ function createHistory(adapter, options = {}) {
217
250
  if (n < 0 || n > total) return;
218
251
  while (undoStack.length > n) {
219
252
  const entry = undoStack.pop();
220
- applyOps(invertEntry(entry));
253
+ stepBack(entry);
221
254
  redoStack.push(entry);
222
255
  }
223
256
  while (undoStack.length < n) {
224
257
  const entry = redoStack.pop();
225
258
  if (!entry) break;
226
- applyOps(entry.forwardOps);
259
+ stepForward(entry);
227
260
  undoStack.push(entry);
228
261
  }
229
262
  bump();
@@ -250,9 +283,17 @@ function createHistory(adapter, options = {}) {
250
283
  droppedEntries: dropped
251
284
  };
252
285
  },
253
- recordEntry(ops, label) {
286
+ recordEntry(ops, label, options2 = {}) {
254
287
  if (ops.length === 0) return;
255
- undoStack.push({ id: nextEntryId++, forwardOps: ops, baseOps: ops, label, timestamp: now(), touchedIds: touchedIdsFromOps(ops) });
288
+ undoStack.push({
289
+ id: nextEntryId++,
290
+ forwardOps: ops,
291
+ baseOps: ops,
292
+ label,
293
+ timestamp: now(),
294
+ touchedIds: touchedIdsFromOps(ops),
295
+ ...options2.selectionBefore ? { selectionBefore: [...options2.selectionBefore] } : {}
296
+ });
256
297
  dropRedo();
257
298
  enforceLimit();
258
299
  bump();
@@ -273,7 +314,7 @@ function createHistory(adapter, options = {}) {
273
314
  }
274
315
  const j = createJournalInternal(this, adapter, opts, () => {
275
316
  activeJournal = null;
276
- });
317
+ }, selection);
277
318
  activeJournal = j;
278
319
  return j;
279
320
  },
@@ -337,7 +378,14 @@ function entryToSerial(e, logger) {
337
378
  }
338
379
  baseOps.push(s);
339
380
  }
340
- return { id: e.id, label: e.label, forwardOps, baseOps };
381
+ return {
382
+ id: e.id,
383
+ label: e.label,
384
+ forwardOps,
385
+ baseOps,
386
+ ...e.selectionBefore ? { selectionBefore: e.selectionBefore } : {},
387
+ ...e.selectionAfter ? { selectionAfter: e.selectionAfter } : {}
388
+ };
341
389
  }
342
390
  function placeholderOp(name, args, label) {
343
391
  const op = {
@@ -368,7 +416,9 @@ function serialToEntry(se, custom, logger) {
368
416
  timestamp: 0,
369
417
  // Re-derive touchedIds from the rebuilt ops rather than trying to
370
418
  // round-trip the Set through the serialized form (Sets aren't JSON-safe).
371
- touchedIds: touchedIdsFromOps(forwardOps)
419
+ touchedIds: touchedIdsFromOps(forwardOps),
420
+ ...se.selectionBefore ? { selectionBefore: [...se.selectionBefore] } : {},
421
+ ...se.selectionAfter ? { selectionAfter: [...se.selectionAfter] } : {}
372
422
  };
373
423
  }
374
424
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/journal.ts","../src/history.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Op } from './op';\nimport { createHistory, type History, type HistoryEntry } from './history';\n\nconst RESUMERS = new WeakMap<Journal, () => void>();\n\n/** Called by `history.resumeJournal`. Not part of the public API. */\nexport function _resumeJournalInternal(j: Journal): void {\n const r = RESUMERS.get(j);\n if (!r) throw new Error('Journal is not resumable (already committed or cancelled)');\n r();\n}\n\n/** Options for `history.beginJournal()`. */\nexport interface BeginJournalOptions {\n /** Label for the single parent-history entry the journal flushes on commit. */\n label: string;\n /** Caller-supplied tag naming what this journal is scoped to — typically the\n * id of the node being edited. The history layer only carries it; callers\n * read it back off the journal to decide whether a suspended journal\n * matches what they are about to edit. */\n targetId?: string;\n}\n\n/**\n * A scoped sub-history forked from a `History`, opened by\n * `history.beginJournal()`. Applies, undoes and redoes against the same\n * adapter as its parent, but keeps its entries to itself: `commit` flushes the\n * journal's net forward ops to the parent as one entry, `cancel` rewinds them\n * and contributes nothing. Use it when a self-contained editing session (a\n * text edit, a modal drag) should collapse to a single step in the parent's\n * undo stack while still offering undo *within* the session.\n *\n * A journal is active, suspended or closed. `commit` and `cancel` are\n * terminal; `suspend` lets the parent be used again and can be reversed with\n * `history.resumeJournal()`. Every mutating method throws when the journal is\n * not active.\n */\nexport interface Journal {\n readonly targetId: string | undefined;\n readonly forkedAtEntryId: number;\n\n // Same operational surface as History\n applyBatch(ops: Op[], label: string): void;\n undo(): void;\n redo(): void;\n canUndo(): boolean;\n canRedo(): boolean;\n entries(): { undo: HistoryEntry[]; redo: HistoryEntry[] };\n\n // Lifecycle\n commit(label: string): void;\n cancel(): void;\n suspend(): void;\n isActive(): boolean;\n}\n\n/** Internal factory used by `createHistory`'s `beginJournal` method.\n * Not exported via the package's `index.ts` — callers go through\n * `history.beginJournal()`. */\nexport function createJournalInternal(\n parent: History,\n adapter: unknown,\n opts: BeginJournalOptions,\n onClose?: () => void,\n): Journal {\n const inner = createHistory(adapter);\n const forkedAtEntryId = parent.currentEntryId();\n type State = 'active' | 'suspended' | 'closed';\n let state: State = 'active';\n const targetId = opts.targetId;\n\n const journal: Journal = {\n targetId,\n forkedAtEntryId,\n\n applyBatch(ops: Op[], label: string): void {\n if (state !== 'active') throw new Error('Journal is not active');\n inner.applyOps(ops, label);\n },\n undo(): void {\n if (state !== 'active') throw new Error('Journal is not active');\n inner.undo();\n },\n redo(): void {\n if (state !== 'active') throw new Error('Journal is not active');\n inner.redo();\n },\n canUndo(): boolean {\n return inner.canUndo();\n },\n canRedo(): boolean {\n return inner.canRedo();\n },\n entries() {\n return inner.entries();\n },\n commit(label: string): void {\n if (state !== 'active') throw new Error('Journal is not active');\n const netOps = inner.allForwardOps();\n if (netOps.length > 0) {\n parent.recordEntry(netOps, label);\n }\n state = 'closed';\n RESUMERS.delete(journal);\n onClose?.();\n },\n cancel(): void {\n if (state !== 'active') throw new Error('Journal is not active');\n inner.goto(0);\n state = 'closed';\n RESUMERS.delete(journal);\n onClose?.();\n },\n suspend(): void {\n if (state !== 'active') throw new Error('Journal is not active');\n state = 'suspended';\n onClose?.();\n },\n isActive(): boolean {\n return state === 'active';\n },\n };\n\n RESUMERS.set(journal, () => {\n if (state !== 'suspended') throw new Error('Journal is not suspended');\n state = 'active';\n });\n\n return journal;\n}\n","import type { Op } from './op';\nimport { createJournalInternal, _resumeJournalInternal, type Journal, type BeginJournalOptions } from './journal';\n\ninterface Entry {\n /** Monotonic id assigned at first push. Stable across coalesce merges\n * (a merged entry keeps the original id) so UI lists keyed on `id` don't\n * flicker when the underlying entry mutates. */\n id: number;\n /** Forward ops — applied on redo, reflect the latest to-state after any\n * coalescing. Diverges from `baseOps` only after a coalesce. */\n forwardOps: Op[];\n /** Original ops at first push — their `.invert()` is what undo replays.\n * Preserved across coalesces so undo always returns to the original\n * pre-edit state, no matter how many coalesces happened. */\n baseOps: Op[];\n label: string;\n /** ms timestamp at last push or coalesce; used to gate the coalesce window. */\n timestamp: number;\n /** Node ids touched by ops in this entry. See `HistoryEntry.touchedIds`. */\n touchedIds: ReadonlySet<string>;\n}\n\n/** Wire form of a single op inside a serialized history. The pair\n * `(name, args)` reconstructs a live `Op` via the op-factory registry. */\nexport interface SerializedOp {\n name: string;\n args: unknown;\n}\n\n/** Wire form of one history entry. `forwardOps` / `baseOps` mirror the\n * in-memory entry's fields (see `Entry` above) but only carry the\n * serializable `(name, args)` projection of each op. */\nexport interface SerializedHistoryEntry {\n id: number;\n label: string;\n forwardOps: SerializedOp[];\n baseOps: SerializedOp[];\n}\n\n/** Snapshot of an entire `History` instance. Designed to live alongside the\n * scene snapshot in IDB so a reload restores the undo / redo stacks to\n * exactly where they were. */\nexport interface SerializedHistory {\n version: 1;\n undoStack: SerializedHistoryEntry[];\n /** Stored newest-first, mirroring the in-memory stack so a deserialized\n * history matches the original's `entries().redo` ordering. */\n redoStack: SerializedHistoryEntry[];\n nextEntryId: number;\n /** Entries dropped because at least one of their ops lacked a `name`\n * and therefore couldn't round-trip through the op-factory registry.\n * Always present (zero when nothing was dropped) so callers can detect\n * loss without parsing the debug log. */\n droppedEntries: number;\n}\n\n/** Snapshot of an entry handed to `onEvict` when it permanently leaves the\n * reachable stacks. Ops are live references — read `name`/`args`, don't\n * mutate. */\nexport interface EvictedEntry {\n id: number;\n label: string;\n forwardOps: readonly Op[];\n baseOps: readonly Op[];\n}\n\n/** Read-only view of a history entry exposed via `History.entries()`. */\nexport interface HistoryEntry {\n /** Stable monotonic id (preserved across coalesce merges). */\n id: number;\n /** Human-readable label (the `label` arg passed to `applyOps`). */\n label: string;\n /** Push/last-coalesce timestamp (ms). */\n timestamp: number;\n /** Set of node ids touched by any op in this entry. Populated from ops\n * whose `args` carry an `id` field (transform, setPath, reparent) or a\n * `node.id` field (insert, delete). Ops without a recognisable id field\n * contribute nothing. May be `undefined` for deserialized entries\n * restored from an older snapshot that predates this field. */\n touchedIds?: ReadonlySet<string>;\n}\n\n/** Op-batched undo/redo controller returned by `createHistory`. */\nexport interface History {\n apply(op: Op, label?: string): void;\n applyOps(ops: Op[], label: string): void;\n undo(): void;\n redo(): void;\n canUndo(): boolean;\n canRedo(): boolean;\n /** Number of entries on the undo stack (O(1); `entries().undo.length`\n * without materializing the views). */\n undoDepth(): number;\n /** Number of entries on the redo stack (O(1)). */\n redoDepth(): number;\n clear(): void;\n /** Snapshot of the current undo + redo stacks. `undo` is oldest→newest\n * (i.e. the last element is what `undo()` would pop next); `redo` is\n * also oldest→newest from the user's perspective (i.e. the *first*\n * element is what `redo()` would pop next — see implementation note).\n * Callers should treat the arrays as immutable. */\n entries(): { undo: HistoryEntry[]; redo: HistoryEntry[] };\n /** Walk the history forward/back until exactly `n` entries are on the\n * undo stack (0 ≤ n ≤ entries().undo.length + entries().redo.length).\n * Equivalent to repeated `undo()`/`redo()` calls but doesn't bother\n * rebuilding entry snapshots between steps. No-op if already at `n`. */\n goto(n: number): void;\n /** Monotonic counter bumped on every push/undo/redo/clear/coalesce.\n * Cheap to read; callers use it as a React dep to detect changes. */\n getVersion(): number;\n /** Subscribe to history changes. Fires after every push/undo/redo/\n * clear/coalesce. Returns an unsubscribe fn. */\n subscribe(listener: () => void): () => void;\n /** Snapshot the undo + redo stacks in a structured-clone-safe form.\n * Entries whose ops aren't all kit-registered (i.e. any op missing a\n * `name`) are dropped from the snapshot with a debug-level log — they\n * can't round-trip, so we omit them rather than emit a half-restorable\n * entry. The in-memory stacks aren't modified. */\n serialize(): SerializedHistory;\n /** Replace the current undo + redo stacks with the deserialized contents\n * of `snapshot`. Ops are rebuilt via the `rebuildOp` option when\n * provided, then the global registry; unknown names become no-op\n * placeholders so stack ordering survives across kit-version skew.\n * Bumps `version` and notifies subscribers exactly once. */\n restore(snapshot: SerializedHistory): void;\n /** Push an entry whose ops have already been applied to the adapter.\n * Unlike `applyOps`, does NOT call `op.apply()`. Used by Journal.commit\n * to flush a session's net forward ops to the parent as one entry without\n * re-mutating the scene. */\n recordEntry(ops: Op[], label: string): void;\n /** Concatenated forwardOps of every undo-stack entry, in order. Snapshot\n * of \"what changes are currently applied via this history\" — useful for\n * Journal.commit to flush to a parent, and for any caller that wants to\n * diff against a baseline. */\n allForwardOps(): Op[];\n /** The id that will be assigned to the *next* pushed entry. Stable\n * monotonic counter; callers use it to tag a fork point (see Journal). */\n currentEntryId(): number;\n /** Open a scoped sub-history. All apply/undo/redo on the returned Journal\n * affect the same adapter; on commit, the Journal's net forward ops are\n * flushed to this History as one entry. See spec docs/superpowers/specs/\n * 2026-05-24-modality-design.md for the full lifecycle. */\n beginJournal(opts: BeginJournalOptions): Journal;\n /** Re-activate a suspended journal. Throws if the journal was committed or\n * cancelled (those are terminal), or if a different journal is currently\n * active — at most one journal writes to the adapter at a time, on resume\n * as well as on open. Staleness checking is the caller's\n * responsibility — consult `journal.forkedAtEntryId` against\n * `currentEntryId()` and your own op-semantic rules to decide whether\n * to resume or discard before calling this. */\n resumeJournal(journal: Journal): void;\n}\n\n/** Options for `createHistory`. */\nexport interface CreateHistoryOptions {\n /** Window (ms) within which a new entry may merge into the previous one\n * via matching `Op.coalesceKey`. Defaults to `0` (no coalescing — every\n * `applyOps` pushes a discrete entry). Recommended: ~500ms for typical\n * rapid-input UX (nudge, per-keystroke text edits). The window resets on\n * each successful coalesce, so a sustained burst keeps merging, and closes\n * on any other history operation — only the entry the last push created is\n * ever a merge target. */\n coalesceWindowMs?: number;\n /** Clock injection point for tests. Defaults to `Date.now`. */\n now?: () => number;\n /** Maximum undo-stack depth. When a push overflows the cap the oldest\n * entry is evicted (reported via `onEvict`) and can no longer be undone.\n * `0` disables the undo stack entirely — every push is evicted\n * synchronously (negative values are clamped to `0`). Default: unbounded. */\n historyLimit?: number;\n /** Fired once per entry that permanently leaves the reachable stacks:\n * redo entries dropped by a branch edit (a new push or `recordEntry`\n * after undo) and undo entries evicted by `historyLimit`.\n * NOT fired by `clear()` or `restore()` — those wholesale-replace the\n * history and the caller already knows. Note `restore()` does not enforce\n * `historyLimit` either: a restored snapshot may exceed the cap, which\n * re-applies (evicting via `onEvict`) on the next push. */\n onEvict?: (entry: EvictedEntry) => void;\n /** Custom op rebuilder consulted by `restore()` before the global\n * op-factory registry. Return `null` to fall through (global registry,\n * then a no-op placeholder). Lets an owner rebuild ops whose handlers\n * live in per-instance state the global registry can't reach (e.g. a\n * Scene's registered op kinds).\n *\n * May be invoked more than once per entry with the same `(name, args)` —\n * once per op for `forwardOps` and again for `baseOps` (the same array\n * until a coalesce splits them). Unlike `onEvict`, a throwing hook is\n * NOT caught: it aborts `restore()` mid-rebuild and can leave the\n * stacks partially rebuilt. */\n rebuildOp?: (name: string, args: unknown) => Op | null;\n /** Diagnostics sink. Omitted, the engine is silent — it deliberately owns no\n * logging utility, so that this package depends on nothing. `@weasel-js/core`'s\n * `createHistory` wrapper routes these into its `debug/flag` namespace, which\n * is what makes `DEBUG=history` work for kit consumers. */\n debug?: HistoryLogger;\n}\n\n/** Diagnostics sink for {@link CreateHistoryOptions.debug}. Messages arrive\n * pre-formatted and unconditional; deciding whether to emit them is the\n * caller's job. */\nexport interface HistoryLogger {\n log(message: string): void;\n warn(message: string): void;\n}\n\n/** Silent default — keeps every call site unconditional. */\nconst SILENT: HistoryLogger = { log: () => {}, warn: () => {} };\n\n/** Build an op-batched undo/redo `History`. The adapter is passed to each op's `apply`/`invert`. */\nexport function createHistory(adapter: unknown, options: CreateHistoryOptions = {}): History {\n const undoStack: Entry[] = [];\n const redoStack: Entry[] = [];\n let activeJournal: Journal | null = null;\n const coalesceWindowMs = options.coalesceWindowMs ?? 0;\n const now = options.now ?? (() => Date.now());\n const historyLimit = Math.max(0, options.historyLimit ?? Infinity);\n const onEvict = options.onEvict;\n const customRebuild = options.rebuildOp;\n const logger = options.debug ?? SILENT;\n let nextEntryId = 1;\n let version = 0;\n /** `version` as of the last push or coalesce. A batch may only merge into\n * the top entry while this still matches: every other operation bumps\n * `version`, so undo, redo, goto, clear, restore and `recordEntry` all\n * expire the merge window without having to remember to. Without that, an\n * edit made after stepping back rewrites whatever entry the step left on\n * top, and one undo then jumps past it. */\n let coalesceAnchorVersion = -1;\n const listeners = new Set<() => void>();\n function bump(): void {\n version++;\n for (const l of listeners) l();\n }\n\n /** Report entries that just became permanently unreachable. A throwing\n * callback must not desync the stacks mid-mutation, so failures are\n * contained and surfaced via the debug flag. */\n function reportEvicted(entries: Entry[]): void {\n if (!onEvict) return;\n for (const e of entries) {\n try {\n onEvict({ id: e.id, label: e.label, forwardOps: e.forwardOps, baseOps: e.baseOps });\n } catch (err) {\n logger.warn(`onEvict callback threw for entry id=${e.id} \"${e.label}\": ${String(err)}`);\n }\n }\n }\n\n /** Clear the redo stack (branch-on-edit), reporting dropped entries. */\n function dropRedo(): void {\n if (redoStack.length === 0) return;\n reportEvicted(redoStack.splice(0));\n }\n\n /** Evict the oldest undo entries past `historyLimit`, reporting each. */\n function enforceLimit(): void {\n while (undoStack.length > historyLimit) {\n reportEvicted([undoStack.shift()!]);\n }\n }\n\n function applyOps(ops: Op[]): void {\n for (const op of ops) op.apply(adapter);\n }\n\n /** Apply each op and collect whether any reported a real mutation.\n * Returns true iff at least one op did NOT explicitly return `false` /\n * `'noop'`. Used by `pushOrCoalesce` to skip pushing entries when every\n * op in the batch was a silent no-op (e.g. reorder where the order\n * already matched). Existing ops that return `undefined`/`void` count\n * as \"mutated\" — the default — so this is backwards-compatible. */\n function applyOpsAndDetectMutation(ops: Op[]): boolean {\n let anyMutated = false;\n for (const op of ops) {\n const r = op.apply(adapter);\n if (r !== false && r !== 'noop') anyMutated = true;\n }\n return anyMutated;\n }\n\n function invertEntry(entry: Entry): Op[] {\n return [...entry.baseOps].reverse().map((op) => op.invert());\n }\n\n /** Coalesce eligibility: every op on both sides has a `coalesceKey`, and\n * the multisets of keys match (order-independent). Match by multiset\n * rather than positional index so a multi-id selection can re-emit ops in\n * any order between batches without breaking the merge. */\n function canCoalesce(top: Entry, incoming: Op[]): boolean {\n if (coalesceWindowMs <= 0) return false;\n if (version !== coalesceAnchorVersion) return false;\n if (now() - top.timestamp > coalesceWindowMs) return false;\n if (top.forwardOps.length === 0 || incoming.length === 0) return false;\n if (top.forwardOps.length !== incoming.length) return false;\n const counts = new Map<string, number>();\n for (const op of top.forwardOps) {\n const k = op.coalesceKey;\n if (k === undefined) return false;\n counts.set(k, (counts.get(k) ?? 0) + 1);\n }\n for (const op of incoming) {\n const k = op.coalesceKey;\n if (k === undefined) return false;\n const c = counts.get(k);\n if (!c) return false;\n counts.set(k, c - 1);\n }\n return true;\n }\n\n function pushOrCoalesce(ops: Op[], label: string): void {\n if (ops.length === 0) return;\n const anyMutated = applyOpsAndDetectMutation(ops);\n if (!anyMutated) {\n // Every op reported `false`/`'noop'`. Skip the push so undo stays\n // tied to real state changes. Surfaced through the kit's debug\n // flag so the upstream caller can consider avoiding the dispatch\n // entirely. Hidden by default; enable via\n // `localStorage.setItem('weasel.debug', '1')`.\n logger.warn(\n `'${label}' batch was a no-op — every op reported false/'noop'. ` +\n `Skipping the undo entry; consider gating the dispatch upstream to avoid the wasted work.`,\n );\n return;\n }\n const incoming = touchedIdsFromOps(ops);\n const top = undoStack[undoStack.length - 1];\n if (top && canCoalesce(top, ops)) {\n top.forwardOps = ops;\n top.timestamp = now();\n // Merge incoming touched ids into the coalesced entry's set.\n if (incoming.size > 0) {\n const merged = new Set(top.touchedIds);\n for (const id of incoming) merged.add(id);\n top.touchedIds = merged;\n }\n // baseOps + label + id intentionally preserved — undo returns to the\n // pre-edit state, the original label sticks, and the entry id stays\n // stable so React lists keyed on id don't flicker. No dropRedo() here:\n // the anchor only survives until the stack is stepped, and every step\n // clears it, so a live anchor implies an empty redo stack.\n logger.log(`coalesce '${label}' into entry id=${top.id} (${ops.length} ops)`);\n bump();\n coalesceAnchorVersion = version;\n return;\n }\n logger.log(`push '${label}' (${ops.length} ops)`);\n undoStack.push({ id: nextEntryId++, forwardOps: ops, baseOps: ops, label, timestamp: now(), touchedIds: incoming });\n dropRedo();\n enforceLimit();\n bump();\n coalesceAnchorVersion = version;\n }\n\n return {\n apply(op, label) {\n pushOrCoalesce([op], label ?? op.label ?? '');\n },\n applyOps(ops, label) {\n pushOrCoalesce(ops, label);\n },\n undo() {\n const entry = undoStack.pop();\n if (!entry) return;\n applyOps(invertEntry(entry));\n redoStack.push(entry);\n bump();\n },\n redo() {\n const entry = redoStack.pop();\n if (!entry) return;\n applyOps(entry.forwardOps);\n undoStack.push(entry);\n bump();\n },\n canUndo: () => undoStack.length > 0,\n canRedo: () => redoStack.length > 0,\n undoDepth: () => undoStack.length,\n redoDepth: () => redoStack.length,\n clear: () => {\n const had = undoStack.length > 0 || redoStack.length > 0;\n undoStack.length = 0;\n redoStack.length = 0;\n if (had) bump();\n },\n entries() {\n const toView = (e: Entry): HistoryEntry => ({ id: e.id, label: e.label, timestamp: e.timestamp, touchedIds: e.touchedIds });\n // redoStack is internally stored newest-on-top (so `pop()` redoes the\n // next-most-recent undo). Reverse on the way out so callers see the\n // entries in chronological order — the user's next redo is the first\n // element, matching `entries().redo[0]` semantics.\n return {\n undo: undoStack.map(toView),\n redo: [...redoStack].reverse().map(toView),\n };\n },\n goto(n) {\n // Total length stays constant during this walk (we only shuffle\n // entries between undo and redo stacks).\n const total = undoStack.length + redoStack.length;\n if (n < 0 || n > total) return;\n while (undoStack.length > n) {\n const entry = undoStack.pop()!;\n applyOps(invertEntry(entry));\n redoStack.push(entry);\n }\n while (undoStack.length < n) {\n const entry = redoStack.pop();\n if (!entry) break; // defensive — shouldn't fire given the bounds check above\n applyOps(entry.forwardOps);\n undoStack.push(entry);\n }\n bump();\n },\n getVersion: () => version,\n subscribe(listener) {\n listeners.add(listener);\n return () => { listeners.delete(listener); };\n },\n serialize(): SerializedHistory {\n let dropped = 0;\n const project = (e: Entry): SerializedHistoryEntry | null => {\n const s = entryToSerial(e, logger);\n if (s === null) dropped++;\n return s;\n };\n return {\n version: 1,\n undoStack: undoStack.map(project).filter((e): e is SerializedHistoryEntry => e !== null),\n redoStack: redoStack.map(project).filter((e): e is SerializedHistoryEntry => e !== null),\n nextEntryId,\n droppedEntries: dropped,\n };\n },\n recordEntry(ops: Op[], label: string): void {\n if (ops.length === 0) return;\n undoStack.push({ id: nextEntryId++, forwardOps: ops, baseOps: ops, label, timestamp: now(), touchedIds: touchedIdsFromOps(ops) });\n dropRedo();\n enforceLimit();\n bump();\n },\n allForwardOps(): Op[] {\n const out: Op[] = [];\n for (const e of undoStack) {\n for (const op of e.forwardOps) out.push(op);\n }\n return out;\n },\n currentEntryId(): number {\n return nextEntryId;\n },\n beginJournal(opts: BeginJournalOptions): Journal {\n if (activeJournal !== null && activeJournal.isActive()) {\n throw new Error('A journal is already active — commit, cancel, or suspend it first');\n }\n // `adapter` is the closure-captured adapter passed to createHistory.\n // The returned History object's `this` doesn't carry it, so we pass\n // it through to the factory directly.\n const j = createJournalInternal(this, adapter, opts, () => { activeJournal = null; });\n activeJournal = j;\n return j;\n },\n resumeJournal(journal: Journal): void {\n if (activeJournal !== null && activeJournal !== journal && activeJournal.isActive()) {\n throw new Error('A journal is already active — commit, cancel, or suspend it first');\n }\n _resumeJournalInternal(journal);\n activeJournal = journal;\n },\n restore(snapshot: SerializedHistory): void {\n undoStack.length = 0;\n redoStack.length = 0;\n for (const se of snapshot.undoStack) {\n undoStack.push(serialToEntry(se, customRebuild, logger));\n }\n for (const se of snapshot.redoStack) {\n redoStack.push(serialToEntry(se, customRebuild, logger));\n }\n // Seed nextEntryId from the snapshot, then defensively bump past any\n // restored id — a malformed snapshot with duplicate or out-of-range\n // ids should never produce a collision with future entries.\n nextEntryId = snapshot.nextEntryId;\n for (const e of undoStack) if (e.id >= nextEntryId) nextEntryId = e.id + 1;\n for (const e of redoStack) if (e.id >= nextEntryId) nextEntryId = e.id + 1;\n bump();\n },\n };\n}\n\n/** Extract node ids from an op's `args`. Handles the common patterns:\n * - `args.id` (string) — transform, setPath, reparent\n * - `args.node.id` (string) — insert, delete\n * Ops that don't match either pattern contribute nothing.\n * Exported for in-package test use; not part of the published package API. */\nexport function touchedIdsFromOps(ops: Op[]): ReadonlySet<string> {\n const ids = new Set<string>();\n for (const op of ops) {\n if (op.args === null || typeof op.args !== 'object') continue;\n const a = op.args as Record<string, unknown>;\n if (typeof a['id'] === 'string') {\n ids.add(a['id']);\n } else if (a['node'] !== null && typeof a['node'] === 'object') {\n const n = a['node'] as Record<string, unknown>;\n if (typeof n['id'] === 'string') ids.add(n['id']);\n }\n }\n return ids;\n}\n\n/** Project an `Op` to its `(name, args)` wire form. Returns `null` for ops\n * missing `name` — the caller drops the containing entry. */\nfunction opToSerial(op: Op): SerializedOp | null {\n if (typeof op.name !== 'string') return null;\n return { name: op.name, args: op.args };\n}\n\n/** Project a runtime entry to its serialized form, or `null` if any op in\n * the entry can't be serialized (we drop the whole entry then — a partially\n * serializable entry would invert against the wrong baseline on undo). */\nfunction entryToSerial(e: Entry, logger: HistoryLogger): SerializedHistoryEntry | null {\n const forwardOps: SerializedOp[] = [];\n for (const op of e.forwardOps) {\n const s = opToSerial(op);\n if (s === null) {\n logger.log(`serialize: dropping entry id=${e.id} \"${e.label}\" — forwardOp without name`);\n return null;\n }\n forwardOps.push(s);\n }\n const baseOps: SerializedOp[] = [];\n for (const op of e.baseOps) {\n const s = opToSerial(op);\n if (s === null) {\n logger.log(`serialize: dropping entry id=${e.id} \"${e.label}\" — baseOp without name`);\n return null;\n }\n baseOps.push(s);\n }\n return { id: e.id, label: e.label, forwardOps, baseOps };\n}\n\n/** Placeholder op used when the registry lacks the requested name. Stable\n * identity (each placeholder is its own invert) keeps undo/redo plumbing\n * happy without performing any adapter mutation. */\nfunction placeholderOp(name: string, args: unknown, label?: string): Op {\n const op: Op = {\n name,\n args,\n label,\n apply: () => 'noop' as const,\n invert: () => op,\n };\n return op;\n}\n\n/** Signature of a per-instance op rebuilder (`CreateHistoryOptions.rebuildOp`). */\ntype CustomRebuild = (name: string, args: unknown) => Op | null;\n\n/** Rebuild a single serialized op via `custom` (if provided), falling back to\n * a no-op placeholder.\n *\n * This engine deliberately knows nothing about any op registry: hydrating a\n * `(name, args)` pair back into an op is the caller's concern, injected\n * through `CreateHistoryOptions.rebuildOp`. `@weasel-js/core`'s\n * `createHistory` wrapper supplies its global op-factory registry as that\n * hook, so core consumers see unchanged behavior. */\nfunction rebuildSerialOp(so: SerializedOp, label: string, custom: CustomRebuild | undefined, logger: HistoryLogger): Op {\n const viaCustom = custom ? custom(so.name, so.args) : null;\n if (viaCustom !== null) return viaCustom;\n logger.log(`restore: unknown op name \"${so.name}\" — substituting no-op placeholder`);\n return placeholderOp(so.name, so.args, label);\n}\n\n/** Rebuild a runtime entry from its serialized form. Unknown op names become\n * no-op placeholders so the entry still occupies its slot in the stack. */\nfunction serialToEntry(se: SerializedHistoryEntry, custom: CustomRebuild | undefined, logger: HistoryLogger): Entry {\n const forwardOps = se.forwardOps.map((so) => rebuildSerialOp(so, se.label, custom, logger));\n const baseOps = se.baseOps.map((so) => rebuildSerialOp(so, se.label, custom, logger));\n return {\n id: se.id,\n label: se.label,\n forwardOps,\n baseOps,\n // Coalescing is a within-session concept; a restored entry is never a\n // coalesce anchor, so its timestamp only has to be non-null.\n timestamp: 0,\n // Re-derive touchedIds from the rebuilt ops rather than trying to\n // round-trip the Set through the serialized form (Sets aren't JSON-safe).\n touchedIds: touchedIdsFromOps(forwardOps),\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/journal.ts","../src/history.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import type { Op } from './op';\nimport { createHistory, type History, type HistoryEntry, type HistorySelection } from './history';\n\nconst RESUMERS = new WeakMap<Journal, () => void>();\n\n/** Called by `history.resumeJournal`. Not part of the public API. */\nexport function _resumeJournalInternal(j: Journal): void {\n const r = RESUMERS.get(j);\n if (!r) throw new Error('Journal is not resumable (already committed or cancelled)');\n r();\n}\n\n/** Options for `history.beginJournal()`. */\nexport interface BeginJournalOptions {\n /** Label for the single parent-history entry the journal flushes on commit. */\n label: string;\n /** Caller-supplied tag naming what this journal is scoped to — typically the\n * id of the node being edited. The history layer only carries it; callers\n * read it back off the journal to decide whether a suspended journal\n * matches what they are about to edit. */\n targetId?: string;\n}\n\n/**\n * A scoped sub-history forked from a `History`, opened by\n * `history.beginJournal()`. Applies, undoes and redoes against the same\n * adapter as its parent, but keeps its entries to itself: `commit` flushes the\n * journal's net forward ops to the parent as one entry, `cancel` rewinds them\n * and contributes nothing. Use it when a self-contained editing session (a\n * text edit, a modal drag) should collapse to a single step in the parent's\n * undo stack while still offering undo *within* the session.\n *\n * A journal is active, suspended or closed. `commit` and `cancel` are\n * terminal; `suspend` lets the parent be used again and can be reversed with\n * `history.resumeJournal()`. Every mutating method throws when the journal is\n * not active.\n */\nexport interface Journal {\n readonly targetId: string | undefined;\n readonly forkedAtEntryId: number;\n\n // Same operational surface as History\n applyBatch(ops: Op[], label: string): void;\n undo(): void;\n redo(): void;\n canUndo(): boolean;\n canRedo(): boolean;\n entries(): { undo: HistoryEntry[]; redo: HistoryEntry[] };\n\n // Lifecycle\n commit(label: string): void;\n cancel(): void;\n suspend(): void;\n isActive(): boolean;\n}\n\n/** Internal factory used by `createHistory`'s `beginJournal` method.\n * Not exported via the package's `index.ts` — callers go through\n * `history.beginJournal()`. */\nexport function createJournalInternal(\n parent: History,\n adapter: unknown,\n opts: BeginJournalOptions,\n onClose?: () => void,\n selection?: HistorySelection,\n): Journal {\n const inner = createHistory(adapter, selection ? { selection } : {});\n const forkedAtEntryId = parent.currentEntryId();\n // The whole session collapses to one parent entry, so the selection that\n // entry restores is the one the session opened under — not whatever the\n // last keystroke left.\n const selectionBefore = selection ? [...selection.get()] : undefined;\n type State = 'active' | 'suspended' | 'closed';\n let state: State = 'active';\n const targetId = opts.targetId;\n\n const journal: Journal = {\n targetId,\n forkedAtEntryId,\n\n applyBatch(ops: Op[], label: string): void {\n if (state !== 'active') throw new Error('Journal is not active');\n inner.applyOps(ops, label);\n },\n undo(): void {\n if (state !== 'active') throw new Error('Journal is not active');\n inner.undo();\n },\n redo(): void {\n if (state !== 'active') throw new Error('Journal is not active');\n inner.redo();\n },\n canUndo(): boolean {\n return inner.canUndo();\n },\n canRedo(): boolean {\n return inner.canRedo();\n },\n entries() {\n return inner.entries();\n },\n commit(label: string): void {\n if (state !== 'active') throw new Error('Journal is not active');\n const netOps = inner.allForwardOps();\n if (netOps.length > 0) {\n parent.recordEntry(netOps, label, selectionBefore ? { selectionBefore } : undefined);\n }\n state = 'closed';\n RESUMERS.delete(journal);\n onClose?.();\n },\n cancel(): void {\n if (state !== 'active') throw new Error('Journal is not active');\n inner.goto(0);\n state = 'closed';\n RESUMERS.delete(journal);\n onClose?.();\n },\n suspend(): void {\n if (state !== 'active') throw new Error('Journal is not active');\n state = 'suspended';\n onClose?.();\n },\n isActive(): boolean {\n return state === 'active';\n },\n };\n\n RESUMERS.set(journal, () => {\n if (state !== 'suspended') throw new Error('Journal is not suspended');\n state = 'active';\n });\n\n return journal;\n}\n","import type { Op } from './op';\nimport { createJournalInternal, _resumeJournalInternal, type Journal, type BeginJournalOptions } from './journal';\n\ninterface Entry {\n /** Monotonic id assigned at first push. Stable across coalesce merges\n * (a merged entry keeps the original id) so UI lists keyed on `id` don't\n * flicker when the underlying entry mutates. */\n id: number;\n /** Forward ops — applied on redo, reflect the latest to-state after any\n * coalescing. Diverges from `baseOps` only after a coalesce. */\n forwardOps: Op[];\n /** Original ops at first push — their `.invert()` is what undo replays.\n * Preserved across coalesces so undo always returns to the original\n * pre-edit state, no matter how many coalesces happened. */\n baseOps: Op[];\n label: string;\n /** ms timestamp at last push or coalesce; used to gate the coalesce window. */\n timestamp: number;\n /** Node ids touched by ops in this entry. See `HistoryEntry.touchedIds`. */\n touchedIds: ReadonlySet<string>;\n /** Selection as of just before this entry's ops ran; restored by undo.\n * Undefined when nothing supplied one (no `selection` option, or a\n * `recordEntry` call that captured none). */\n selectionBefore?: readonly string[];\n /** Selection as of just before this entry was last undone; restored by\n * redo. Written on the way past, so it reflects where the user actually\n * was rather than where the entry's ops left them. */\n selectionAfter?: readonly string[];\n}\n\n/** Wire form of a single op inside a serialized history. The pair\n * `(name, args)` reconstructs a live `Op` via the op-factory registry. */\nexport interface SerializedOp {\n name: string;\n args: unknown;\n}\n\n/** Wire form of one history entry. `forwardOps` / `baseOps` mirror the\n * in-memory entry's fields (see `Entry` above) but only carry the\n * serializable `(name, args)` projection of each op. */\nexport interface SerializedHistoryEntry {\n id: number;\n label: string;\n forwardOps: SerializedOp[];\n baseOps: SerializedOp[];\n selectionBefore?: readonly string[];\n selectionAfter?: readonly string[];\n}\n\n/** Snapshot of an entire `History` instance. Designed to live alongside the\n * scene snapshot in IDB so a reload restores the undo / redo stacks to\n * exactly where they were. */\nexport interface SerializedHistory {\n version: 1;\n undoStack: SerializedHistoryEntry[];\n /** Stored newest-first, mirroring the in-memory stack so a deserialized\n * history matches the original's `entries().redo` ordering. */\n redoStack: SerializedHistoryEntry[];\n nextEntryId: number;\n /** Entries dropped because at least one of their ops lacked a `name`\n * and therefore couldn't round-trip through the op-factory registry.\n * Always present (zero when nothing was dropped) so callers can detect\n * loss without parsing the debug log. */\n droppedEntries: number;\n}\n\n/** Snapshot of an entry handed to `onEvict` when it permanently leaves the\n * reachable stacks. Ops are live references — read `name`/`args`, don't\n * mutate. */\nexport interface EvictedEntry {\n id: number;\n label: string;\n forwardOps: readonly Op[];\n baseOps: readonly Op[];\n}\n\n/** Read-only view of a history entry exposed via `History.entries()`. */\nexport interface HistoryEntry {\n /** Stable monotonic id (preserved across coalesce merges). */\n id: number;\n /** Human-readable label (the `label` arg passed to `applyOps`). */\n label: string;\n /** Push/last-coalesce timestamp (ms). */\n timestamp: number;\n /** Set of node ids touched by any op in this entry. Populated from ops\n * whose `args` carry an `id` field (transform, setPath, reparent) or a\n * `node.id` field (insert, delete). Ops without a recognisable id field\n * contribute nothing. May be `undefined` for deserialized entries\n * restored from an older snapshot that predates this field. */\n touchedIds?: ReadonlySet<string>;\n /** Selection restored when this entry is undone. */\n selectionBefore?: readonly string[];\n /** Selection restored when this entry is redone. */\n selectionAfter?: readonly string[];\n}\n\n/** Op-batched undo/redo controller returned by `createHistory`. */\nexport interface History {\n apply(op: Op, label?: string): void;\n applyOps(ops: Op[], label: string): void;\n undo(): void;\n redo(): void;\n canUndo(): boolean;\n canRedo(): boolean;\n /** Number of entries on the undo stack (O(1); `entries().undo.length`\n * without materializing the views). */\n undoDepth(): number;\n /** Number of entries on the redo stack (O(1)). */\n redoDepth(): number;\n clear(): void;\n /** Snapshot of the current undo + redo stacks. `undo` is oldest→newest\n * (i.e. the last element is what `undo()` would pop next); `redo` is\n * also oldest→newest from the user's perspective (i.e. the *first*\n * element is what `redo()` would pop next — see implementation note).\n * Callers should treat the arrays as immutable. */\n entries(): { undo: HistoryEntry[]; redo: HistoryEntry[] };\n /** Walk the history forward/back until exactly `n` entries are on the\n * undo stack (0 ≤ n ≤ entries().undo.length + entries().redo.length).\n * Equivalent to repeated `undo()`/`redo()` calls but doesn't bother\n * rebuilding entry snapshots between steps. No-op if already at `n`. */\n goto(n: number): void;\n /** Monotonic counter bumped on every push/undo/redo/clear/coalesce.\n * Cheap to read; callers use it as a React dep to detect changes. */\n getVersion(): number;\n /** Subscribe to history changes. Fires after every push/undo/redo/\n * clear/coalesce. Returns an unsubscribe fn. */\n subscribe(listener: () => void): () => void;\n /** Snapshot the undo + redo stacks in a structured-clone-safe form.\n * Entries whose ops aren't all kit-registered (i.e. any op missing a\n * `name`) are dropped from the snapshot with a debug-level log — they\n * can't round-trip, so we omit them rather than emit a half-restorable\n * entry. The in-memory stacks aren't modified. */\n serialize(): SerializedHistory;\n /** Replace the current undo + redo stacks with the deserialized contents\n * of `snapshot`. Ops are rebuilt via the `rebuildOp` option when\n * provided, then the global registry; unknown names become no-op\n * placeholders so stack ordering survives across kit-version skew.\n * Bumps `version` and notifies subscribers exactly once. */\n restore(snapshot: SerializedHistory): void;\n /** Push an entry whose ops have already been applied to the adapter.\n * Unlike `applyOps`, does NOT call `op.apply()`. Used by Journal.commit\n * to flush a session's net forward ops to the parent as one entry without\n * re-mutating the scene. */\n recordEntry(ops: Op[], label: string, options?: RecordEntryOptions): void;\n /** Concatenated forwardOps of every undo-stack entry, in order. Snapshot\n * of \"what changes are currently applied via this history\" — useful for\n * Journal.commit to flush to a parent, and for any caller that wants to\n * diff against a baseline. */\n allForwardOps(): Op[];\n /** The id that will be assigned to the *next* pushed entry. Stable\n * monotonic counter; callers use it to tag a fork point (see Journal). */\n currentEntryId(): number;\n /** Open a scoped sub-history. All apply/undo/redo on the returned Journal\n * affect the same adapter; on commit, the Journal's net forward ops are\n * flushed to this History as one entry. See spec docs/superpowers/specs/\n * 2026-05-24-modality-design.md for the full lifecycle. */\n beginJournal(opts: BeginJournalOptions): Journal;\n /** Re-activate a suspended journal. Throws if the journal was committed or\n * cancelled (those are terminal), or if a different journal is currently\n * active — at most one journal writes to the adapter at a time, on resume\n * as well as on open. Staleness checking is the caller's\n * responsibility — consult `journal.forkedAtEntryId` against\n * `currentEntryId()` and your own op-semantic rules to decide whether\n * to resume or discard before calling this. */\n resumeJournal(journal: Journal): void;\n}\n\n/** Read/write access to whatever holds the caller's selection. The engine\n * stores the ids it is handed and hands them back on the way past; it\n * attaches no meaning to them and never records a selection change as an\n * entry of its own. */\nexport interface HistorySelection {\n get(): readonly string[];\n set(ids: readonly string[]): void;\n}\n\n/** Options for `recordEntry`. */\nexport interface RecordEntryOptions {\n /** Selection as of before the already-applied ops ran. `recordEntry` is\n * called after the fact, so the live selection has moved on by then and\n * the engine cannot sample it — a caller that wants undo to restore the\n * selection captures it when the batch opens and passes it here. */\n selectionBefore?: readonly string[];\n}\n\n/** Options for `createHistory`. */\nexport interface CreateHistoryOptions {\n /** Window (ms) within which a new entry may merge into the previous one\n * via matching `Op.coalesceKey`. Defaults to `0` (no coalescing — every\n * `applyOps` pushes a discrete entry). Recommended: ~500ms for typical\n * rapid-input UX (nudge, per-keystroke text edits). The window resets on\n * each successful coalesce, so a sustained burst keeps merging, and closes\n * on any other history operation — only the entry the last push created is\n * ever a merge target. */\n coalesceWindowMs?: number;\n /** Clock injection point for tests. Defaults to `Date.now`. */\n now?: () => number;\n /** Where the caller's selection lives. Supplied, each entry records the\n * selection around it and undo / redo / goto restore it; omitted, the\n * engine never reads or writes selection at all. */\n selection?: HistorySelection;\n /** Maximum undo-stack depth. When a push overflows the cap the oldest\n * entry is evicted (reported via `onEvict`) and can no longer be undone.\n * `0` disables the undo stack entirely — every push is evicted\n * synchronously (negative values are clamped to `0`). Default: unbounded. */\n historyLimit?: number;\n /** Fired once per entry that permanently leaves the reachable stacks:\n * redo entries dropped by a branch edit (a new push or `recordEntry`\n * after undo) and undo entries evicted by `historyLimit`.\n * NOT fired by `clear()` or `restore()` — those wholesale-replace the\n * history and the caller already knows. Note `restore()` does not enforce\n * `historyLimit` either: a restored snapshot may exceed the cap, which\n * re-applies (evicting via `onEvict`) on the next push. */\n onEvict?: (entry: EvictedEntry) => void;\n /** Custom op rebuilder consulted by `restore()` before the global\n * op-factory registry. Return `null` to fall through (global registry,\n * then a no-op placeholder). Lets an owner rebuild ops whose handlers\n * live in per-instance state the global registry can't reach (e.g. a\n * Scene's registered op kinds).\n *\n * May be invoked more than once per entry with the same `(name, args)` —\n * once per op for `forwardOps` and again for `baseOps` (the same array\n * until a coalesce splits them). Unlike `onEvict`, a throwing hook is\n * NOT caught: it aborts `restore()` mid-rebuild and can leave the\n * stacks partially rebuilt. */\n rebuildOp?: (name: string, args: unknown) => Op | null;\n /** Diagnostics sink. Omitted, the engine is silent — it deliberately owns no\n * logging utility, so that this package depends on nothing. `@weasel-js/core`'s\n * `createHistory` wrapper routes these into its `debug/flag` namespace, which\n * is what makes `DEBUG=history` work for kit consumers. */\n debug?: HistoryLogger;\n}\n\n/** Diagnostics sink for {@link CreateHistoryOptions.debug}. Messages arrive\n * pre-formatted and unconditional; deciding whether to emit them is the\n * caller's job. */\nexport interface HistoryLogger {\n log(message: string): void;\n warn(message: string): void;\n}\n\n/** Silent default — keeps every call site unconditional. */\nconst SILENT: HistoryLogger = { log: () => {}, warn: () => {} };\n\n/** Build an op-batched undo/redo `History`. The adapter is passed to each op's `apply`/`invert`. */\nexport function createHistory(adapter: unknown, options: CreateHistoryOptions = {}): History {\n const undoStack: Entry[] = [];\n const redoStack: Entry[] = [];\n let activeJournal: Journal | null = null;\n const coalesceWindowMs = options.coalesceWindowMs ?? 0;\n const now = options.now ?? (() => Date.now());\n const historyLimit = Math.max(0, options.historyLimit ?? Infinity);\n const onEvict = options.onEvict;\n const customRebuild = options.rebuildOp;\n const selection = options.selection;\n const logger = options.debug ?? SILENT;\n let nextEntryId = 1;\n let version = 0;\n /** `version` as of the last push or coalesce. A batch may only merge into\n * the top entry while this still matches: every other operation bumps\n * `version`, so undo, redo, goto, clear, restore and `recordEntry` all\n * expire the merge window without having to remember to. Without that, an\n * edit made after stepping back rewrites whatever entry the step left on\n * top, and one undo then jumps past it. */\n let coalesceAnchorVersion = -1;\n const listeners = new Set<() => void>();\n function bump(): void {\n version++;\n for (const l of listeners) l();\n }\n\n /** Report entries that just became permanently unreachable. A throwing\n * callback must not desync the stacks mid-mutation, so failures are\n * contained and surfaced via the debug flag. */\n function reportEvicted(entries: Entry[]): void {\n if (!onEvict) return;\n for (const e of entries) {\n try {\n onEvict({ id: e.id, label: e.label, forwardOps: e.forwardOps, baseOps: e.baseOps });\n } catch (err) {\n logger.warn(`onEvict callback threw for entry id=${e.id} \"${e.label}\": ${String(err)}`);\n }\n }\n }\n\n /** Clear the redo stack (branch-on-edit), reporting dropped entries. */\n function dropRedo(): void {\n if (redoStack.length === 0) return;\n reportEvicted(redoStack.splice(0));\n }\n\n /** Evict the oldest undo entries past `historyLimit`, reporting each. */\n function enforceLimit(): void {\n while (undoStack.length > historyLimit) {\n reportEvicted([undoStack.shift()!]);\n }\n }\n\n function applyOps(ops: Op[]): void {\n for (const op of ops) op.apply(adapter);\n }\n\n /** Apply each op and collect whether any reported a real mutation.\n * Returns true iff at least one op did NOT explicitly return `false` /\n * `'noop'`. Used by `pushOrCoalesce` to skip pushing entries when every\n * op in the batch was a silent no-op (e.g. reorder where the order\n * already matched). Existing ops that return `undefined`/`void` count\n * as \"mutated\" — the default — so this is backwards-compatible. */\n function applyOpsAndDetectMutation(ops: Op[]): boolean {\n let anyMutated = false;\n for (const op of ops) {\n const r = op.apply(adapter);\n if (r !== false && r !== 'noop') anyMutated = true;\n }\n return anyMutated;\n }\n\n function invertEntry(entry: Entry): Op[] {\n return [...entry.baseOps].reverse().map((op) => op.invert());\n }\n\n /** Copy on read: the caller's array is theirs to mutate. */\n function readSelection(): readonly string[] | undefined {\n return selection ? [...selection.get()] : undefined;\n }\n\n /** Walk one entry backwards. The selection is restored *after* the\n * inverted ops apply, so an entry that also carries a `setSelection`-style\n * op can't overwrite the snapshot. */\n function stepBack(entry: Entry): void {\n const leaving = readSelection();\n if (leaving) entry.selectionAfter = leaving;\n applyOps(invertEntry(entry));\n if (selection && entry.selectionBefore) selection.set(entry.selectionBefore);\n }\n\n /** Walk one entry forwards. Mirror of `stepBack`. */\n function stepForward(entry: Entry): void {\n const leaving = readSelection();\n if (leaving) entry.selectionBefore = leaving;\n applyOps(entry.forwardOps);\n if (selection && entry.selectionAfter) selection.set(entry.selectionAfter);\n }\n\n /** Coalesce eligibility: every op on both sides has a `coalesceKey`, and\n * the multisets of keys match (order-independent). Match by multiset\n * rather than positional index so a multi-id selection can re-emit ops in\n * any order between batches without breaking the merge. */\n function canCoalesce(top: Entry, incoming: Op[]): boolean {\n if (coalesceWindowMs <= 0) return false;\n if (version !== coalesceAnchorVersion) return false;\n if (now() - top.timestamp > coalesceWindowMs) return false;\n if (top.forwardOps.length === 0 || incoming.length === 0) return false;\n if (top.forwardOps.length !== incoming.length) return false;\n const counts = new Map<string, number>();\n for (const op of top.forwardOps) {\n const k = op.coalesceKey;\n if (k === undefined) return false;\n counts.set(k, (counts.get(k) ?? 0) + 1);\n }\n for (const op of incoming) {\n const k = op.coalesceKey;\n if (k === undefined) return false;\n const c = counts.get(k);\n if (!c) return false;\n counts.set(k, c - 1);\n }\n return true;\n }\n\n function pushOrCoalesce(ops: Op[], label: string): void {\n if (ops.length === 0) return;\n const selectionBefore = readSelection();\n const anyMutated = applyOpsAndDetectMutation(ops);\n if (!anyMutated) {\n // Every op reported `false`/`'noop'`. Skip the push so undo stays\n // tied to real state changes. Surfaced through the kit's debug\n // flag so the upstream caller can consider avoiding the dispatch\n // entirely. Hidden by default; enable via\n // `localStorage.setItem('weasel.debug', '1')`.\n logger.warn(\n `'${label}' batch was a no-op — every op reported false/'noop'. ` +\n `Skipping the undo entry; consider gating the dispatch upstream to avoid the wasted work.`,\n );\n return;\n }\n const incoming = touchedIdsFromOps(ops);\n const top = undoStack[undoStack.length - 1];\n if (top && canCoalesce(top, ops)) {\n top.forwardOps = ops;\n top.timestamp = now();\n // Merge incoming touched ids into the coalesced entry's set.\n if (incoming.size > 0) {\n const merged = new Set(top.touchedIds);\n for (const id of incoming) merged.add(id);\n top.touchedIds = merged;\n }\n // baseOps + label + id intentionally preserved — undo returns to the\n // pre-edit state, the original label sticks, and the entry id stays\n // stable so React lists keyed on id don't flicker. No dropRedo() here:\n // the anchor only survives until the stack is stepped, and every step\n // clears it, so a live anchor implies an empty redo stack.\n logger.log(`coalesce '${label}' into entry id=${top.id} (${ops.length} ops)`);\n bump();\n coalesceAnchorVersion = version;\n return;\n }\n logger.log(`push '${label}' (${ops.length} ops)`);\n undoStack.push({\n id: nextEntryId++, forwardOps: ops, baseOps: ops, label, timestamp: now(), touchedIds: incoming,\n ...(selectionBefore ? { selectionBefore } : {}),\n });\n dropRedo();\n enforceLimit();\n bump();\n coalesceAnchorVersion = version;\n }\n\n return {\n apply(op, label) {\n pushOrCoalesce([op], label ?? op.label ?? '');\n },\n applyOps(ops, label) {\n pushOrCoalesce(ops, label);\n },\n undo() {\n const entry = undoStack.pop();\n if (!entry) return;\n stepBack(entry);\n redoStack.push(entry);\n bump();\n },\n redo() {\n const entry = redoStack.pop();\n if (!entry) return;\n stepForward(entry);\n undoStack.push(entry);\n bump();\n },\n canUndo: () => undoStack.length > 0,\n canRedo: () => redoStack.length > 0,\n undoDepth: () => undoStack.length,\n redoDepth: () => redoStack.length,\n clear: () => {\n const had = undoStack.length > 0 || redoStack.length > 0;\n undoStack.length = 0;\n redoStack.length = 0;\n if (had) bump();\n },\n entries() {\n const toView = (e: Entry): HistoryEntry => ({\n id: e.id, label: e.label, timestamp: e.timestamp, touchedIds: e.touchedIds,\n ...(e.selectionBefore ? { selectionBefore: e.selectionBefore } : {}),\n ...(e.selectionAfter ? { selectionAfter: e.selectionAfter } : {}),\n });\n // redoStack is internally stored newest-on-top (so `pop()` redoes the\n // next-most-recent undo). Reverse on the way out so callers see the\n // entries in chronological order — the user's next redo is the first\n // element, matching `entries().redo[0]` semantics.\n return {\n undo: undoStack.map(toView),\n redo: [...redoStack].reverse().map(toView),\n };\n },\n goto(n) {\n // Total length stays constant during this walk (we only shuffle\n // entries between undo and redo stacks).\n const total = undoStack.length + redoStack.length;\n if (n < 0 || n > total) return;\n while (undoStack.length > n) {\n const entry = undoStack.pop()!;\n stepBack(entry);\n redoStack.push(entry);\n }\n while (undoStack.length < n) {\n const entry = redoStack.pop();\n if (!entry) break; // defensive — shouldn't fire given the bounds check above\n stepForward(entry);\n undoStack.push(entry);\n }\n bump();\n },\n getVersion: () => version,\n subscribe(listener) {\n listeners.add(listener);\n return () => { listeners.delete(listener); };\n },\n serialize(): SerializedHistory {\n let dropped = 0;\n const project = (e: Entry): SerializedHistoryEntry | null => {\n const s = entryToSerial(e, logger);\n if (s === null) dropped++;\n return s;\n };\n return {\n version: 1,\n undoStack: undoStack.map(project).filter((e): e is SerializedHistoryEntry => e !== null),\n redoStack: redoStack.map(project).filter((e): e is SerializedHistoryEntry => e !== null),\n nextEntryId,\n droppedEntries: dropped,\n };\n },\n recordEntry(ops: Op[], label: string, options: RecordEntryOptions = {}): void {\n if (ops.length === 0) return;\n undoStack.push({\n id: nextEntryId++, forwardOps: ops, baseOps: ops, label, timestamp: now(),\n touchedIds: touchedIdsFromOps(ops),\n ...(options.selectionBefore ? { selectionBefore: [...options.selectionBefore] } : {}),\n });\n dropRedo();\n enforceLimit();\n bump();\n },\n allForwardOps(): Op[] {\n const out: Op[] = [];\n for (const e of undoStack) {\n for (const op of e.forwardOps) out.push(op);\n }\n return out;\n },\n currentEntryId(): number {\n return nextEntryId;\n },\n beginJournal(opts: BeginJournalOptions): Journal {\n if (activeJournal !== null && activeJournal.isActive()) {\n throw new Error('A journal is already active — commit, cancel, or suspend it first');\n }\n // `adapter` is the closure-captured adapter passed to createHistory.\n // The returned History object's `this` doesn't carry it, so we pass\n // it through to the factory directly.\n const j = createJournalInternal(this, adapter, opts, () => { activeJournal = null; }, selection);\n activeJournal = j;\n return j;\n },\n resumeJournal(journal: Journal): void {\n if (activeJournal !== null && activeJournal !== journal && activeJournal.isActive()) {\n throw new Error('A journal is already active — commit, cancel, or suspend it first');\n }\n _resumeJournalInternal(journal);\n activeJournal = journal;\n },\n restore(snapshot: SerializedHistory): void {\n undoStack.length = 0;\n redoStack.length = 0;\n for (const se of snapshot.undoStack) {\n undoStack.push(serialToEntry(se, customRebuild, logger));\n }\n for (const se of snapshot.redoStack) {\n redoStack.push(serialToEntry(se, customRebuild, logger));\n }\n // Seed nextEntryId from the snapshot, then defensively bump past any\n // restored id — a malformed snapshot with duplicate or out-of-range\n // ids should never produce a collision with future entries.\n nextEntryId = snapshot.nextEntryId;\n for (const e of undoStack) if (e.id >= nextEntryId) nextEntryId = e.id + 1;\n for (const e of redoStack) if (e.id >= nextEntryId) nextEntryId = e.id + 1;\n bump();\n },\n };\n}\n\n/** Extract node ids from an op's `args`. Handles the common patterns:\n * - `args.id` (string) — transform, setPath, reparent\n * - `args.node.id` (string) — insert, delete\n * Ops that don't match either pattern contribute nothing.\n * Exported for in-package test use; not part of the published package API. */\nexport function touchedIdsFromOps(ops: Op[]): ReadonlySet<string> {\n const ids = new Set<string>();\n for (const op of ops) {\n if (op.args === null || typeof op.args !== 'object') continue;\n const a = op.args as Record<string, unknown>;\n if (typeof a['id'] === 'string') {\n ids.add(a['id']);\n } else if (a['node'] !== null && typeof a['node'] === 'object') {\n const n = a['node'] as Record<string, unknown>;\n if (typeof n['id'] === 'string') ids.add(n['id']);\n }\n }\n return ids;\n}\n\n/** Project an `Op` to its `(name, args)` wire form. Returns `null` for ops\n * missing `name` — the caller drops the containing entry. */\nfunction opToSerial(op: Op): SerializedOp | null {\n if (typeof op.name !== 'string') return null;\n return { name: op.name, args: op.args };\n}\n\n/** Project a runtime entry to its serialized form, or `null` if any op in\n * the entry can't be serialized (we drop the whole entry then — a partially\n * serializable entry would invert against the wrong baseline on undo). */\nfunction entryToSerial(e: Entry, logger: HistoryLogger): SerializedHistoryEntry | null {\n const forwardOps: SerializedOp[] = [];\n for (const op of e.forwardOps) {\n const s = opToSerial(op);\n if (s === null) {\n logger.log(`serialize: dropping entry id=${e.id} \"${e.label}\" — forwardOp without name`);\n return null;\n }\n forwardOps.push(s);\n }\n const baseOps: SerializedOp[] = [];\n for (const op of e.baseOps) {\n const s = opToSerial(op);\n if (s === null) {\n logger.log(`serialize: dropping entry id=${e.id} \"${e.label}\" — baseOp without name`);\n return null;\n }\n baseOps.push(s);\n }\n return {\n id: e.id, label: e.label, forwardOps, baseOps,\n ...(e.selectionBefore ? { selectionBefore: e.selectionBefore } : {}),\n ...(e.selectionAfter ? { selectionAfter: e.selectionAfter } : {}),\n };\n}\n\n/** Placeholder op used when the registry lacks the requested name. Stable\n * identity (each placeholder is its own invert) keeps undo/redo plumbing\n * happy without performing any adapter mutation. */\nfunction placeholderOp(name: string, args: unknown, label?: string): Op {\n const op: Op = {\n name,\n args,\n label,\n apply: () => 'noop' as const,\n invert: () => op,\n };\n return op;\n}\n\n/** Signature of a per-instance op rebuilder (`CreateHistoryOptions.rebuildOp`). */\ntype CustomRebuild = (name: string, args: unknown) => Op | null;\n\n/** Rebuild a single serialized op via `custom` (if provided), falling back to\n * a no-op placeholder.\n *\n * This engine deliberately knows nothing about any op registry: hydrating a\n * `(name, args)` pair back into an op is the caller's concern, injected\n * through `CreateHistoryOptions.rebuildOp`. `@weasel-js/core`'s\n * `createHistory` wrapper supplies its global op-factory registry as that\n * hook, so core consumers see unchanged behavior. */\nfunction rebuildSerialOp(so: SerializedOp, label: string, custom: CustomRebuild | undefined, logger: HistoryLogger): Op {\n const viaCustom = custom ? custom(so.name, so.args) : null;\n if (viaCustom !== null) return viaCustom;\n logger.log(`restore: unknown op name \"${so.name}\" — substituting no-op placeholder`);\n return placeholderOp(so.name, so.args, label);\n}\n\n/** Rebuild a runtime entry from its serialized form. Unknown op names become\n * no-op placeholders so the entry still occupies its slot in the stack. */\nfunction serialToEntry(se: SerializedHistoryEntry, custom: CustomRebuild | undefined, logger: HistoryLogger): Entry {\n const forwardOps = se.forwardOps.map((so) => rebuildSerialOp(so, se.label, custom, logger));\n const baseOps = se.baseOps.map((so) => rebuildSerialOp(so, se.label, custom, logger));\n return {\n id: se.id,\n label: se.label,\n forwardOps,\n baseOps,\n // Coalescing is a within-session concept; a restored entry is never a\n // coalesce anchor, so its timestamp only has to be non-null.\n timestamp: 0,\n // Re-derive touchedIds from the rebuilt ops rather than trying to\n // round-trip the Set through the serialized form (Sets aren't JSON-safe).\n touchedIds: touchedIdsFromOps(forwardOps),\n ...(se.selectionBefore ? { selectionBefore: [...se.selectionBefore] } : {}),\n ...(se.selectionAfter ? { selectionAfter: [...se.selectionAfter] } : {}),\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weasel-js/history",
3
- "version": "1.0.4",
3
+ "version": "1.2.0",
4
4
  "description": "Undo/redo history with scoped sub-history (Journal) primitive. No React, no DOM.",
5
5
  "license": "MIT",
6
6
  "type": "module",