@miy2/xml-api 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,31 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SyncEngine = void 0;
4
- const parser_1 = require("../cst/parser");
5
- const xml_grammar_1 = require("../cst/xml-grammar");
6
- const history_manager_1 = require("../history-manager");
7
- const formatter_1 = require("../model/formatter");
8
- const xml_api_model_1 = require("../model/xml-api-model");
9
- const xml_binder_1 = require("../model/xml-binder");
10
- const xml_api_events_1 = require("../xml-api-events");
11
- const editor_state_1 = require("./editor-state");
12
- const transaction_1 = require("./transaction");
13
- class SyncEngine {
14
- constructor(source, grammar = xml_grammar_1.grammar) {
1
+ import { Parser } from "../cst/parser";
2
+ import { grammar as defaultGrammar } from "../cst/xml-grammar";
3
+ import { HistoryManager } from "../history-manager";
4
+ import { ModelElement } from "../model/xml-api-model";
5
+ import { XMLBinder } from "../model/xml-binder";
6
+ import { EventEmitter } from "../xml-api-events";
7
+ import { EditorState } from "./editor-state";
8
+ import { Transaction } from "./transaction";
9
+ import { TransactionBuilder } from "./transaction-builder";
10
+ /**
11
+ * The core engine that manages the editor state and coordinates synchronization.
12
+ *
13
+ * It implements a transaction-based update cycle:
14
+ * 1. Receives a `Transaction` describing changes.
15
+ * 2. Updates the `EditorState` (Source).
16
+ * 3. Triggers the `Parser` (Source -> CST).
17
+ * 4. Triggers the `XMLBinder` (CST -> Model).
18
+ * 5. Notifies listeners (including `SchemaView`s) of changes.
19
+ */
20
+ export class SyncEngine {
21
+ constructor(source, grammar = defaultGrammar) {
15
22
  this.isTransacting = false;
16
23
  this.collabBridge = null;
17
- this._state = editor_state_1.EditorState.create(source);
18
- this.parser = new parser_1.Parser(grammar);
19
- this.binder = new xml_binder_1.XMLBinder(source);
20
- this.history = new history_manager_1.HistoryManager();
21
- this.events = new xml_api_events_1.EventEmitter();
24
+ this._state = EditorState.create(source);
25
+ this.parser = new Parser(grammar);
26
+ this.binder = new XMLBinder(source);
27
+ this.history = new HistoryManager();
28
+ this.events = new EventEmitter();
22
29
  this.fullParse();
23
30
  }
24
31
  get state() {
@@ -47,6 +54,15 @@ class SyncEngine {
47
54
  }
48
55
  /**
49
56
  * Applies a transaction to the engine, updating the state and notifying listeners.
57
+ * This is the single point of truth for all state transitions in the system.
58
+ *
59
+ * It handles:
60
+ * - History recording (Undo/Redo)
61
+ * - Incremental Parsing and Reconciliation
62
+ * - Event Dispatching
63
+ * - Collaboration hooks
64
+ *
65
+ * @param tr The transaction to apply.
50
66
  */
51
67
  dispatch(tr) {
52
68
  if (!tr.docChanged)
@@ -66,6 +82,7 @@ class SyncEngine {
66
82
  const oldText = oldState.source.slice(p.from, p.to);
67
83
  const newEnd = p.from + p.text.length;
68
84
  this.history.push({
85
+ timestamp: Date.now(),
69
86
  redo: { from: p.from, to: p.to, text: p.text },
70
87
  undo: { from: p.from, to: newEnd, text: oldText },
71
88
  });
@@ -78,7 +95,7 @@ class SyncEngine {
78
95
  }
79
96
  this._state = this._state.update({ source: newSource });
80
97
  // Core Update Logic (Parser / Binder)
81
- this.binder = new xml_binder_1.XMLBinder(newSource);
98
+ this.binder = new XMLBinder(newSource);
82
99
  // Optimization: If single patch, try incremental. Else full parse.
83
100
  let handled = false;
84
101
  if (tr.patches.length === 1 && oldState.cst) {
@@ -86,17 +103,23 @@ class SyncEngine {
86
103
  const delta = p.text.length - (p.to - p.from);
87
104
  const incrementalResult = this.tryIncrementalUpdate(p.from, p.to, delta);
88
105
  if (incrementalResult) {
106
+ let success = true;
89
107
  if (oldState.model) {
90
- this.updateModelIncremental(incrementalResult.oldNode, incrementalResult.newNode, tr);
108
+ success = this.updateModelIncremental(incrementalResult.oldNode, incrementalResult.newNode, tr);
91
109
  }
92
110
  else {
93
111
  this.events.emit({ type: "full", transaction: tr });
94
112
  }
95
- if (this._state.cst && !this._state.cst.wellFormed) {
96
- this._state = this._state.update({ model: null });
97
- this.events.emit({ type: "full", transaction: tr });
113
+ if (success) {
114
+ if (this._state.cst && !this._state.cst.wellFormed) {
115
+ this._state = this._state.update({ model: null });
116
+ this.events.emit({ type: "full", transaction: tr });
117
+ }
118
+ handled = true;
119
+ }
120
+ else {
121
+ handled = false;
98
122
  }
99
- handled = true;
100
123
  }
101
124
  }
102
125
  if (!handled) {
@@ -112,18 +135,33 @@ class SyncEngine {
112
135
  * Update the source code (e.g. from text editor).
113
136
  * Handles history recording and incremental parsing.
114
137
  */
115
- updateSource(from, to, text) {
116
- const tr = new transaction_1.Transaction(this._state);
138
+ updateSource(from, to, text,
139
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
140
+ meta) {
141
+ const tr = new Transaction(this._state);
142
+ if (meta) {
143
+ for (const [key, value] of Object.entries(meta)) {
144
+ tr.setMeta(key, value);
145
+ }
146
+ }
117
147
  tr.replace(from, to, text);
118
148
  this.dispatch(tr);
119
149
  }
120
150
  /**
121
151
  * Apply a programmatic change derived from Model operations.
122
152
  * This is the "Application -> Source" flow.
153
+ * @deprecated Use `dispatch(new Transaction(state).replace(...))` instead.
123
154
  */
124
- applyPatch(start, end, text) {
155
+ applyPatch(start, end, text,
156
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
157
+ meta) {
125
158
  // Uses dispatch via updateSource logic, but conceptually distinct
126
- const tr = new transaction_1.Transaction(this._state);
159
+ const tr = new Transaction(this._state);
160
+ if (meta) {
161
+ for (const [key, value] of Object.entries(meta)) {
162
+ tr.setMeta(key, value);
163
+ }
164
+ }
127
165
  tr.replace(start, end, text);
128
166
  this.dispatch(tr);
129
167
  }
@@ -153,78 +191,80 @@ class SyncEngine {
153
191
  }
154
192
  }
155
193
  // --- High-Level Model Operations (delegated to Binder) ---
156
- setAttribute(modelNode, key, value) {
157
- if (!modelNode.cst)
158
- throw new Error("Model node not linked to CST");
159
- const patch = this.binder.calcSetAttributePatch(modelNode, key, value);
160
- if (patch) {
161
- this.applyPatch(patch.start, patch.end, patch.text);
194
+ /**
195
+ * @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).setAttribute(...))` instead.
196
+ */
197
+ setAttribute(modelNode, key, value,
198
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
199
+ meta) {
200
+ const builder = new TransactionBuilder(this._state, this.binder);
201
+ const tr = builder.setAttribute(modelNode, key, value);
202
+ if (meta) {
203
+ for (const [k, v] of Object.entries(meta)) {
204
+ tr.setMeta(k, v);
205
+ }
162
206
  }
207
+ this.dispatch(tr);
163
208
  }
164
- updateText(modelNode, text) {
165
- if (!modelNode.cst)
166
- throw new Error("Model node not linked to CST");
167
- const patch = this.binder.calcUpdateTextPatch(modelNode, text);
168
- if (patch) {
169
- this.applyPatch(patch.start, patch.end, patch.text);
209
+ /**
210
+ * @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).updateText(...))` instead.
211
+ */
212
+ updateText(modelNode, text,
213
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
214
+ meta) {
215
+ const builder = new TransactionBuilder(this._state, this.binder);
216
+ const tr = builder.updateText(modelNode, text);
217
+ if (meta) {
218
+ for (const [k, v] of Object.entries(meta)) {
219
+ tr.setMeta(k, v);
220
+ }
170
221
  }
222
+ this.dispatch(tr);
171
223
  }
172
- replaceNode(target, content) {
173
- var _a;
174
- if (!target.cst)
175
- throw new Error("Model node not linked to CST");
176
- // Formatting logic
177
- let indentUnit = " ";
178
- let currentIndent = "";
179
- if (target.cst) {
180
- currentIndent = this.detectIndent(target.cst);
181
- if ((_a = target.parent) === null || _a === void 0 ? void 0 : _a.cst) {
182
- const parentIndent = this.detectIndent(target.parent.cst);
183
- if (currentIndent.startsWith(parentIndent)) {
184
- const diff = currentIndent.slice(parentIndent.length);
185
- if (diff.length > 0 && !diff.includes("\n")) {
186
- indentUnit = diff;
187
- }
188
- }
224
+ /**
225
+ * @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).replaceNode(...))` instead.
226
+ */
227
+ replaceNode(target, content,
228
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
229
+ meta) {
230
+ const builder = new TransactionBuilder(this._state, this.binder);
231
+ const tr = builder.replaceNode(target, content);
232
+ if (meta) {
233
+ for (const [k, v] of Object.entries(meta)) {
234
+ tr.setMeta(k, v);
189
235
  }
190
236
  }
191
- const formatter = new formatter_1.Formatter({ indent: indentUnit });
192
- let newXml = formatter.format(content);
193
- if (currentIndent && newXml.includes("\n")) {
194
- newXml = newXml
195
- .split("\n")
196
- .map((line, index) => (index === 0 ? line : currentIndent + line))
197
- .join("\n");
198
- }
199
- const patch = this.binder.calcReplaceNodePatch(target, newXml);
200
- if (patch) {
201
- this.applyPatch(patch.start, patch.end, patch.text);
202
- }
237
+ this.dispatch(tr);
203
238
  }
204
- insertNode(parent, child, index) {
205
- if (!parent.cst)
206
- throw new Error("Parent node not linked to CST");
207
- // Determine basic indentation (simplistic)
208
- let indentUnit = " ";
209
- if (parent.cst) {
210
- const parentIndent = this.detectIndent(parent.cst);
211
- // Try to find a child to detect indent step
212
- // ... skipping complex logic for now
213
- }
214
- const formatter = new formatter_1.Formatter({ indent: indentUnit });
215
- const insertText = formatter.format(child);
216
- const patch = this.binder.calcInsertNodePatch(parent, index, insertText);
217
- if (patch) {
218
- this.applyPatch(patch.start, patch.end, patch.text);
239
+ /**
240
+ * @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).insertNode(...))` instead.
241
+ */
242
+ insertNode(parent, child, index,
243
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
244
+ meta) {
245
+ const builder = new TransactionBuilder(this._state, this.binder);
246
+ const tr = builder.insertNode(parent, child, index);
247
+ if (meta) {
248
+ for (const [k, v] of Object.entries(meta)) {
249
+ tr.setMeta(k, v);
250
+ }
219
251
  }
252
+ this.dispatch(tr);
220
253
  }
221
- removeNode(parent, child) {
222
- if (!child.cst)
223
- throw new Error("Target node not linked to CST");
224
- const patch = this.binder.calcRemoveNodePatch(child);
225
- if (patch) {
226
- this.applyPatch(patch.start, patch.end, patch.text);
254
+ /**
255
+ * @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).removeNode(...))` instead.
256
+ */
257
+ removeNode(parent, child,
258
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
259
+ meta) {
260
+ const builder = new TransactionBuilder(this._state, this.binder);
261
+ const tr = builder.removeNode(parent, child);
262
+ if (meta) {
263
+ for (const [k, v] of Object.entries(meta)) {
264
+ tr.setMeta(k, v);
265
+ }
227
266
  }
267
+ this.dispatch(tr);
228
268
  }
229
269
  // --- Internal Logic ---
230
270
  fullParse() {
@@ -232,10 +272,19 @@ class SyncEngine {
232
272
  const cst = this.parser.parse(this._state.source);
233
273
  let model = null;
234
274
  if (cst === null || cst === void 0 ? void 0 : cst.wellFormed) {
235
- // Hydrate full model
236
- const newModel = this.binder.hydrate(cst);
237
- if (newModel instanceof xml_api_model_1.ModelElement) {
238
- model = newModel;
275
+ if (this._state.model) {
276
+ // Attempt to reconcile with existing model to preserve identity
277
+ const result = this.binder.reconcile(this._state.model, cst);
278
+ if (result.node instanceof ModelElement) {
279
+ model = result.node;
280
+ }
281
+ }
282
+ else {
283
+ // Initial hydration
284
+ const newModel = this.binder.hydrate(cst);
285
+ if (newModel instanceof ModelElement) {
286
+ model = newModel;
287
+ }
239
288
  }
240
289
  }
241
290
  this._state = this._state.update({ cst, model });
@@ -283,24 +332,28 @@ class SyncEngine {
283
332
  return null;
284
333
  }
285
334
  updateModelIncremental(oldNode, newNode, tr) {
335
+ var _a, _b;
286
336
  const currentModel = this._state.model;
287
337
  if (!currentModel)
288
- return;
338
+ return false;
289
339
  if (currentModel.cst === oldNode) {
290
- const newModelNode = this.binder.hydrate(newNode);
291
- if (newModelNode instanceof xml_api_model_1.ModelElement) {
292
- this._state = this._state.update({ model: newModelNode });
293
- this.events.emit({
294
- type: "full",
295
- target: newModelNode,
296
- transaction: tr,
297
- });
340
+ // Reconcile root to preserve identity
341
+ const result = this.binder.reconcile(currentModel, newNode);
342
+ const reconciled = result.node;
343
+ if (reconciled !== currentModel) {
344
+ this._state = this._state.update({ model: reconciled });
298
345
  }
299
- return;
346
+ this.events.emit({
347
+ type: "full", // Or structure? Full implies root changed/updated
348
+ target: reconciled || undefined,
349
+ transaction: tr,
350
+ });
351
+ return true;
300
352
  }
301
353
  const modelPath = this.findModelNodePath(currentModel, oldNode);
302
354
  if (modelPath) {
303
- const reconciledModel = this.binder.reconcile(modelPath.node, newNode);
355
+ const result = this.binder.reconcile(modelPath.node, newNode);
356
+ const reconciledModel = result.node;
304
357
  if (reconciledModel) {
305
358
  if (reconciledModel !== modelPath.node) {
306
359
  modelPath.parent.children[modelPath.index] = reconciledModel;
@@ -310,8 +363,14 @@ class SyncEngine {
310
363
  type: "structure",
311
364
  target: reconciledModel,
312
365
  transaction: tr,
366
+ addedNodes: (_a = result.diff) === null || _a === void 0 ? void 0 : _a.addedNodes,
367
+ removedNodes: (_b = result.diff) === null || _b === void 0 ? void 0 : _b.removedNodes,
313
368
  });
314
369
  }
370
+ return true;
371
+ }
372
+ else {
373
+ return false;
315
374
  }
316
375
  }
317
376
  // --- Helpers ---
@@ -354,7 +413,7 @@ class SyncEngine {
354
413
  if (child.cst === cstNode) {
355
414
  return { parent: root, index: i, node: child };
356
415
  }
357
- if (child instanceof xml_api_model_1.ModelElement) {
416
+ if (child instanceof ModelElement) {
358
417
  const found = this.findModelNodePath(child, cstNode);
359
418
  if (found)
360
419
  return found;
@@ -383,19 +442,4 @@ class SyncEngine {
383
442
  current = current.parent;
384
443
  }
385
444
  }
386
- detectIndent(node) {
387
- const input = this._state.source;
388
- let i = node.start - 1;
389
- while (i >= 0) {
390
- if (input[i] === "\n") {
391
- return input.slice(i + 1, node.start);
392
- }
393
- if (input[i] !== " " && input[i] !== "\t") {
394
- return "";
395
- }
396
- i--;
397
- }
398
- return "";
399
- }
400
445
  }
401
- exports.SyncEngine = SyncEngine;
@@ -0,0 +1,14 @@
1
+ import { type ModelElement, type ModelNode } from "../model/xml-api-model";
2
+ import type { XMLBinder } from "../model/xml-binder";
3
+ import type { EditorState } from "./editor-state";
4
+ import { Transaction } from "./transaction";
5
+ export declare class TransactionBuilder {
6
+ private state;
7
+ private binder;
8
+ constructor(state: EditorState, binder: XMLBinder);
9
+ setAttribute(modelNode: ModelElement, key: string, value: string): Transaction;
10
+ updateText(modelNode: ModelElement, text: string): Transaction;
11
+ removeNode(parent: ModelElement, child: ModelNode): Transaction;
12
+ replaceNode(target: ModelNode, content: ModelNode): Transaction;
13
+ insertNode(parent: ModelElement, child: ModelNode, index: number): Transaction;
14
+ }
@@ -0,0 +1,143 @@
1
+ import { detectIndent } from "../cst/cst-utils";
2
+ import { Formatter } from "../model/formatter";
3
+ import { ModelText, } from "../model/xml-api-model";
4
+ import { Transaction } from "./transaction";
5
+ export class TransactionBuilder {
6
+ constructor(state, binder) {
7
+ this.state = state;
8
+ this.binder = binder;
9
+ }
10
+ setAttribute(modelNode, key, value) {
11
+ if (!modelNode.cst)
12
+ throw new Error("Model node not linked to CST");
13
+ const patch = this.binder.calcSetAttributePatch(modelNode, key, value);
14
+ const tr = new Transaction(this.state);
15
+ if (patch) {
16
+ tr.replace(patch.start, patch.end, patch.text);
17
+ }
18
+ return tr;
19
+ }
20
+ updateText(modelNode, text) {
21
+ if (!modelNode.cst)
22
+ throw new Error("Model node not linked to CST");
23
+ const patch = this.binder.calcUpdateTextPatch(modelNode, text);
24
+ const tr = new Transaction(this.state);
25
+ if (patch) {
26
+ tr.replace(patch.start, patch.end, patch.text);
27
+ }
28
+ return tr;
29
+ }
30
+ removeNode(parent, child) {
31
+ if (!parent.cst)
32
+ throw new Error("Parent node not linked to CST");
33
+ const patch = this.binder.calcRemoveNodePatch(child);
34
+ const tr = new Transaction(this.state);
35
+ if (patch) {
36
+ tr.replace(patch.start, patch.end, patch.text);
37
+ }
38
+ return tr;
39
+ }
40
+ replaceNode(target, content) {
41
+ var _a;
42
+ if (!target.cst)
43
+ throw new Error("Model node not linked to CST");
44
+ // Formatting logic
45
+ let indentUnit = " ";
46
+ let currentIndent = "";
47
+ if (target.cst) {
48
+ currentIndent = detectIndent(target.cst, this.state.source) || "";
49
+ if ((_a = target.parent) === null || _a === void 0 ? void 0 : _a.cst) {
50
+ const parentIndent = detectIndent(target.parent.cst, this.state.source) || "";
51
+ if (currentIndent.startsWith(parentIndent)) {
52
+ const diff = currentIndent.slice(parentIndent.length);
53
+ if (diff.length > 0 && !diff.includes("\n")) {
54
+ indentUnit = diff;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ const formatter = new Formatter({ indent: indentUnit });
60
+ let newXml = formatter.format(content);
61
+ if (currentIndent && newXml.includes("\n")) {
62
+ newXml = newXml
63
+ .split("\n")
64
+ .map((line, index) => (index === 0 ? line : currentIndent + line))
65
+ .join("\n");
66
+ }
67
+ const patch = this.binder.calcReplaceNodePatch(target, newXml);
68
+ const tr = new Transaction(this.state);
69
+ if (patch) {
70
+ tr.replace(patch.start, patch.end, patch.text);
71
+ }
72
+ return tr;
73
+ }
74
+ insertNode(parent, child, index) {
75
+ if (!parent.cst)
76
+ throw new Error("Parent node not linked to CST");
77
+ // Determine basic indentation
78
+ const indentUnit = " ";
79
+ // Smart Formatting: Determine baseIndent and prefix/suffix
80
+ let baseIndent = "";
81
+ let prefix = "";
82
+ let suffix = "";
83
+ // Check if child is already in model (DOM usage)
84
+ const isAlreadyInModel = parent.children[index] === child;
85
+ // Scan backwards for significant node
86
+ let probe = isAlreadyInModel ? index - 1 : index - 1;
87
+ let refNode = null;
88
+ let newlineFound = false;
89
+ while (probe >= 0) {
90
+ const node = parent.children[probe];
91
+ if (node instanceof ModelText && node.text.trim().length === 0) {
92
+ if (node.text.includes("\n"))
93
+ newlineFound = true;
94
+ probe--;
95
+ }
96
+ else {
97
+ refNode = node;
98
+ break;
99
+ }
100
+ }
101
+ if (refNode && refNode.formatting.indent !== null) {
102
+ baseIndent = refNode.formatting.indent;
103
+ prefix = newlineFound ? baseIndent : `\n${baseIndent}`;
104
+ }
105
+ else if (!refNode) {
106
+ // Empty or first significant child
107
+ // Check next sibling to decide mode
108
+ const nextNode = isAlreadyInModel
109
+ ? index + 1 < parent.children.length
110
+ ? parent.children[index + 1]
111
+ : null
112
+ : index < parent.children.length
113
+ ? parent.children[index]
114
+ : null;
115
+ if (nextNode && nextNode.formatting.indent === null) {
116
+ // Next is inline. Stay inline.
117
+ }
118
+ else {
119
+ // Next is block (or doesn't exist).
120
+ // If parent has indent, assume block.
121
+ if (parent.formatting.indent !== null) {
122
+ baseIndent = parent.formatting.indent + indentUnit;
123
+ prefix = `\n${baseIndent}`;
124
+ }
125
+ }
126
+ }
127
+ // Suffix logic: ensure closing tag is on new line if block mode
128
+ // If we are appending at the end, or next is end-tag
129
+ // Simple heuristic: if we added a newline prefix (block mode), add a newline suffix
130
+ if (prefix.includes("\n") || newlineFound) {
131
+ // Use parent's indent for the closing tag
132
+ suffix = `\n${parent.formatting.indent || ""}`;
133
+ }
134
+ const formatter = new Formatter({ indent: indentUnit, baseIndent });
135
+ const insertText = prefix + formatter.format(child) + suffix;
136
+ const patch = this.binder.calcInsertNodePatch(parent, index, insertText);
137
+ const tr = new Transaction(this.state);
138
+ if (patch) {
139
+ tr.replace(patch.start, patch.end, patch.text);
140
+ }
141
+ return tr;
142
+ }
143
+ }
@@ -14,7 +14,10 @@ export declare class Transaction {
14
14
  docChanged: boolean;
15
15
  /** Indicates if this transaction originated from a remote source (collaboration). */
16
16
  isRemote: boolean;
17
+ metadata: Map<string, any>;
17
18
  constructor(startState: EditorState);
19
+ setMeta(key: string, value: any): this;
20
+ getMeta(key: string): any;
18
21
  /**
19
22
  * Adds a text change to the transaction.
20
23
  * @param from Start index
@@ -1,20 +1,25 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Transaction = void 0;
4
1
  /**
5
2
  * Represents a unit of change to the editor state.
6
3
  * Currently focuses on text changes (patches).
7
4
  */
8
- class Transaction {
9
- // Placeholder for future selection and metadata
10
- // public selection: Selection | null = null;
11
- // public meta: Map<string, any> = new Map();
5
+ export class Transaction {
12
6
  constructor(startState) {
13
7
  this.startState = startState;
14
8
  this.patches = [];
15
9
  this.docChanged = false;
16
10
  /** Indicates if this transaction originated from a remote source (collaboration). */
17
11
  this.isRemote = false;
12
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
13
+ this.metadata = new Map();
14
+ }
15
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
16
+ setMeta(key, value) {
17
+ this.metadata.set(key, value);
18
+ return this;
19
+ }
20
+ // biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
21
+ getMeta(key) {
22
+ return this.metadata.get(key);
18
23
  }
19
24
  /**
20
25
  * Adds a text change to the transaction.
@@ -49,4 +54,3 @@ class Transaction {
49
54
  return source;
50
55
  }
51
56
  }
52
- exports.Transaction = Transaction;
@@ -1,4 +1,5 @@
1
1
  export interface Transaction {
2
+ timestamp: number;
2
3
  redo: {
3
4
  from: number;
4
5
  to: number;
@@ -14,8 +15,11 @@ export declare class HistoryManager {
14
15
  private undoStack;
15
16
  private redoStack;
16
17
  private maxHistory;
18
+ private mergeThreshold;
17
19
  constructor(maxHistory?: number);
18
20
  push(transaction: Transaction): void;
21
+ private shouldMerge;
22
+ private merge;
19
23
  undo(): Transaction | null;
20
24
  redo(): Transaction | null;
21
25
  canUndo(): boolean;