@fluid-experimental/tree 0.58.3000-61081 → 0.59.2000-61729

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +156 -43
  2. package/dist/ChangeTypes.d.ts +9 -14
  3. package/dist/ChangeTypes.d.ts.map +1 -1
  4. package/dist/ChangeTypes.js.map +1 -1
  5. package/dist/Checkout.d.ts.map +1 -1
  6. package/dist/Checkout.js +3 -2
  7. package/dist/Checkout.js.map +1 -1
  8. package/dist/LogViewer.d.ts +2 -3
  9. package/dist/LogViewer.d.ts.map +1 -1
  10. package/dist/LogViewer.js +5 -4
  11. package/dist/LogViewer.js.map +1 -1
  12. package/dist/NodeIdUtilities.d.ts +26 -11
  13. package/dist/NodeIdUtilities.d.ts.map +1 -1
  14. package/dist/NodeIdUtilities.js.map +1 -1
  15. package/dist/SharedTree.d.ts +61 -22
  16. package/dist/SharedTree.d.ts.map +1 -1
  17. package/dist/SharedTree.js +75 -30
  18. package/dist/SharedTree.js.map +1 -1
  19. package/dist/Transaction.d.ts +22 -4
  20. package/dist/Transaction.d.ts.map +1 -1
  21. package/dist/Transaction.js +27 -11
  22. package/dist/Transaction.js.map +1 -1
  23. package/dist/TransactionInternal.d.ts +3 -6
  24. package/dist/TransactionInternal.d.ts.map +1 -1
  25. package/dist/TransactionInternal.js +8 -4
  26. package/dist/TransactionInternal.js.map +1 -1
  27. package/dist/TreeCompressor.d.ts +0 -1
  28. package/dist/TreeCompressor.d.ts.map +1 -1
  29. package/dist/TreeCompressor.js +31 -26
  30. package/dist/TreeCompressor.js.map +1 -1
  31. package/dist/TreeView.d.ts.map +1 -1
  32. package/dist/TreeView.js +3 -2
  33. package/dist/TreeView.js.map +1 -1
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/docs/Write-Format.md +19 -0
  38. package/lib/ChangeTypes.d.ts +9 -14
  39. package/lib/ChangeTypes.d.ts.map +1 -1
  40. package/lib/ChangeTypes.js.map +1 -1
  41. package/lib/Checkout.d.ts.map +1 -1
  42. package/lib/Checkout.js +3 -2
  43. package/lib/Checkout.js.map +1 -1
  44. package/lib/LogViewer.d.ts +2 -3
  45. package/lib/LogViewer.d.ts.map +1 -1
  46. package/lib/LogViewer.js +5 -4
  47. package/lib/LogViewer.js.map +1 -1
  48. package/lib/NodeIdUtilities.d.ts +26 -11
  49. package/lib/NodeIdUtilities.d.ts.map +1 -1
  50. package/lib/NodeIdUtilities.js.map +1 -1
  51. package/lib/SharedTree.d.ts +61 -22
  52. package/lib/SharedTree.d.ts.map +1 -1
  53. package/lib/SharedTree.js +75 -30
  54. package/lib/SharedTree.js.map +1 -1
  55. package/lib/Transaction.d.ts +22 -4
  56. package/lib/Transaction.d.ts.map +1 -1
  57. package/lib/Transaction.js +26 -10
  58. package/lib/Transaction.js.map +1 -1
  59. package/lib/TransactionInternal.d.ts +3 -6
  60. package/lib/TransactionInternal.d.ts.map +1 -1
  61. package/lib/TransactionInternal.js +8 -4
  62. package/lib/TransactionInternal.js.map +1 -1
  63. package/lib/TreeCompressor.d.ts +0 -1
  64. package/lib/TreeCompressor.d.ts.map +1 -1
  65. package/lib/TreeCompressor.js +31 -26
  66. package/lib/TreeCompressor.js.map +1 -1
  67. package/lib/TreeView.d.ts.map +1 -1
  68. package/lib/TreeView.js +3 -2
  69. package/lib/TreeView.js.map +1 -1
  70. package/lib/index.d.ts +1 -1
  71. package/lib/index.d.ts.map +1 -1
  72. package/lib/index.js.map +1 -1
  73. package/lib/test/LogViewer.tests.js +13 -10
  74. package/lib/test/LogViewer.tests.js.map +1 -1
  75. package/lib/test/Transaction.tests.js +36 -2
  76. package/lib/test/Transaction.tests.js.map +1 -1
  77. package/lib/test/TreeView.tests.js +29 -0
  78. package/lib/test/TreeView.tests.js.map +1 -1
  79. package/lib/test/Virtualization.tests.js +1 -2
  80. package/lib/test/Virtualization.tests.js.map +1 -1
  81. package/lib/test/fuzz/SharedTreeFuzzTests.d.ts.map +1 -1
  82. package/lib/test/fuzz/SharedTreeFuzzTests.js +19 -2
  83. package/lib/test/fuzz/SharedTreeFuzzTests.js.map +1 -1
  84. package/lib/test/utilities/SharedTreeVersioningTests.d.ts.map +1 -1
  85. package/lib/test/utilities/SharedTreeVersioningTests.js +26 -1
  86. package/lib/test/utilities/SharedTreeVersioningTests.js.map +1 -1
  87. package/package.json +18 -18
  88. package/src/ChangeTypes.ts +9 -14
  89. package/src/Checkout.ts +3 -3
  90. package/src/LogViewer.ts +4 -4
  91. package/src/NodeIdUtilities.ts +26 -11
  92. package/src/SharedTree.ts +81 -28
  93. package/src/Transaction.ts +39 -13
  94. package/src/TransactionInternal.ts +9 -10
  95. package/src/TreeCompressor.ts +31 -40
  96. package/src/TreeView.ts +3 -2
  97. package/src/index.ts +0 -1
  98. package/docs/Future.md +0 -155
package/src/SharedTree.ts CHANGED
@@ -110,13 +110,10 @@ export class SharedTreeFactory implements IChannelFactory {
110
110
 
111
111
  /**
112
112
  * Get a factory for SharedTree to register with the data store.
113
- * @param writeFormat - Determines the format version the SharedTree will write summaries in.
114
- * This format may be updated to a newer (supported) version at runtime if a collaborating shared-tree
115
- * that was initialized with a newer write version connects to the session. Care must be taken when changing this value,
116
- * as a staged rollout must have occurred such that all collaborating clients must have the code to read at least the version
117
- * written.
113
+ * @param writeFormat - Determines the format version the SharedTree will write ops and summaries in. See [the write format
114
+ * documentation](../docs/Write-Format.md) for more information.
118
115
  * @param summarizeHistory - Determines if the history is included in summaries and if edit chunks are uploaded when they are full.
119
- * See docs/Breaking-Change-Migration for more details on this scheme.
116
+ * See the [breaking change migration documentation](docs/Breaking-Change-Migration) for more details on this scheme.
120
117
  * @param expensiveValidation - Enables expensive asserts on SharedTree.
121
118
  * @returns A factory that creates `SharedTree`s and loads them from storage.
122
119
  */
@@ -270,7 +267,7 @@ export type SequencedEditAppliedHandler = (args: SequencedEditAppliedEventArgume
270
267
  const sharedTreeTelemetryProperties: ITelemetryLoggerPropertyBags = { all: { isSharedTreeEvent: true } };
271
268
 
272
269
  /**
273
- * A distributed tree.
270
+ * A [distributed tree](../Readme.md).
274
271
  * @public
275
272
  */
276
273
  export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeIdContext {
@@ -284,6 +281,7 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
284
281
  /**
285
282
  * Get a factory for SharedTree to register with the data store.
286
283
  * @param summarizeHistory - Determines if the history is included in summaries and if edit chunks are uploaded when they are full.
284
+ *
287
285
  * On 0.1.1 documents, due to current code limitations, this parameter is only impactful for newly created documents.
288
286
  * `SharedTree`s which load existing documents will summarize history if and only if the loaded summary included history.
289
287
  *
@@ -291,17 +289,22 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
291
289
  * In the future we may allow modification of whether or not a particular document saves history, but only via a consensus mechanism.
292
290
  * See the skipped test in SharedTreeFuzzTests.ts for more details on this issue.
293
291
  * See docs/Breaking-Change-Migration for more details on the consensus scheme.
294
- * @param writeFormat - Determines the format version the SharedTree will write summaries in.
292
+ * @param writeFormat - Determines the format version the SharedTree will write ops and summaries in.
295
293
  * This format may be updated to a newer (supported) version at runtime if a collaborating shared-tree
296
294
  * that was initialized with a newer write version connects to the session. Care must be taken when changing this value,
297
295
  * as a staged rollout must of occurred such that all collaborating clients must have the code to read at least the version
298
296
  * written.
297
+ * See [the write format documentation](../docs/Write-Format.md) for more information.
299
298
  * @returns A factory that creates `SharedTree`s and loads them from storage.
300
299
  */
301
300
  public static getFactory(
302
301
  writeFormat: WriteFormat,
303
302
  summarizeHistory: false | { uploadEditChunks: boolean } = false
304
303
  ): SharedTreeFactory {
304
+ // On 0.1.1 documents, due to current code limitations, all clients MUST agree on the value of `summarizeHistory`.
305
+ // Note that this means staged rollout changing this value should not be attempted.
306
+ // It is possible to update shared-tree to correctly handle such a staged rollout, but that hasn't been implemented.
307
+ // See the skipped test in SharedTreeFuzzTests.ts for more details on this issue.
305
308
  return new SharedTreeFactory(writeFormat, summarizeHistory);
306
309
  }
307
310
 
@@ -368,8 +371,6 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
368
371
  this.emit(SharedTreeEvent.SequencedEditApplied, eventArguments);
369
372
  };
370
373
 
371
- public readonly transactionFactory = TransactionInternal.factory;
372
-
373
374
  private summarizeHistory: boolean;
374
375
  private uploadEditChunks: boolean;
375
376
 
@@ -377,7 +378,8 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
377
378
  * Create a new SharedTreeFactory.
378
379
  * @param runtime - The runtime the SharedTree will be associated with
379
380
  * @param id - Unique ID for the SharedTree
380
- * @param writeFormat - Determines the format version the SharedTree will write summaries in.
381
+ * @param writeFormat - Determines the format version the SharedTree will write ops and summaries in. See [the write format
382
+ * documentation](../docs/Write-Format.md) for more information.
381
383
  * @param summarizeHistory - Determines if the history is included in summaries and if edit chunks are uploaded when they are full.
382
384
  * @param expensiveValidation - Enable expensive asserts.
383
385
  */
@@ -490,31 +492,64 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
490
492
  }
491
493
 
492
494
  /**
493
- * {@inheritdoc NodeIdGenerator.generateNodeId}
495
+ * Generates a node identifier.
496
+ * The returned IDs may be used as the identifier of a node in the SharedTree.
497
+ * `NodeId`s are *always* unique and stable within the scope of the tree and session that generated them. They are *not* unique within
498
+ * a Fluid container, and *cannot* be compared across instances of a SharedTree. They are *not* stable across sessions/lifetimes of a
499
+ * SharedTree, and *cannot* be persisted (e.g. stored in payloads, uploaded in blobs, etc.). If stable persistence is needed,
500
+ * NodeIdConverter.convertToStableNodeId may be used to return a corresponding UUID that is globally unique and stable.
501
+ * @param override - if supplied, calls to `convertToStableNodeId` using the returned node ID will return the override instead of
502
+ * the UUID. Calls to `generateNodeId` with the same override always return the same ID. Performance note: passing an override string
503
+ * incurs a storage cost that is significantly higher that a node ID without one, and should be avoided if possible.
494
504
  * @public
495
505
  */
496
506
  public generateNodeId(override?: string): NodeId {
497
507
  return this.idCompressor.generateCompressedId(override) as NodeId;
498
508
  }
499
509
 
500
- /** {@inheritdoc NodeIdConverter.convertToStableNodeId} */
510
+ /**
511
+ * Given a NodeId, returns the corresponding stable ID or throws if the supplied node ID was not generated with this tree (`NodeId`s
512
+ * may not be used across SharedTree instances, see `generateNodeId` for more).
513
+ * The returned value will be a UUID, unless the creation of `id` used an override string (see `generateNodeId` for more).
514
+ * The result is safe to persist and re-use across `SharedTree` instances, unlike `NodeId`.
515
+ * @public
516
+ */
501
517
  public convertToStableNodeId(id: NodeId): StableNodeId {
502
518
  return (this.idCompressor.tryDecompress(id) as StableNodeId) ?? fail('Node id is not known to this SharedTree');
503
519
  }
504
520
 
505
- /** {@inheritdoc NodeIdConverter.tryConvertToStableNodeId} */
521
+ /**
522
+ * Given a NodeId, attempt to return the corresponding stable ID.
523
+ * The returned value will be a UUID, unless the creation of `id` used an override string (see `generateNodeId` for more).
524
+ * The returned stable ID is undefined if `id` was never created with this SharedTree. If a stable ID is returned, this does not imply
525
+ * that there is a node with `id` in the current revision of the tree, only that `id` was at some point generated by some instance of
526
+ * this tree.
527
+ * @public
528
+ */
506
529
  public tryConvertToStableNodeId(id: NodeId): StableNodeId | undefined {
507
530
  return this.idCompressor.tryDecompress(id) as StableNodeId | undefined;
508
531
  }
509
532
 
510
- /** {@inheritdoc NodeIdConverter.convertToNodeId} */
533
+ /**
534
+ * Given a stable ID, return the corresponding NodeId or throws if the supplied stable ID was never generated with this tree, either
535
+ * as a UUID corresponding to a `NodeId` or as an override passed to `generateNodeId`.
536
+ * If a stable ID is returned, this does not imply that there is a node with `id` in the current revision of the tree, only that
537
+ * `id` was at some point generated by an instance of this SharedTree.
538
+ * @public
539
+ */
511
540
  public convertToNodeId(id: StableNodeId): NodeId {
512
541
  return (
513
542
  (this.idCompressor.tryRecompress(id) as NodeId) ?? fail('Stable node id is not known to this SharedTree')
514
543
  );
515
544
  }
516
545
 
517
- /** {@inheritdoc NodeIdConverter.tryConvertToNodeId} */
546
+ /**
547
+ * Given a stable ID, return the corresponding NodeId or return undefined if the supplied stable ID was never generated with this tree,
548
+ * either as a UUID corresponding to a `NodeId` or as an override passed to `generateNodeId`.
549
+ * If a stable ID is returned, this does not imply that there is a node with `id` in the current revision of the tree, only that
550
+ * `id` was at some point generated by an instance of this SharedTree.
551
+ * @public
552
+ */
518
553
  public tryConvertToNodeId(id: StableNodeId): NodeId | undefined {
519
554
  return this.idCompressor.tryRecompress(id) as NodeId | undefined;
520
555
  }
@@ -835,7 +870,6 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
835
870
  this.expensiveValidation,
836
871
  editStatusCallback,
837
872
  sequencedEditResultCallback,
838
- this.transactionFactory,
839
873
  0
840
874
  );
841
875
 
@@ -863,7 +897,7 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
863
897
  }
864
898
 
865
899
  /**
866
- * Compares this shared tree to another for equality. Should only be used for correctness testing.
900
+ * Compares this shared tree to another for equality. Should only be used for internal correctness testing.
867
901
  *
868
902
  * Equality means that the histories as captured by the EditLogs are equivalent.
869
903
  *
@@ -942,7 +976,9 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
942
976
  }
943
977
  // TODO: This cast can be removed on typescript 4.6
944
978
  const edit = this.parseSequencedEdit(op as SharedTreeEditOp | SharedTreeEditOp_0_0_2);
945
- this.internStringsFromEdit(edit);
979
+ if (op.version === WriteFormat.v0_1_1) {
980
+ this.internStringsFromEdit(edit);
981
+ }
946
982
  this.processSequencedEdit(edit, typedMessage);
947
983
  }
948
984
  } else if (type === SharedTreeOpType.Update) {
@@ -1083,9 +1119,8 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
1083
1119
  }
1084
1120
 
1085
1121
  private upgradeFrom_0_0_2_to_0_1_1(): void {
1086
- for (let i = 0; i < this.editLog.numberOfSequencedEdits; i++) {
1087
- this.internStringsFromEdit(this.editsInternal.getEditInSessionAtIndex(i));
1088
- }
1122
+ // Reset the string interner, re-populate only with information that there is consensus on
1123
+ this.interner = new MutableStringInterner([initialTree.definition]);
1089
1124
  const oldIdCompressor = this.idCompressor;
1090
1125
  // Create the IdCompressor that will be used after the upgrade
1091
1126
  const newIdCompressor = new IdCompressor(createSessionId(), reservedIdCount); // TODO: attribution info
@@ -1109,11 +1144,20 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
1109
1144
  // All clients have the full history, and can therefore all "generate" the same final IDs for every ID in the history
1110
1145
  // via the ghost compressor.
1111
1146
  unifyHistoricalIds(ghostContext);
1147
+ // The same logic applies to string interning, so intern all the strings in the history (superset of those in the current view)
1148
+ for (let i = 0; i < this.editLog.numberOfSequencedEdits; i++) {
1149
+ this.internStringsFromEdit(this.editsInternal.getEditInSessionAtIndex(i));
1150
+ }
1112
1151
  } else {
1113
1152
  // Clients do not have the full history, but all share the same current view (sequenced). They can all finalize the same final
1114
1153
  // IDs for every ID in the view via the ghost compressor.
1154
+ // The same logic applies for the string interner.
1115
1155
  for (const node of this.logViewer.getRevisionViewInSession(this.editLog.numberOfSequencedEdits)) {
1116
1156
  ghostContext.generateNodeId(this.convertToStableNodeId(node.identifier));
1157
+ this.interner.getOrCreateInternedId(node.definition);
1158
+ for (const label of [...node.traits.keys()].sort()) {
1159
+ this.interner.getOrCreateInternedId(label);
1160
+ }
1117
1161
  }
1118
1162
  // Every node in this client's history can simply be generated in the new compressor as well, preserving the UUID
1119
1163
  unifyHistoricalIds(newContext);
@@ -1124,12 +1168,16 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
1124
1168
  }
1125
1169
 
1126
1170
  /**
1127
- * Add an `Edit` directly.
1128
- * External users should use one of the more specialized functions, like applyEdit which handles constructing the actual `Edit` object.
1129
- * This is exposed as it is useful for testing, particularly with invalid and malformed Edits.
1130
- * @internal
1171
+ * Applies a set of changes to this tree. The result will be reflected in `SharedTree.currentView`.
1172
+ * This method does not allow for snapshot isolation, as the changes are always applied to the most recent revision.
1173
+ * If it is desireable to read from and apply changes to a fixed view that does not change when remote changes arrive, `Checkout`
1174
+ * should be used instead.
1175
+ * @public
1131
1176
  */
1132
- public applyEdit(...changes: readonly Change[]): Edit<unknown> {
1177
+ public applyEdit(...changes: Change[]): Edit<unknown>;
1178
+ public applyEdit(changes: Change[]): Edit<unknown>;
1179
+ public applyEdit(headOrChanges: Change | Change[], ...tail: Change[]): Edit<unknown> {
1180
+ const changes = Array.isArray(headOrChanges) ? headOrChanges : [headOrChanges, ...tail];
1133
1181
  const id = newEditId();
1134
1182
  const internalEdit: Edit<ChangeInternal> = {
1135
1183
  id,
@@ -1141,6 +1189,10 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
1141
1189
  }
1142
1190
 
1143
1191
  /**
1192
+ * Applies a set of internal changes to this tree. The result will be reflected in `SharedTree.currentView`.
1193
+ * External users should use one of the more specialized functions, like `applyEdit` which handles constructing the actual `Edit`
1194
+ * and uses public Change types.
1195
+ * This is exposed for internal use only.
1144
1196
  * @internal
1145
1197
  */
1146
1198
  public applyEditInternal(editOrChanges: Edit<ChangeInternal> | readonly ChangeInternal[]): Edit<ChangeInternal> {
@@ -1157,7 +1209,8 @@ export class SharedTree extends SharedObject<ISharedTreeEvents> implements NodeI
1157
1209
  }
1158
1210
 
1159
1211
  /**
1160
- * Given a newly constructed edit, add any necessary metadata
1212
+ * Converts a public Change type to an internal representation.
1213
+ * This is exposed for internal use only.
1161
1214
  * @internal
1162
1215
  */
1163
1216
  public internalizeChange(change: Change): ChangeInternal {
@@ -3,6 +3,8 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import { IErrorEvent } from '@fluidframework/common-definitions';
7
+ import { TypedEventEmitter } from '@fluidframework/common-utils';
6
8
  import { ChangeInternal, Edit, EditStatus } from './persisted-types';
7
9
  import { newEditId } from './EditUtilities';
8
10
  import { TreeView } from './TreeView';
@@ -11,16 +13,32 @@ import { SharedTree } from './SharedTree';
11
13
  import { GenericTransaction, TransactionInternal } from './TransactionInternal';
12
14
  import { CachingLogViewer } from './LogViewer';
13
15
 
16
+ /**
17
+ * An event emitted by a `Transaction` to indicate a state change. See {@link TransactionEvents} for event argument information.
18
+ * @public
19
+ */
20
+ export enum TransactionEvent {
21
+ /**
22
+ * `currentView` has changed from `before` to `after`
23
+ */
24
+ ViewChange = 'viewChange',
25
+ }
26
+
27
+ /**
28
+ * Events which may be emitted by `Transaction`
29
+ */
30
+ export interface TransactionEvents extends IErrorEvent {
31
+ (event: TransactionEvent.ViewChange, listener: (before: TreeView, after: TreeView) => void);
32
+ }
33
+
14
34
  /**
15
35
  * Buffers changes to be applied to an isolated view of a `SharedTree` over time before applying them directly to the tree itself as a
16
36
  * single edit
17
37
  */
18
- export class Transaction {
38
+ export class Transaction extends TypedEventEmitter<TransactionEvents> {
19
39
  /** The view of the tree when this transaction was created */
20
40
  public readonly startingView: TreeView;
21
-
22
41
  private readonly transaction: GenericTransaction;
23
- private open: boolean = true;
24
42
 
25
43
  /**
26
44
  * Create a new transaction over the given tree. The tree's `currentView` at this time will become the `startingView` for this
@@ -28,6 +46,7 @@ export class Transaction {
28
46
  * @param tree - the `SharedTree` that this transaction applies changes to
29
47
  */
30
48
  public constructor(public readonly tree: SharedTree) {
49
+ super();
31
50
  const { currentView } = tree;
32
51
  this.transaction = new GenericTransaction(currentView, new TransactionInternal.Policy());
33
52
  this.startingView = currentView;
@@ -38,7 +57,7 @@ export class Transaction {
38
57
  * be automatically closed by a change in this transaction failing to apply (see `applyChange()`).
39
58
  */
40
59
  public get isOpen(): boolean {
41
- return this.open && this.status === EditStatus.Applied;
60
+ return this.transaction.isOpen && this.status === EditStatus.Applied;
42
61
  }
43
62
 
44
63
  /**
@@ -59,18 +78,26 @@ export class Transaction {
59
78
  /**
60
79
  * Attempt to apply a sequence of changes in this transaction. The `currentView` will be updated to reflect the new tree state after all
61
80
  * applied changes. If any change fails to apply, the remaining changes will be ignored and this transaction will be automatically
62
- * closed (see `isOpen`). If this transaction is already closed, this method has no effect.
81
+ * closed (see `isOpen`). If this transaction is already closed, this method has no effect. This method will emit a
82
+ * `TransactionEvent.ViewChange` event at most once per call.
63
83
  * @param changes - the changes to apply
64
84
  * @returns either the `EditStatus` of the given changes or the `EditStatus` of the last change before the transaction was closed
65
85
  */
66
- public apply(change: Change, ...changes: Change[]): EditStatus;
86
+ public apply(...changes: Change[]): EditStatus;
67
87
  public apply(changes: Change[]): EditStatus;
68
- public apply(changeOrChanges: Change | Change[], ...changes: Change[]): EditStatus {
88
+ public apply(headOrChanges: Change | Change[], ...tail: Change[]): EditStatus {
69
89
  if (this.isOpen) {
70
- const changeArray = Array.isArray(changeOrChanges)
71
- ? [...changeOrChanges, ...changes]
72
- : [changeOrChanges, ...changes];
73
- this.transaction.applyChanges(changeArray.map((c) => this.tree.internalizeChange(c)));
90
+ const changes = Array.isArray(headOrChanges) ? headOrChanges : [headOrChanges, ...tail];
91
+ if (changes.length > 0) {
92
+ const previousView = this.currentView;
93
+ this.transaction.applyChanges(changes.map((c) => this.tree.internalizeChange(c)));
94
+ if (
95
+ this.listenerCount(TransactionEvent.ViewChange) > 0 &&
96
+ !previousView.hasEqualForest(this.currentView)
97
+ ) {
98
+ this.emit(TransactionEvent.ViewChange, previousView, this.currentView);
99
+ }
100
+ }
74
101
  }
75
102
  return this.status;
76
103
  }
@@ -81,14 +108,13 @@ export class Transaction {
81
108
  public closeAndCommit(): void {
82
109
  if (this.isOpen) {
83
110
  if (this.transaction.changes.length > 0) {
84
- const edit: Edit<ChangeInternal> = { id: newEditId(), changes: this.transaction.changes };
85
111
  const result = this.transaction.close();
112
+ const edit: Edit<ChangeInternal> = { id: newEditId(), changes: result.changes };
86
113
  if (this.tree.editsInternal instanceof CachingLogViewer) {
87
114
  this.tree.editsInternal.setKnownEditingResult(edit, result);
88
115
  }
89
116
  this.tree.applyEditInternal(edit);
90
117
  }
91
- this.open = false;
92
118
  }
93
119
  }
94
120
  }
@@ -34,12 +34,6 @@ import { RevisionView, TransactionView } from './RevisionView';
34
34
  import { ReconciliationChange, ReconciliationPath } from './ReconciliationPath';
35
35
  import { TreeViewNode } from './TreeView';
36
36
 
37
- /**
38
- * A function which can produce a Transaction from a RevisionView
39
- * @public
40
- */
41
- export type TransactionFactory = (view: RevisionView) => GenericTransaction;
42
-
43
37
  /**
44
38
  * Result of applying a transaction.
45
39
  * @public
@@ -192,7 +186,7 @@ export class GenericTransaction {
192
186
  private readonly policy: GenericTransactionPolicy;
193
187
  protected readonly before: RevisionView;
194
188
  private state: TransactionState;
195
- private isOpen = true;
189
+ private open = true;
196
190
 
197
191
  /**
198
192
  * Create and open an edit of the provided `TreeView`. After applying 0 or more changes, this editor should be closed via `close()`.
@@ -209,6 +203,11 @@ export class GenericTransaction {
209
203
  };
210
204
  }
211
205
 
206
+ /** Whether or not this transaction has been closed via `close()` */
207
+ public get isOpen(): boolean {
208
+ return this.open;
209
+ }
210
+
212
211
  /**
213
212
  * The most up-to-date `TreeView` for this edit. This is the state of the tree after all changes applied so far.
214
213
  */
@@ -239,8 +238,8 @@ export class GenericTransaction {
239
238
 
240
239
  /** @returns the final `EditStatus` and `TreeView` after all changes are applied. */
241
240
  public close(): EditingResult {
242
- assert(this.isOpen, 'transaction has already been closed');
243
- this.isOpen = false;
241
+ assert(this.open, 'transaction has already been closed');
242
+ this.open = false;
244
243
  if (this.state.status === EditStatus.Applied) {
245
244
  const validation = this.policy.validateOnClose(this.state);
246
245
  if (Result.isOk(validation)) {
@@ -337,7 +336,7 @@ export class GenericTransaction {
337
336
  * @returns this
338
337
  */
339
338
  public applyChange(change: ChangeInternal, path: ReconciliationPath = []): this {
340
- assert(this.isOpen, 'Editor must be open to apply changes.');
339
+ assert(this.open, 'Editor must be open to apply changes.');
341
340
  if (this.state.status !== EditStatus.Applied) {
342
341
  fail('Cannot apply change to an edit unless all previous changes have applied');
343
342
  }
@@ -7,7 +7,7 @@ import type { Definition, DetachedSequenceId, InternedStringId, OpSpaceNodeId, T
7
7
  import type { StringInterner } from './StringInterner';
8
8
  import type { CompressedTraits, CompressedPlaceholderTree, PlaceholderTree, Payload } from './persisted-types';
9
9
  import type { ContextualizedNodeIdNormalizer } from './NodeIdUtilities';
10
- import { assert, fail } from './Common';
10
+ import { assert, fail, Mutable } from './Common';
11
11
 
12
12
  /**
13
13
  * Compresses a given {@link PlaceholderTree} into a more compact serializable format.
@@ -122,22 +122,21 @@ export class InterningTreeCompressor<TPlaceholder extends DetachedSequenceId | n
122
122
  }
123
123
 
124
124
  let compressedId: TId | undefined;
125
- let compressedTraits: CompressedTraits<TId, TPlaceholder> | undefined;
125
+ let compressedTraits:
126
+ | [Payload, ...CompressedTraits<TId, TPlaceholder>]
127
+ | CompressedTraits<TId, TPlaceholder>
128
+ | undefined;
126
129
  let payload: Payload | undefined;
127
130
  const [maybeInternedDefinition, idOrPayloadTraits, payloadTraits] = node;
128
131
  if (idOrPayloadTraits !== undefined) {
129
132
  if (typeof idOrPayloadTraits === 'number') {
130
133
  compressedId = idOrPayloadTraits;
131
134
  if (payloadTraits !== undefined) {
132
- ({ compressedTraits, payload } = this.parseTraitsAndPayload(payloadTraits));
135
+ compressedTraits = payloadTraits;
133
136
  }
134
137
  } else {
135
138
  // TODO: This cast can be removed on typescript 4.6
136
- ({ compressedTraits, payload } = this.parseTraitsAndPayload(
137
- idOrPayloadTraits as
138
- | [Payload, ...CompressedTraits<TId, TPlaceholder>]
139
- | CompressedTraits<TId, TPlaceholder>
140
- ));
139
+ compressedTraits = idOrPayloadTraits as typeof compressedTraits;
141
140
  }
142
141
  }
143
142
 
@@ -146,14 +145,30 @@ export class InterningTreeCompressor<TPlaceholder extends DetachedSequenceId | n
146
145
  ? maybeInternedDefinition
147
146
  : // TODO: This cast can be removed on typescript 4.6
148
147
  (interner.getString(maybeInternedDefinition as number) as Definition);
149
- const identifier = compressedId ?? (getNextElidedId(this.previousId ?? fail()) as TId);
148
+
149
+ let identifier: TId;
150
+ if (compressedId !== undefined) {
151
+ identifier = compressedId;
152
+ } else {
153
+ const prevId = this.previousId ?? fail();
154
+ identifier = prevId < 0 ? ((prevId - 1) as TId) : (((prevId as number) + 1) as TId);
155
+ }
150
156
  this.previousId = identifier;
151
157
 
152
158
  const traits = {};
153
159
  if (compressedTraits !== undefined) {
154
- for (let i = 0; i < Object.entries(compressedTraits).sort().length; i += 2) {
155
- const maybeCompressedLabel = compressedTraits[i] as InternedStringId;
156
- const compressedChildren = compressedTraits[i + 1] as (
160
+ let offset: number;
161
+ if (compressedTraits.length % 2 === 1) {
162
+ offset = 1;
163
+ payload = compressedTraits[0];
164
+ } else {
165
+ offset = 0;
166
+ }
167
+ const traitsLength = compressedTraits.length - offset;
168
+ for (let i = 0; i < traitsLength; i += 2) {
169
+ const offsetIndex = i + offset;
170
+ const maybeCompressedLabel = compressedTraits[offsetIndex] as InternedStringId;
171
+ const compressedChildren = compressedTraits[offsetIndex + 1] as (
157
172
  | TPlaceholder
158
173
  | CompressedPlaceholderTree<TId, TPlaceholder>
159
174
  )[];
@@ -170,42 +185,18 @@ export class InterningTreeCompressor<TPlaceholder extends DetachedSequenceId | n
170
185
  }
171
186
  }
172
187
 
173
- const decompressedNode = {
188
+ const decompressedNode: Mutable<PlaceholderTree<TPlaceholder>> = {
174
189
  identifier: idNormalizer.normalizeToSessionSpace(identifier),
175
190
  definition,
176
191
  traits,
177
- ...(payload !== undefined ? { payload } : {}),
178
192
  };
179
193
 
180
- return decompressedNode;
181
- }
182
-
183
- private parseTraitsAndPayload<TId extends OpSpaceNodeId>(
184
- traitsAndPayload: [Payload, ...CompressedTraits<TId, TPlaceholder>] | CompressedTraits<TId, TPlaceholder>
185
- ): {
186
- compressedTraits: CompressedTraits<TId, TPlaceholder>;
187
- payload?: Payload;
188
- } {
189
- if (traitsAndPayload.length % 2 === 1) {
190
- return {
191
- compressedTraits: traitsAndPayload.slice(1),
192
- payload: traitsAndPayload[0],
193
- };
194
+ if (payload !== undefined) {
195
+ decompressedNode.payload = payload;
194
196
  }
195
197
 
196
- return {
197
- compressedTraits: traitsAndPayload,
198
- };
199
- }
200
- }
201
-
202
- function getNextElidedId<TId extends OpSpaceNodeId>(id: TId): TId {
203
- const numericId: number = id;
204
- if (numericId < 0) {
205
- return (numericId - 1) as TId;
198
+ return decompressedNode;
206
199
  }
207
-
208
- return (numericId + 1) as TId;
209
200
  }
210
201
 
211
202
  function canElideId<TId extends OpSpaceNodeId>(previousId: TId | undefined, id: TId): boolean {
package/src/TreeView.ts CHANGED
@@ -291,8 +291,9 @@ export abstract class TreeView {
291
291
 
292
292
  private *iterateNodeDescendants(node: TreeViewNode): IterableIterator<TreeViewNode> {
293
293
  yield node;
294
- for (const trait of Array.from(node.traits.values()).sort()) {
295
- for (const childId of trait) {
294
+ for (const label of [...node.traits.keys()].sort()) {
295
+ const trait = node.traits.get(label);
296
+ for (const childId of trait ?? fail('Expected trait with label')) {
296
297
  const child = this.getViewNode(childId);
297
298
  yield* this.iterateNodeDescendants(child);
298
299
  }
package/src/index.ts CHANGED
@@ -111,7 +111,6 @@ export {
111
111
  export { comparePayloads } from './PayloadUtilities';
112
112
  export {
113
113
  TransactionInternal,
114
- TransactionFactory,
115
114
  GenericTransaction,
116
115
  GenericTransactionPolicy,
117
116
  EditingResult,