@y/y 14.0.0-19 → 14.0.0-21

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 (135) hide show
  1. package/README.md +7 -5
  2. package/dist/src/index.d.ts +2 -1
  3. package/dist/src/internals.d.ts +2 -9
  4. package/dist/src/structs/ContentType.d.ts +6 -12
  5. package/dist/src/structs/ContentType.d.ts.map +1 -1
  6. package/dist/src/structs/Item.d.ts +5 -6
  7. package/dist/src/structs/Item.d.ts.map +1 -1
  8. package/dist/src/utils/AttributionManager.d.ts +16 -14
  9. package/dist/src/utils/AttributionManager.d.ts.map +1 -1
  10. package/dist/src/utils/Doc.d.ts +7 -70
  11. package/dist/src/utils/Doc.d.ts.map +1 -1
  12. package/dist/src/utils/ID.d.ts +2 -2
  13. package/dist/src/utils/ID.d.ts.map +1 -1
  14. package/dist/src/utils/IdMap.d.ts +22 -19
  15. package/dist/src/utils/IdMap.d.ts.map +1 -1
  16. package/dist/src/utils/IdSet.d.ts +6 -6
  17. package/dist/src/utils/IdSet.d.ts.map +1 -1
  18. package/dist/src/utils/RelativePosition.d.ts +8 -8
  19. package/dist/src/utils/RelativePosition.d.ts.map +1 -1
  20. package/dist/src/utils/Snapshot.d.ts +3 -3
  21. package/dist/src/utils/Snapshot.d.ts.map +1 -1
  22. package/dist/src/utils/Transaction.d.ts +9 -5
  23. package/dist/src/utils/Transaction.d.ts.map +1 -1
  24. package/dist/src/utils/UndoManager.d.ts +14 -12
  25. package/dist/src/utils/UndoManager.d.ts.map +1 -1
  26. package/dist/src/utils/UpdateDecoder.d.ts +8 -4
  27. package/dist/src/utils/UpdateDecoder.d.ts.map +1 -1
  28. package/dist/src/utils/UpdateEncoder.d.ts +2 -0
  29. package/dist/src/utils/UpdateEncoder.d.ts.map +1 -1
  30. package/dist/src/utils/YEvent.d.ts +21 -42
  31. package/dist/src/utils/YEvent.d.ts.map +1 -1
  32. package/dist/src/utils/encoding.d.ts +3 -3
  33. package/dist/src/utils/encoding.d.ts.map +1 -1
  34. package/dist/src/utils/isParentOf.d.ts +1 -1
  35. package/dist/src/utils/isParentOf.d.ts.map +1 -1
  36. package/dist/src/utils/logging.d.ts +2 -2
  37. package/dist/src/utils/logging.d.ts.map +1 -1
  38. package/dist/src/utils/meta.d.ts +71 -0
  39. package/dist/src/utils/meta.d.ts.map +1 -0
  40. package/dist/src/utils/ts.d.ts +4 -0
  41. package/dist/src/utils/ts.d.ts.map +1 -0
  42. package/dist/src/utils/updates.d.ts +11 -11
  43. package/dist/src/utils/updates.d.ts.map +1 -1
  44. package/dist/src/ytype.d.ts +498 -0
  45. package/dist/src/ytype.d.ts.map +1 -0
  46. package/dist/tests/IdMap.tests.d.ts.map +1 -1
  47. package/dist/tests/attribution.tests.d.ts +1 -0
  48. package/dist/tests/attribution.tests.d.ts.map +1 -1
  49. package/dist/tests/compatibility.tests.d.ts.map +1 -1
  50. package/dist/tests/doc.tests.d.ts.map +1 -1
  51. package/dist/tests/relativePositions.tests.d.ts +9 -9
  52. package/dist/tests/relativePositions.tests.d.ts.map +1 -1
  53. package/dist/tests/snapshot.tests.d.ts.map +1 -1
  54. package/dist/tests/testHelper.d.ts +28 -27
  55. package/dist/tests/testHelper.d.ts.map +1 -1
  56. package/dist/tests/undo-redo.tests.d.ts.map +1 -1
  57. package/dist/tests/updates.tests.d.ts +2 -1
  58. package/dist/tests/updates.tests.d.ts.map +1 -1
  59. package/dist/tests/y-array.tests.d.ts +0 -2
  60. package/dist/tests/y-array.tests.d.ts.map +1 -1
  61. package/dist/tests/y-map.tests.d.ts +0 -3
  62. package/dist/tests/y-map.tests.d.ts.map +1 -1
  63. package/dist/tests/y-text.tests.d.ts +1 -1
  64. package/dist/tests/y-text.tests.d.ts.map +1 -1
  65. package/dist/tests/y-xml.tests.d.ts +0 -1
  66. package/dist/tests/y-xml.tests.d.ts.map +1 -1
  67. package/package.json +16 -16
  68. package/src/index.js +156 -0
  69. package/src/internals.js +35 -0
  70. package/src/structs/AbstractStruct.js +59 -0
  71. package/src/structs/ContentAny.js +115 -0
  72. package/src/structs/ContentBinary.js +93 -0
  73. package/src/structs/ContentDeleted.js +101 -0
  74. package/src/structs/ContentDoc.js +141 -0
  75. package/src/structs/ContentEmbed.js +98 -0
  76. package/src/structs/ContentFormat.js +105 -0
  77. package/src/structs/ContentJSON.js +119 -0
  78. package/src/structs/ContentString.js +113 -0
  79. package/src/structs/ContentType.js +152 -0
  80. package/src/structs/GC.js +80 -0
  81. package/src/structs/Item.js +841 -0
  82. package/src/structs/Skip.js +75 -0
  83. package/src/utils/AttributionManager.js +653 -0
  84. package/src/utils/Doc.js +266 -0
  85. package/src/utils/EventHandler.js +87 -0
  86. package/src/utils/ID.js +89 -0
  87. package/src/utils/IdMap.js +673 -0
  88. package/src/utils/IdSet.js +825 -0
  89. package/src/utils/RelativePosition.js +352 -0
  90. package/src/utils/Snapshot.js +220 -0
  91. package/src/utils/StructSet.js +137 -0
  92. package/src/utils/StructStore.js +289 -0
  93. package/src/utils/Transaction.js +671 -0
  94. package/src/utils/UndoManager.js +406 -0
  95. package/src/utils/UpdateDecoder.js +285 -0
  96. package/src/utils/UpdateEncoder.js +327 -0
  97. package/src/utils/YEvent.js +189 -0
  98. package/src/utils/delta-helpers.js +54 -0
  99. package/src/utils/encoding.js +623 -0
  100. package/src/utils/isParentOf.js +21 -0
  101. package/src/utils/logging.js +21 -0
  102. package/src/utils/meta.js +190 -0
  103. package/src/utils/ts.js +3 -0
  104. package/src/utils/updates.js +802 -0
  105. package/src/ytype.js +1962 -0
  106. package/dist/Skip-CE05BUF8.js +0 -11875
  107. package/dist/Skip-CE05BUF8.js.map +0 -1
  108. package/dist/index-C21sDQ5u.js +0 -163
  109. package/dist/index-C21sDQ5u.js.map +0 -1
  110. package/dist/internals.js +0 -25
  111. package/dist/internals.js.map +0 -1
  112. package/dist/src/types/AbstractType.d.ts +0 -239
  113. package/dist/src/types/AbstractType.d.ts.map +0 -1
  114. package/dist/src/types/YArray.d.ts +0 -128
  115. package/dist/src/types/YArray.d.ts.map +0 -1
  116. package/dist/src/types/YMap.d.ts +0 -112
  117. package/dist/src/types/YMap.d.ts.map +0 -1
  118. package/dist/src/types/YText.d.ts +0 -216
  119. package/dist/src/types/YText.d.ts.map +0 -1
  120. package/dist/src/types/YXmlElement.d.ts +0 -106
  121. package/dist/src/types/YXmlElement.d.ts.map +0 -1
  122. package/dist/src/types/YXmlFragment.d.ts +0 -143
  123. package/dist/src/types/YXmlFragment.d.ts.map +0 -1
  124. package/dist/src/types/YXmlHook.d.ts +0 -32
  125. package/dist/src/types/YXmlHook.d.ts.map +0 -1
  126. package/dist/src/types/YXmlText.d.ts +0 -34
  127. package/dist/src/types/YXmlText.d.ts.map +0 -1
  128. package/dist/src/utils/AbstractConnector.d.ts +0 -20
  129. package/dist/src/utils/AbstractConnector.d.ts.map +0 -1
  130. package/dist/src/utils/types.d.ts +0 -7
  131. package/dist/src/utils/types.d.ts.map +0 -1
  132. package/dist/testHelper.js +0 -617
  133. package/dist/testHelper.js.map +0 -1
  134. package/dist/yjs.js +0 -26
  135. package/dist/yjs.js.map +0 -1
@@ -0,0 +1,671 @@
1
+ import {
2
+ getState,
3
+ writeStructsFromTransaction,
4
+ writeIdSet,
5
+ getStateVector,
6
+ findIndexSS,
7
+ callEventHandlerListeners,
8
+ createIdSet,
9
+ Item,
10
+ generateNewClientId,
11
+ createID,
12
+ iterateStructsByIdSet,
13
+ ContentFormat,
14
+ IdSet, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractStruct, YEvent, Doc // eslint-disable-line
15
+ } from '../internals.js'
16
+
17
+ import { YType } from '../ytype.js' // eslint-disable-line
18
+ import * as error from 'lib0/error'
19
+ import * as map from 'lib0/map'
20
+ import * as math from 'lib0/math'
21
+ import * as set from 'lib0/set'
22
+ import * as logging from 'lib0/logging'
23
+ import { callAll } from 'lib0/function'
24
+
25
+ /**
26
+ * A transaction is created for every change on the Yjs model. It is possible
27
+ * to bundle changes on the Yjs model in a single transaction to
28
+ * minimize the number on messages sent and the number of observer calls.
29
+ * If possible the user of this library should bundle as many changes as
30
+ * possible. Here is an example to illustrate the advantages of bundling:
31
+ *
32
+ * @example
33
+ * const ydoc = new Y.Doc()
34
+ * const map = ydoc.getMap('map')
35
+ * // Log content when change is triggered
36
+ * map.observe(() => {
37
+ * console.log('change triggered')
38
+ * })
39
+ * // Each change on the map type triggers a log message:
40
+ * map.set('a', 0) // => "change triggered"
41
+ * map.set('b', 0) // => "change triggered"
42
+ * // When put in a transaction, it will trigger the log after the transaction:
43
+ * ydoc.transact(() => {
44
+ * map.set('a', 1)
45
+ * map.set('b', 1)
46
+ * }) // => "change triggered"
47
+ *
48
+ * @public
49
+ */
50
+ export class Transaction {
51
+ /**
52
+ * @param {Doc} doc
53
+ * @param {any} origin
54
+ * @param {boolean} local
55
+ */
56
+ constructor (doc, origin, local) {
57
+ /**
58
+ * The Yjs instance.
59
+ * @type {Doc}
60
+ */
61
+ this.doc = doc
62
+ /**
63
+ * Describes the set of deleted items by ids
64
+ */
65
+ this.deleteSet = createIdSet()
66
+ /**
67
+ * Describes the set of items that are cleaned up / deleted by ids. It is a subset of
68
+ * this.deleteSet
69
+ */
70
+ this.cleanUps = createIdSet()
71
+ /**
72
+ * Describes the set of inserted items by ids
73
+ */
74
+ this.insertSet = createIdSet()
75
+ /**
76
+ * Holds the state before the transaction started.
77
+ * @type {Map<Number,Number>?}
78
+ */
79
+ this._beforeState = null
80
+ /**
81
+ * Holds the state after the transaction.
82
+ * @type {Map<Number,Number>?}
83
+ */
84
+ this._afterState = null
85
+ /**
86
+ * All types that were directly modified (property added or child
87
+ * inserted/deleted). New types are not included in this Set.
88
+ * Maps from type to parentSubs (`item.parentSub = null` for YArray)
89
+ * @type {Map<YType,Set<String|null>>}
90
+ */
91
+ this.changed = new Map()
92
+ /**
93
+ * Stores the events for the types that observe also child elements.
94
+ * It is mainly used by `observeDeep`.
95
+ * @type {Map<YType,Array<YEvent<any>>>}
96
+ */
97
+ this.changedParentTypes = new Map()
98
+ /**
99
+ * @type {Array<AbstractStruct>}
100
+ */
101
+ this._mergeStructs = []
102
+ /**
103
+ * @type {any}
104
+ */
105
+ this.origin = origin
106
+ /**
107
+ * Stores meta information on the transaction
108
+ * @type {Map<any,any>}
109
+ */
110
+ this.meta = new Map()
111
+ /**
112
+ * Whether this change originates from this doc.
113
+ * @type {boolean}
114
+ */
115
+ this.local = local
116
+ /**
117
+ * @type {Set<Doc>}
118
+ */
119
+ this.subdocsAdded = new Set()
120
+ /**
121
+ * @type {Set<Doc>}
122
+ */
123
+ this.subdocsRemoved = new Set()
124
+ /**
125
+ * @type {Set<Doc>}
126
+ */
127
+ this.subdocsLoaded = new Set()
128
+ /**
129
+ * @type {boolean}
130
+ */
131
+ this._needFormattingCleanup = false
132
+ this._done = false
133
+ }
134
+
135
+ /**
136
+ * Holds the state before the transaction started.
137
+ *
138
+ * @deprecated
139
+ * @type {Map<Number,Number>}
140
+ */
141
+ get beforeState () {
142
+ if (this._beforeState == null) {
143
+ const sv = getStateVector(this.doc.store)
144
+ this.insertSet.clients.forEach((ranges, client) => {
145
+ sv.set(client, ranges.getIds()[0].clock)
146
+ })
147
+ this._beforeState = sv
148
+ }
149
+ return this._beforeState
150
+ }
151
+
152
+ /**
153
+ * Holds the state after the transaction.
154
+ *
155
+ * @deprecated
156
+ * @type {Map<Number,Number>}
157
+ */
158
+ get afterState () {
159
+ if (!this._done) error.unexpectedCase()
160
+ if (this._afterState == null) {
161
+ const sv = getStateVector(this.doc.store)
162
+ this.insertSet.clients.forEach((_ranges, client) => {
163
+ const ranges = _ranges.getIds()
164
+ const d = ranges[ranges.length - 1]
165
+ sv.set(client, d.clock + d.len)
166
+ })
167
+ this._afterState = sv
168
+ }
169
+ return this._afterState
170
+ }
171
+ }
172
+
173
+ /**
174
+ * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
175
+ * @param {Transaction} transaction
176
+ * @return {boolean} Whether data was written.
177
+ */
178
+ export const writeUpdateMessageFromTransaction = (encoder, transaction) => {
179
+ if (transaction.deleteSet.clients.size === 0 && transaction.insertSet.clients.size === 0) {
180
+ return false
181
+ }
182
+ writeStructsFromTransaction(encoder, transaction)
183
+ writeIdSet(encoder, transaction.deleteSet)
184
+ return true
185
+ }
186
+
187
+ /**
188
+ * @param {Transaction} transaction
189
+ *
190
+ * @private
191
+ * @function
192
+ */
193
+ export const nextID = transaction => {
194
+ const y = transaction.doc
195
+ return createID(y.clientID, getState(y.store, y.clientID))
196
+ }
197
+
198
+ /**
199
+ * If `type.parent` was added in current transaction, `type` technically
200
+ * did not change, it was just added and we should not fire events for `type`.
201
+ *
202
+ * @param {Transaction} transaction
203
+ * @param {YType} type
204
+ * @param {string|null} parentSub
205
+ */
206
+ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
207
+ const item = type._item
208
+ if (item === null || (!item.deleted && !transaction.insertSet.hasId(item.id))) {
209
+ map.setIfUndefined(transaction.changed, type, set.create).add(parentSub)
210
+ }
211
+ }
212
+
213
+ /**
214
+ * @param {Array<AbstractStruct>} structs
215
+ * @param {number} pos
216
+ * @return {number} # of merged structs
217
+ */
218
+ const tryToMergeWithLefts = (structs, pos) => {
219
+ let right = structs[pos]
220
+ let left = structs[pos - 1]
221
+ let i = pos
222
+ for (; i > 0; right = left, left = structs[--i - 1]) {
223
+ if (left.deleted === right.deleted && left.constructor === right.constructor) {
224
+ if (left.mergeWith(right)) {
225
+ if (right instanceof Item && right.parentSub !== null && /** @type {YType} */ (right.parent)._map.get(right.parentSub) === right) {
226
+ /** @type {YType} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
227
+ }
228
+ continue
229
+ }
230
+ }
231
+ break
232
+ }
233
+ const merged = pos - i
234
+ if (merged) {
235
+ // remove all merged structs from the array
236
+ structs.splice(pos + 1 - merged, merged)
237
+ }
238
+ return merged
239
+ }
240
+
241
+ /**
242
+ * @param {Transaction} tr
243
+ * @param {IdSet} ds
244
+ * @param {function(Item):boolean} gcFilter
245
+ */
246
+ const tryGcDeleteSet = (tr, ds, gcFilter) => {
247
+ for (const [client, _deleteItems] of ds.clients.entries()) {
248
+ const deleteItems = _deleteItems.getIds()
249
+ const structs = /** @type {Array<GC|Item>} */ (tr.doc.store.clients.get(client))
250
+ for (let di = deleteItems.length - 1; di >= 0; di--) {
251
+ const deleteItem = deleteItems[di]
252
+ const endDeleteItemClock = deleteItem.clock + deleteItem.len
253
+ for (
254
+ let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
255
+ si < structs.length && struct.id.clock < endDeleteItemClock;
256
+ struct = structs[++si]
257
+ ) {
258
+ const struct = structs[si]
259
+ if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
260
+ break
261
+ }
262
+ if (struct instanceof Item && struct.deleted && !struct.keep && gcFilter(struct)) {
263
+ struct.gc(tr, false)
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ /**
271
+ * @param {IdSet} ds
272
+ * @param {StructStore} store
273
+ */
274
+ const tryMerge = (ds, store) => {
275
+ // try to merge deleted / gc'd items
276
+ // merge from right to left for better efficiency and so we don't miss any merge targets
277
+ ds.clients.forEach((_deleteItems, client) => {
278
+ const deleteItems = _deleteItems.getIds()
279
+ const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
280
+ for (let di = deleteItems.length - 1; di >= 0; di--) {
281
+ const deleteItem = deleteItems[di]
282
+ // start with merging the item next to the last deleted item
283
+ const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
284
+ for (
285
+ let si = mostRightIndexToCheck, struct = structs[si];
286
+ si > 0 && struct.id.clock >= deleteItem.clock;
287
+ struct = structs[si]
288
+ ) {
289
+ si -= 1 + tryToMergeWithLefts(structs, si)
290
+ }
291
+ }
292
+ })
293
+ }
294
+
295
+ /**
296
+ * @param {Transaction} tr
297
+ * @param {IdSet} idset
298
+ * @param {function(Item):boolean} gcFilter
299
+ */
300
+ export const tryGc = (tr, idset, gcFilter) => {
301
+ tryGcDeleteSet(tr, idset, gcFilter)
302
+ tryMerge(idset, tr.doc.store)
303
+ }
304
+
305
+ /**
306
+ * @param {Transaction} transaction
307
+ * @param {Item | null} item
308
+ */
309
+ const cleanupContextlessFormattingGap = (transaction, item) => {
310
+ if (!transaction.doc.cleanupFormatting) return 0
311
+ // iterate until item.right is null or content
312
+ while (item && item.right && (item.right.deleted || !item.right.countable)) {
313
+ item = item.right
314
+ }
315
+ const attrs = new Set()
316
+ // iterate back until a content item is found
317
+ while (item && (item.deleted || !item.countable)) {
318
+ if (!item.deleted && item.content.constructor === ContentFormat) {
319
+ const key = /** @type {ContentFormat} */ (item.content).key
320
+ if (attrs.has(key)) {
321
+ item.delete(transaction)
322
+ transaction.cleanUps.add(item.id.client, item.id.clock, item.length)
323
+ } else {
324
+ attrs.add(key)
325
+ }
326
+ }
327
+ item = item.left
328
+ }
329
+ }
330
+
331
+ /**
332
+ * @param {Map<string,any>} currentAttributes
333
+ * @param {ContentFormat} format
334
+ *
335
+ * @private
336
+ * @function
337
+ */
338
+ const updateCurrentAttributes = (currentAttributes, { key, value }) => {
339
+ if (value === null) {
340
+ currentAttributes.delete(key)
341
+ } else {
342
+ currentAttributes.set(key, value)
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Call this function after string content has been deleted in order to
348
+ * clean up formatting Items.
349
+ *
350
+ * @param {Transaction} transaction
351
+ * @param {Item} start
352
+ * @param {Item|null} curr exclusive end, automatically iterates to the next Content Item
353
+ * @param {Map<string,any>} startAttributes
354
+ * @param {Map<string,any>} currAttributes
355
+ * @return {number} The amount of formatting Items deleted.
356
+ *
357
+ * @function
358
+ */
359
+ export const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
360
+ if (!transaction.doc.cleanupFormatting) return 0
361
+ /**
362
+ * @type {Item|null}
363
+ */
364
+ let end = start
365
+ /**
366
+ * @type {Map<string,ContentFormat>}
367
+ */
368
+ const endFormats = map.create()
369
+ while (end && (!end.countable || end.deleted)) {
370
+ if (!end.deleted && end.content.constructor === ContentFormat) {
371
+ const cf = /** @type {ContentFormat} */ (end.content)
372
+ endFormats.set(cf.key, cf)
373
+ }
374
+ end = end.right
375
+ }
376
+ let cleanups = 0
377
+ let reachedCurr = false
378
+ while (start !== end) {
379
+ if (curr === start) {
380
+ reachedCurr = true
381
+ }
382
+ if (!start.deleted) {
383
+ const content = start.content
384
+ switch (content.constructor) {
385
+ case ContentFormat: {
386
+ const { key, value } = /** @type {ContentFormat} */ (content)
387
+ const startAttrValue = startAttributes.get(key) ?? null
388
+ if (endFormats.get(key) !== content || startAttrValue === value) {
389
+ // Either this format is overwritten or it is not necessary because the attribute already existed.
390
+ start.delete(transaction)
391
+ transaction.cleanUps.add(start.id.client, start.id.clock, start.length)
392
+ cleanups++
393
+ if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
394
+ if (startAttrValue === null) {
395
+ currAttributes.delete(key)
396
+ } else {
397
+ currAttributes.set(key, startAttrValue)
398
+ }
399
+ }
400
+ }
401
+ if (!reachedCurr && !start.deleted) {
402
+ updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content))
403
+ }
404
+ break
405
+ }
406
+ }
407
+ }
408
+ start = /** @type {Item} */ (start.right)
409
+ }
410
+ return cleanups
411
+ }
412
+
413
+ /**
414
+ * This function is experimental and subject to change / be removed.
415
+ *
416
+ * Ideally, we don't need this function at all. Formatting attributes should be cleaned up
417
+ * automatically after each change. This function iterates twice over the complete YText type
418
+ * and removes unnecessary formatting attributes. This is also helpful for testing.
419
+ *
420
+ * This function won't be exported anymore as soon as there is confidence that the YText type works as intended.
421
+ *
422
+ * @param {YType} type
423
+ * @return {number} How many formatting attributes have been cleaned up.
424
+ */
425
+ export const cleanupYTextFormatting = type => {
426
+ if (!type.doc?.cleanupFormatting) return 0
427
+ let res = 0
428
+ transact(/** @type {Doc} */ (type.doc), transaction => {
429
+ let start = /** @type {Item} */ (type._start)
430
+ let end = type._start
431
+ let startAttributes = map.create()
432
+ const currentAttributes = map.copy(startAttributes)
433
+ while (end) {
434
+ if (end.deleted === false) {
435
+ switch (end.content.constructor) {
436
+ case ContentFormat:
437
+ updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content))
438
+ break
439
+ default:
440
+ res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
441
+ startAttributes = map.copy(currentAttributes)
442
+ start = end
443
+ break
444
+ }
445
+ }
446
+ end = end.right
447
+ }
448
+ })
449
+ return res
450
+ }
451
+
452
+ /**
453
+ * This will be called by the transaction once the event handlers are called to potentially cleanup
454
+ * formatting attributes.
455
+ *
456
+ * @param {Transaction} transaction
457
+ */
458
+ export const cleanupYTextAfterTransaction = transaction => {
459
+ /**
460
+ * @type {Set<YType>}
461
+ */
462
+ const needFullCleanup = new Set()
463
+ // check if another formatting item was inserted
464
+ const doc = transaction.doc
465
+ iterateStructsByIdSet(transaction, transaction.insertSet, (item) => {
466
+ if (
467
+ !item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
468
+ ) {
469
+ needFullCleanup.add(/** @type {any} */ (item).parent)
470
+ }
471
+ })
472
+ // cleanup in a new transaction
473
+ transact(doc, (t) => {
474
+ iterateStructsByIdSet(transaction, transaction.deleteSet, item => {
475
+ if (item instanceof GC || !(/** @type {YType} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YType} */ (item.parent))) {
476
+ return
477
+ }
478
+ const parent = /** @type {YType} */ (item.parent)
479
+ if (item.content.constructor === ContentFormat) {
480
+ needFullCleanup.add(parent)
481
+ } else {
482
+ // If no formatting attribute was inserted or deleted, we can make due with contextless
483
+ // formatting cleanups.
484
+ // Contextless: it is not necessary to compute currentAttributes for the affected position.
485
+ cleanupContextlessFormattingGap(t, item)
486
+ }
487
+ })
488
+ // If a formatting item was inserted, we simply clean the whole type.
489
+ // We need to compute currentAttributes for the current position anyway.
490
+ for (const yText of needFullCleanup) {
491
+ cleanupYTextFormatting(yText)
492
+ }
493
+ })
494
+ }
495
+
496
+ /**
497
+ * @param {Array<Transaction>} transactionCleanups
498
+ * @param {number} i
499
+ */
500
+ const cleanupTransactions = (transactionCleanups, i) => {
501
+ if (i < transactionCleanups.length) {
502
+ const transaction = transactionCleanups[i]
503
+ transaction._done = true
504
+ const doc = transaction.doc
505
+ const store = doc.store
506
+ const ds = transaction.deleteSet
507
+ const mergeStructs = transaction._mergeStructs
508
+ // insertIntoIdSet(store.ds, ds)
509
+ try {
510
+ doc.emit('beforeObserverCalls', [transaction, doc])
511
+ /**
512
+ * An array of event callbacks.
513
+ *
514
+ * Each callback is called even if the other ones throw errors.
515
+ *
516
+ * @type {Array<function():void>}
517
+ */
518
+ const fs = []
519
+ // observe events on changed types
520
+ transaction.changed.forEach((subs, itemtype) =>
521
+ fs.push(() => {
522
+ if (itemtype._item === null || !itemtype._item.deleted) {
523
+ itemtype._callObserver(transaction, subs)
524
+ }
525
+ })
526
+ )
527
+ fs.push(() => {
528
+ // deep observe events
529
+ transaction.changedParentTypes.forEach((events, type) => {
530
+ // We need to think about the possibility that the user transforms the
531
+ // Y.Doc in the event.
532
+ if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) {
533
+ /**
534
+ * @type {YEvent<any>}
535
+ */
536
+ const deepEventHandler = events.find(event => event.target === type) || new YEvent(type, transaction, new Set(null))
537
+ callEventHandlerListeners(type._dEH, deepEventHandler, transaction)
538
+ }
539
+ })
540
+ })
541
+ fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
542
+ callAll(fs, [])
543
+ if (transaction._needFormattingCleanup && doc.cleanupFormatting) {
544
+ cleanupYTextAfterTransaction(transaction)
545
+ }
546
+ } finally {
547
+ // Replace deleted items with ItemDeleted / GC.
548
+ // This is where content is actually remove from the Yjs Doc.
549
+ if (doc.gc) {
550
+ tryGcDeleteSet(transaction, ds, doc.gcFilter)
551
+ }
552
+ tryMerge(ds, store)
553
+
554
+ // on all affected store.clients props, try to merge
555
+ transaction.insertSet.clients.forEach((ids, client) => {
556
+ const firstClock = ids.getIds()[0].clock
557
+ const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
558
+ // we iterate from right to left so we can safely remove entries
559
+ const firstChangePos = math.max(findIndexSS(structs, firstClock), 1)
560
+ for (let i = structs.length - 1; i >= firstChangePos;) {
561
+ i -= 1 + tryToMergeWithLefts(structs, i)
562
+ }
563
+ })
564
+ // try to merge mergeStructs
565
+ // @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
566
+ // but at the moment DS does not handle duplicates
567
+ for (let i = mergeStructs.length - 1; i >= 0; i--) {
568
+ const { client, clock } = mergeStructs[i].id
569
+ const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
570
+ const replacedStructPos = findIndexSS(structs, clock)
571
+ if (replacedStructPos + 1 < structs.length) {
572
+ if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) {
573
+ continue // no need to perform next check, both are already merged
574
+ }
575
+ }
576
+ if (replacedStructPos > 0) {
577
+ tryToMergeWithLefts(structs, replacedStructPos)
578
+ }
579
+ }
580
+ if (!transaction.local && transaction.insertSet.clients.has(doc.clientID)) {
581
+ logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.')
582
+ doc.clientID = generateNewClientId()
583
+ }
584
+ // @todo Merge all the transactions into one and provide send the data as a single update message
585
+ doc.emit('afterTransactionCleanup', [transaction, doc])
586
+ if (doc._observers.has('update')) {
587
+ const encoder = new UpdateEncoderV1()
588
+ const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
589
+ if (hasContent) {
590
+ doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc, transaction])
591
+ }
592
+ }
593
+ if (doc._observers.has('updateV2')) {
594
+ const encoder = new UpdateEncoderV2()
595
+ const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
596
+ if (hasContent) {
597
+ doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction])
598
+ }
599
+ }
600
+ const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction
601
+ if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) {
602
+ subdocsAdded.forEach(subdoc => {
603
+ subdoc.clientID = doc.clientID
604
+ if (subdoc.collectionid == null) {
605
+ subdoc.collectionid = doc.collectionid
606
+ }
607
+ doc.subdocs.add(subdoc)
608
+ })
609
+ subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc))
610
+ doc.emit('subdocs', [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc, transaction])
611
+ subdocsRemoved.forEach(subdoc => subdoc.destroy())
612
+ }
613
+
614
+ if (transactionCleanups.length <= i + 1) {
615
+ doc._transactionCleanups = []
616
+ doc.emit('afterAllTransactions', [doc, transactionCleanups])
617
+ } else {
618
+ cleanupTransactions(transactionCleanups, i + 1)
619
+ }
620
+ }
621
+ }
622
+ }
623
+
624
+ /**
625
+ * Implements the functionality of `y.transact(()=>{..})`
626
+ *
627
+ * @template T
628
+ * @param {Doc} doc
629
+ * @param {function(Transaction):T} f
630
+ * @param {any} [origin=true]
631
+ * @return {T}
632
+ *
633
+ * @function
634
+ */
635
+ export const transact = (doc, f, origin = null, local = true) => {
636
+ const transactionCleanups = doc._transactionCleanups
637
+ let initialCall = false
638
+ /**
639
+ * @type {any}
640
+ */
641
+ let result = null
642
+ if (doc._transaction === null) {
643
+ initialCall = true
644
+ doc._transaction = new Transaction(doc, origin, local)
645
+ transactionCleanups.push(doc._transaction)
646
+ if (transactionCleanups.length === 1) {
647
+ doc.emit('beforeAllTransactions', [doc])
648
+ }
649
+ doc.emit('beforeTransaction', [doc._transaction, doc])
650
+ }
651
+ try {
652
+ result = f(doc._transaction)
653
+ } finally {
654
+ if (initialCall) {
655
+ const finishCleanup = doc._transaction === transactionCleanups[0]
656
+ doc._transaction = null
657
+ if (finishCleanup) {
658
+ // The first transaction ended, now process observer calls.
659
+ // Observer call may create new transactions for which we need to call the observers and do cleanup.
660
+ // We don't want to nest these calls, so we execute these calls one after
661
+ // another.
662
+ // Also we need to ensure that all cleanups are called, even if the
663
+ // observes throw errors.
664
+ // This file is full of hacky try {} finally {} blocks to ensure that an
665
+ // event can throw errors and also that the cleanup is called.
666
+ cleanupTransactions(transactionCleanups, 0)
667
+ }
668
+ }
669
+ }
670
+ return result
671
+ }