@y/y 14.0.0-18 → 14.0.0-20

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 (129) 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 +20 -6
  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 +19 -16
  15. package/dist/src/utils/IdMap.d.ts.map +1 -1
  16. package/dist/src/utils/IdSet.d.ts +2 -2
  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/Transaction.d.ts +9 -5
  21. package/dist/src/utils/Transaction.d.ts.map +1 -1
  22. package/dist/src/utils/UndoManager.d.ts +14 -12
  23. package/dist/src/utils/UndoManager.d.ts.map +1 -1
  24. package/dist/src/utils/UpdateEncoder.d.ts +2 -0
  25. package/dist/src/utils/UpdateEncoder.d.ts.map +1 -1
  26. package/dist/src/utils/YEvent.d.ts +21 -42
  27. package/dist/src/utils/YEvent.d.ts.map +1 -1
  28. package/dist/src/utils/isParentOf.d.ts +1 -1
  29. package/dist/src/utils/isParentOf.d.ts.map +1 -1
  30. package/dist/src/utils/logging.d.ts +2 -2
  31. package/dist/src/utils/logging.d.ts.map +1 -1
  32. package/dist/src/utils/meta.d.ts +43 -0
  33. package/dist/src/utils/meta.d.ts.map +1 -0
  34. package/dist/src/utils/ts.d.ts +4 -0
  35. package/dist/src/utils/ts.d.ts.map +1 -0
  36. package/dist/src/utils/updates.d.ts +3 -9
  37. package/dist/src/utils/updates.d.ts.map +1 -1
  38. package/dist/src/ytype.d.ts +498 -0
  39. package/dist/src/ytype.d.ts.map +1 -0
  40. package/dist/tests/IdMap.tests.d.ts.map +1 -1
  41. package/dist/tests/attribution.tests.d.ts +1 -0
  42. package/dist/tests/attribution.tests.d.ts.map +1 -1
  43. package/dist/tests/compatibility.tests.d.ts.map +1 -1
  44. package/dist/tests/doc.tests.d.ts.map +1 -1
  45. package/dist/tests/relativePositions.tests.d.ts +9 -9
  46. package/dist/tests/relativePositions.tests.d.ts.map +1 -1
  47. package/dist/tests/snapshot.tests.d.ts.map +1 -1
  48. package/dist/tests/testHelper.d.ts +28 -27
  49. package/dist/tests/testHelper.d.ts.map +1 -1
  50. package/dist/tests/undo-redo.tests.d.ts.map +1 -1
  51. package/dist/tests/updates.tests.d.ts +1 -1
  52. package/dist/tests/updates.tests.d.ts.map +1 -1
  53. package/dist/tests/y-array.tests.d.ts +0 -2
  54. package/dist/tests/y-array.tests.d.ts.map +1 -1
  55. package/dist/tests/y-map.tests.d.ts +0 -3
  56. package/dist/tests/y-map.tests.d.ts.map +1 -1
  57. package/dist/tests/y-text.tests.d.ts +1 -1
  58. package/dist/tests/y-text.tests.d.ts.map +1 -1
  59. package/dist/tests/y-xml.tests.d.ts +0 -1
  60. package/dist/tests/y-xml.tests.d.ts.map +1 -1
  61. package/package.json +16 -16
  62. package/src/index.js +152 -0
  63. package/src/internals.js +35 -0
  64. package/src/structs/AbstractStruct.js +59 -0
  65. package/src/structs/ContentAny.js +115 -0
  66. package/src/structs/ContentBinary.js +93 -0
  67. package/src/structs/ContentDeleted.js +101 -0
  68. package/src/structs/ContentDoc.js +141 -0
  69. package/src/structs/ContentEmbed.js +98 -0
  70. package/src/structs/ContentFormat.js +105 -0
  71. package/src/structs/ContentJSON.js +119 -0
  72. package/src/structs/ContentString.js +113 -0
  73. package/src/structs/ContentType.js +152 -0
  74. package/src/structs/GC.js +80 -0
  75. package/src/structs/Item.js +841 -0
  76. package/src/structs/Skip.js +75 -0
  77. package/src/utils/AttributionManager.js +653 -0
  78. package/src/utils/Doc.js +266 -0
  79. package/src/utils/EventHandler.js +87 -0
  80. package/src/utils/ID.js +89 -0
  81. package/src/utils/IdMap.js +673 -0
  82. package/src/utils/IdSet.js +825 -0
  83. package/src/utils/RelativePosition.js +352 -0
  84. package/src/utils/Snapshot.js +220 -0
  85. package/src/utils/StructSet.js +137 -0
  86. package/src/utils/StructStore.js +289 -0
  87. package/src/utils/Transaction.js +671 -0
  88. package/src/utils/UndoManager.js +406 -0
  89. package/src/utils/UpdateDecoder.js +281 -0
  90. package/src/utils/UpdateEncoder.js +327 -0
  91. package/src/utils/YEvent.js +189 -0
  92. package/src/utils/delta-helpers.js +54 -0
  93. package/src/utils/encoding.js +623 -0
  94. package/src/utils/isParentOf.js +21 -0
  95. package/src/utils/logging.js +21 -0
  96. package/src/utils/meta.js +97 -0
  97. package/src/utils/ts.js +3 -0
  98. package/src/utils/updates.js +711 -0
  99. package/src/ytype.js +1962 -0
  100. package/dist/Skip-wRT7BKFP.js +0 -11877
  101. package/dist/Skip-wRT7BKFP.js.map +0 -1
  102. package/dist/index-BV-j5wdP.js +0 -163
  103. package/dist/index-BV-j5wdP.js.map +0 -1
  104. package/dist/internals.js +0 -25
  105. package/dist/internals.js.map +0 -1
  106. package/dist/src/types/AbstractType.d.ts +0 -239
  107. package/dist/src/types/AbstractType.d.ts.map +0 -1
  108. package/dist/src/types/YArray.d.ts +0 -128
  109. package/dist/src/types/YArray.d.ts.map +0 -1
  110. package/dist/src/types/YMap.d.ts +0 -112
  111. package/dist/src/types/YMap.d.ts.map +0 -1
  112. package/dist/src/types/YText.d.ts +0 -216
  113. package/dist/src/types/YText.d.ts.map +0 -1
  114. package/dist/src/types/YXmlElement.d.ts +0 -106
  115. package/dist/src/types/YXmlElement.d.ts.map +0 -1
  116. package/dist/src/types/YXmlFragment.d.ts +0 -143
  117. package/dist/src/types/YXmlFragment.d.ts.map +0 -1
  118. package/dist/src/types/YXmlHook.d.ts +0 -32
  119. package/dist/src/types/YXmlHook.d.ts.map +0 -1
  120. package/dist/src/types/YXmlText.d.ts +0 -34
  121. package/dist/src/types/YXmlText.d.ts.map +0 -1
  122. package/dist/src/utils/AbstractConnector.d.ts +0 -20
  123. package/dist/src/utils/AbstractConnector.d.ts.map +0 -1
  124. package/dist/src/utils/types.d.ts +0 -7
  125. package/dist/src/utils/types.d.ts.map +0 -1
  126. package/dist/testHelper.js +0 -617
  127. package/dist/testHelper.js.map +0 -1
  128. package/dist/yjs.js +0 -26
  129. package/dist/yjs.js.map +0 -1
@@ -0,0 +1,653 @@
1
+ import {
2
+ getItem,
3
+ diffIdSet,
4
+ createInsertSetFromStructStore,
5
+ createDeleteSetFromStructStore,
6
+ createIdMapFromIdSet,
7
+ ContentDeleted,
8
+ insertIntoIdMap,
9
+ insertIntoIdSet,
10
+ diffIdMap,
11
+ createIdMap,
12
+ mergeIdMaps,
13
+ createID,
14
+ mergeIdSets,
15
+ applyUpdate,
16
+ writeIdSet,
17
+ UpdateEncoderV1,
18
+ transact,
19
+ createMaybeAttrRange,
20
+ createIdSet,
21
+ writeStructsFromIdSet,
22
+ UndoManager,
23
+ StackItem,
24
+ getItemCleanStart,
25
+ intersectSets,
26
+ intersectMaps,
27
+ ContentFormat,
28
+ createContentAttribute,
29
+ StructStore, Transaction, ID, IdSet, Item, Snapshot, Doc, AbstractContent, IdMap, // eslint-disable-line
30
+ encodeStateAsUpdate
31
+ } from '../internals.js'
32
+
33
+ import * as error from 'lib0/error'
34
+ import { ObservableV2 } from 'lib0/observable'
35
+ import * as encoding from 'lib0/encoding'
36
+ import * as s from 'lib0/schema'
37
+
38
+ export const attributionJsonSchema = s.$object({
39
+ insert: s.$array(s.$string).optional,
40
+ insertedAt: s.$number.optional,
41
+ delete: s.$array(s.$string).optional,
42
+ deletedAt: s.$number.optional,
43
+ format: s.$record(s.$string, s.$array(s.$string)).optional,
44
+ formatAt: s.$number.optional
45
+ })
46
+
47
+ /**
48
+ * @todo rename this to `insertBy`, `insertAt`, ..
49
+ *
50
+ * @typedef {s.Unwrap<typeof attributionJsonSchema>} Attribution
51
+ */
52
+
53
+ /**
54
+ * @todo SHOULD NOT RETURN AN OBJECT!
55
+ * @param {Array<import('./IdMap.js').ContentAttribute<any>>?} attrs
56
+ * @param {boolean} deleted - whether the attributed item is deleted
57
+ * @return {Attribution?}
58
+ */
59
+ export const createAttributionFromAttributionItems = (attrs, deleted) => {
60
+ if (attrs == null) {
61
+ return null
62
+ }
63
+ /**
64
+ * @type {Attribution}
65
+ */
66
+ const attribution = {}
67
+ if (deleted) {
68
+ attribution.delete = []
69
+ } else {
70
+ attribution.insert = []
71
+ }
72
+ attrs.forEach(attr => {
73
+ switch (attr.name) {
74
+ // eslint-disable-next-line no-fallthrough
75
+ case 'insert':
76
+ case 'delete': {
77
+ // needs to be non-ambiguous: don't add existing attr if it doesn't match the actual status
78
+ attribution[attr.name]?.push(attr.val)
79
+ break
80
+ }
81
+ default: {
82
+ if (attr.name[0] !== '_') {
83
+ /** @type {any} */ (attribution)[attr.name] = attr.val
84
+ }
85
+ }
86
+ }
87
+ })
88
+ return attribution
89
+ }
90
+
91
+ /**
92
+ * @template T
93
+ */
94
+ export class AttributedContent {
95
+ /**
96
+ * @param {AbstractContent} content
97
+ * @param {number} clock
98
+ * @param {boolean} deleted
99
+ * @param {Array<import('./IdMap.js').ContentAttribute<T>> | null} attrs
100
+ * @param {0|1|2} renderBehavior
101
+ */
102
+ constructor (content, clock, deleted, attrs, renderBehavior) {
103
+ this.content = content
104
+ this.clock = clock
105
+ this.deleted = deleted
106
+ this.attrs = attrs
107
+ this.render = renderBehavior === 0 ? false : (renderBehavior === 1 ? (!deleted || attrs != null) : true)
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Abstract class for associating Attributions to content / changes
113
+ *
114
+ * Should fire an event when the attributions changed _after_ the original change happens. This
115
+ * Event will be used to update the attribution on the current content.
116
+ *
117
+ * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
118
+ */
119
+ export class AbstractAttributionManager extends ObservableV2 {
120
+ /**
121
+ * @param {Array<AttributedContent<any>>} _contents - where to write the result
122
+ * @param {number} _client
123
+ * @param {number} _clock
124
+ * @param {boolean} _deleted
125
+ * @param {AbstractContent} _content
126
+ * @param {0|1|2} _shouldRender - 0: if undeleted or attributed, render as a retain operation. 1: render only if undeleted or attributed. 2: render as insert operation (if unattributed and deleted, render as delete).
127
+ */
128
+ readContent (_contents, _client, _clock, _deleted, _content, _shouldRender) {
129
+ error.methodUnimplemented()
130
+ }
131
+
132
+ /**
133
+ * Calculate the length of the attributed content. This is used by iterators that walk through the
134
+ * content.
135
+ *
136
+ * If the content is not countable, it should return 0.
137
+ *
138
+ * @param {Item} _item
139
+ * @return {number}
140
+ */
141
+ contentLength (_item) {
142
+ error.methodUnimplemented()
143
+ }
144
+ }
145
+
146
+ /**
147
+ * @implements AbstractAttributionManager
148
+ *
149
+ * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
150
+ */
151
+ export class TwosetAttributionManager extends ObservableV2 {
152
+ /**
153
+ * @param {IdMap<any>} inserts
154
+ * @param {IdMap<any>} deletes
155
+ */
156
+ constructor (inserts, deletes) {
157
+ super()
158
+ this.inserts = inserts
159
+ this.deletes = deletes
160
+ }
161
+
162
+ /**
163
+ * @param {Array<AttributedContent<any>>} contents - where to write the result
164
+ * @param {number} client
165
+ * @param {number} clock
166
+ * @param {boolean} deleted
167
+ * @param {AbstractContent} content
168
+ * @param {0|1|2} shouldRender - whether this should render or just result in a `retain` operation
169
+ */
170
+ readContent (contents, client, clock, deleted, content, shouldRender) {
171
+ const slice = (deleted ? this.deletes : this.inserts).slice(client, clock, content.getLength())
172
+ content = slice.length === 1 ? content : content.copy()
173
+ slice.forEach(s => {
174
+ const c = content
175
+ if (s.len < c.getLength()) {
176
+ content = c.splice(s.len)
177
+ }
178
+ if (!deleted || s.attrs != null || shouldRender) {
179
+ contents.push(new AttributedContent(c, s.clock, deleted, s.attrs, shouldRender))
180
+ }
181
+ })
182
+ }
183
+
184
+ /**
185
+ * @param {Item} item
186
+ * @return {number}
187
+ */
188
+ contentLength (item) {
189
+ if (!item.content.isCountable()) {
190
+ return 0
191
+ } else if (!item.deleted) {
192
+ return item.length
193
+ } else {
194
+ return this.deletes.sliceId(item.id, item.length).reduce((len, s) => s.attrs != null ? len + s.len : len, 0)
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Abstract class for associating Attributions to content / changes
201
+ *
202
+ * @implements AbstractAttributionManager
203
+ *
204
+ * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
205
+ */
206
+ export class NoAttributionsManager extends ObservableV2 {
207
+ /**
208
+ * @param {Array<AttributedContent<any>>} contents - where to write the result
209
+ * @param {number} _client
210
+ * @param {number} clock
211
+ * @param {boolean} deleted
212
+ * @param {AbstractContent} content
213
+ * @param {0|1|2} shouldRender - whether this should render or just result in a `retain` operation
214
+ */
215
+ readContent (contents, _client, clock, deleted, content, shouldRender) {
216
+ if (!deleted || shouldRender) {
217
+ contents.push(new AttributedContent(content, clock, deleted, null, shouldRender))
218
+ }
219
+ }
220
+
221
+ /**
222
+ * @param {Item} item
223
+ * @return {number}
224
+ */
225
+ contentLength (item) {
226
+ return (item.deleted || !item.content.isCountable()) ? 0 : item.length
227
+ }
228
+ }
229
+
230
+ export const noAttributionsManager = new NoAttributionsManager()
231
+
232
+ /**
233
+ * @param {StructStore} store
234
+ * @param {number} client
235
+ * @param {number} clock
236
+ * @param {number} len
237
+ */
238
+ const getItemContent = (store, client, clock, len) => {
239
+ // Retrieved item is never more fragmented than the newer item.
240
+ const prevItem = getItem(store, createID(client, clock))
241
+ const diffStart = clock - prevItem.id.clock
242
+ let content = prevItem.length > 1 ? prevItem.content.copy() : prevItem.content
243
+ // trim itemContent to the correct size.
244
+ if (diffStart > 0) {
245
+ content = content.splice(diffStart)
246
+ }
247
+ if (len < content.getLength()) {
248
+ content.splice(len)
249
+ }
250
+ return content
251
+ }
252
+
253
+ /**
254
+ * @param {Transaction?} tr - only specify this if you want to fill the content of deleted content
255
+ * @param {DiffAttributionManager} am
256
+ * @param {ID} start
257
+ * @param {ID} end
258
+ * @param {boolean} collectAll - collect as many items as possible. Accept adding redundant changes.
259
+ */
260
+ const collectSuggestedChanges = (tr, am, start, end, collectAll) => {
261
+ const inserts = createIdSet()
262
+ const deletes = createIdSet()
263
+ const store = am._nextDoc.store
264
+ /**
265
+ * make sure to collect suggestions until all formats are closed
266
+ * @type {Set<string>}
267
+ */
268
+ const openedCollectedFormats = new Set()
269
+ /**
270
+ * @type {Item?}
271
+ */
272
+ let item = getItem(store, start)
273
+ const endItem = start === end ? item : (end == null ? null : getItem(store, end))
274
+
275
+ // walk to the left and find first un-attributed change that is rendered
276
+ while (item.left != null) {
277
+ item = item.left
278
+ if (item.content instanceof ContentFormat && item.content.value == null) {
279
+ item = item.right
280
+ break
281
+ }
282
+ if (!item.deleted) {
283
+ const slice = am.inserts.slice(item.id.client, item.id.clock, item.length)
284
+ if (slice.some(s => s.attrs === null)) {
285
+ for (let i = slice.length - 1; i >= 0; i--) {
286
+ const s = slice[i]
287
+ if (s.attrs == null) break
288
+ inserts.add(item.id.client, s.clock, s.len)
289
+ }
290
+ item = item.right
291
+ break
292
+ }
293
+ }
294
+ }
295
+ let foundEndItem = false
296
+ // eslint-disable-next-line
297
+ itemLoop: while (item != null) {
298
+ const itemClient = item.id.client
299
+ const slice = (item.deleted ? am.deletes : am.inserts).slice(itemClient, item.id.clock, item.length)
300
+ foundEndItem ||= item === endItem
301
+ if (item.deleted) {
302
+ // item probably gc'd content. Need to split item and fill with content again
303
+ for (let i = slice.length - 1; i >= 0; i--) {
304
+ const s = slice[i]
305
+ if (s.attrs != null || collectAll) {
306
+ deletes.add(itemClient, s.clock, s.len)
307
+ if (collectAll) {
308
+ // in case item has been added and deleted this might be necessary. the forked document
309
+ // will automatically filter this if it doesn't have it already.
310
+ inserts.add(itemClient, s.clock, s.len)
311
+ }
312
+ }
313
+ if (tr != null) {
314
+ const splicedItem = getItemCleanStart(tr, createID(itemClient, s.clock))
315
+ if (s.attrs != null) {
316
+ splicedItem.content = getItemContent(am._prevDocStore, itemClient, s.clock, s.len)
317
+ }
318
+ }
319
+ }
320
+ } else {
321
+ if (item.content instanceof ContentFormat) {
322
+ const { key, value } = item.content
323
+ if (value == null) {
324
+ openedCollectedFormats.delete(key)
325
+ } else {
326
+ openedCollectedFormats.add(key)
327
+ }
328
+ }
329
+ for (let i = 0; i < slice.length; i++) {
330
+ const s = slice[i]
331
+ if (s.attrs != null) {
332
+ inserts.add(itemClient, s.clock, s.len)
333
+ } else if (foundEndItem && openedCollectedFormats.size === 0) {
334
+ // eslint-disable-next-line
335
+ break itemLoop
336
+ }
337
+ }
338
+ }
339
+ item = item.right
340
+ }
341
+ return { inserts, deletes }
342
+ }
343
+
344
+ export class Attributions {
345
+ constructor () {
346
+ this.inserts = createIdMap()
347
+ this.deletes = createIdMap()
348
+ }
349
+ }
350
+
351
+ /**
352
+ * @param {IdMap<any>|undefined} attrs
353
+ * @param {IdSet} slice
354
+ *
355
+ */
356
+ const extractAttributions = (attrs, slice) => attrs == null ? createIdMapFromIdSet(slice, []) : mergeIdMaps([intersectMaps(attrs, slice), createIdMapFromIdSet(slice, [])])
357
+
358
+ /**
359
+ * @implements AbstractAttributionManager
360
+ *
361
+ * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
362
+ */
363
+ export class DiffAttributionManager extends ObservableV2 {
364
+ /**
365
+ * @param {Doc} prevDoc
366
+ * @param {Doc} nextDoc
367
+ * @param {Object} [options] - options for the attribution manager
368
+ * @param {Attributions?} [options.attrs] - the attributes to apply to the diff
369
+ */
370
+ constructor (prevDoc, nextDoc, { attrs = null } = {}) {
371
+ super()
372
+ const _nextDocInserts = createInsertSetFromStructStore(nextDoc.store, false) // unmaintained
373
+ const _prevDocInserts = createInsertSetFromStructStore(prevDoc.store, false) // unmaintained
374
+ const nextDocDeletes = createDeleteSetFromStructStore(nextDoc.store) // maintained
375
+ const prevDocDeletes = createDeleteSetFromStructStore(prevDoc.store) // maintained
376
+ this.inserts = extractAttributions(attrs?.inserts, diffIdSet(_nextDocInserts, _prevDocInserts))
377
+ this.deletes = extractAttributions(attrs?.deletes, diffIdSet(nextDocDeletes, prevDocDeletes))
378
+ this._prevDoc = prevDoc
379
+ this._prevDocStore = prevDoc.store
380
+ this._nextDoc = nextDoc
381
+ // update before observer calls fired
382
+ this._nextBOH = nextDoc.on('beforeObserverCalls', tr => {
383
+ // update inserts
384
+ const diffInserts = diffIdSet(tr.insertSet, _prevDocInserts)
385
+ insertIntoIdMap(this.inserts, extractAttributions(attrs?.inserts, diffInserts))
386
+ // update deletes
387
+ const diffDeletes = diffIdSet(diffIdSet(tr.deleteSet, prevDocDeletes), this.inserts)
388
+ insertIntoIdMap(this.deletes, extractAttributions(attrs?.deletes, diffDeletes))
389
+ // @todo fire update ranges on `diffInserts` and `diffDeletes`
390
+ })
391
+ this._prevBOH = prevDoc.on('beforeObserverCalls', tr => {
392
+ insertIntoIdSet(_prevDocInserts, tr.insertSet)
393
+ insertIntoIdSet(prevDocDeletes, tr.deleteSet)
394
+ if (tr.insertSet.clients.size < 2) {
395
+ tr.insertSet.forEach((attrRange, client) => {
396
+ this.inserts.delete(client, attrRange.clock, attrRange.len)
397
+ })
398
+ } else {
399
+ this.inserts = diffIdMap(this.inserts, tr.insertSet)
400
+ }
401
+ // insertIntoIdMap(this.deletes, createIdMapFromIdSet(intersectSets(tr.deleteSet, this.deletes), [createAttributionItem('acceptDelete', 'unknown')]))
402
+ if (tr.deleteSet.clients.size < 2) {
403
+ tr.deleteSet.forEach((attrRange, client) => {
404
+ this.deletes.delete(client, attrRange.clock, attrRange.len)
405
+ })
406
+ } else {
407
+ this.deletes = diffIdMap(this.deletes, tr.deleteSet)
408
+ }
409
+ // fire event of "changed" attributions. exclude items that were added & deleted in the same
410
+ // transaction
411
+ this.emit('change', [diffIdSet(mergeIdSets([tr.insertSet, tr.deleteSet]), intersectSets(tr.insertSet, tr.deleteSet)), tr.origin, tr.local])
412
+ })
413
+ // changes from prevDoc should always flow into suggestionDoc
414
+ // changes from suggestionDoc only flow into ydoc if suggestion-mode is disabled
415
+ this._prevUpdateListener = prevDoc.on('update', (update, origin) => {
416
+ origin !== this && applyUpdate(nextDoc, update)
417
+ })
418
+ this._ndUpdateListener = nextDoc.on('update', (update, origin, _doc, tr) => {
419
+ // only if event is local and suggestion mode is enabled
420
+ if (!this.suggestionMode && tr.local && (this.suggestionOrigins == null || this.suggestionOrigins.some(o => o === origin))) {
421
+ applyUpdate(prevDoc, update, this)
422
+ }
423
+ })
424
+ this._afterTrListener = nextDoc.on('afterTransaction', (tr) => {
425
+ // apply deletes on attributed deletes (content that is already deleted, but is rendered by
426
+ // the attribution manager)
427
+ if (!this.suggestionMode && tr.local && (this.suggestionOrigins == null || this.suggestionOrigins.some(o => o === tr.origin))) {
428
+ const attributedDeletes = tr.meta.get('attributedDeletes')
429
+ if (attributedDeletes != null) {
430
+ transact(prevDoc, () => {
431
+ // apply attributed deletes if there are any
432
+ const ds = new UpdateEncoderV1()
433
+ encoding.writeVarUint(ds.restEncoder, 0) // encode 0 structs
434
+ writeIdSet(ds, attributedDeletes)
435
+ applyUpdate(prevDoc, ds.toUint8Array())
436
+ }, this)
437
+ }
438
+ }
439
+ })
440
+ this.suggestionMode = true
441
+ /**
442
+ * Optionally limit origins that may sync changes to the main doc if suggestion-mode is
443
+ * disabled.
444
+ *
445
+ * @type {Array<any>?}
446
+ */
447
+ this.suggestionOrigins = null
448
+ this._destroyHandler = nextDoc.on('destroy', this.destroy.bind(this))
449
+ prevDoc.on('destroy', this._destroyHandler)
450
+ }
451
+
452
+ destroy () {
453
+ super.destroy()
454
+ this._nextDoc.off('destroy', this._destroyHandler)
455
+ this._prevDoc.off('destroy', this._destroyHandler)
456
+ this._nextDoc.off('beforeObserverCalls', this._nextBOH)
457
+ this._prevDoc.off('beforeObserverCalls', this._prevBOH)
458
+ this._prevDoc.off('update', this._prevUpdateListener)
459
+ this._nextDoc.off('update', this._ndUpdateListener)
460
+ this._nextDoc.off('afterTransaction', this._afterTrListener)
461
+ }
462
+
463
+ acceptAllChanges () {
464
+ applyUpdate(this._prevDoc, encodeStateAsUpdate(this._nextDoc))
465
+ }
466
+
467
+ rejectAllChanges () {
468
+ this._prevDoc.transact(tr => {
469
+ applyUpdate(this._prevDoc, encodeStateAsUpdate(this._nextDoc))
470
+ const um = new UndoManager(this._prevDoc)
471
+ um.undoStack.push(new StackItem(tr.insertSet, tr.deleteSet))
472
+ um.undo()
473
+ um.destroy()
474
+ })
475
+ }
476
+
477
+ /**
478
+ * @param {ID} start
479
+ * @param {ID} end
480
+ */
481
+ acceptChanges (start, end = start) {
482
+ const { inserts, deletes } = collectSuggestedChanges(null, this, start, end, true)
483
+ const encoder = new UpdateEncoderV1()
484
+ writeStructsFromIdSet(encoder, this._nextDoc.store, inserts)
485
+ writeIdSet(encoder, deletes)
486
+ applyUpdate(this._prevDoc, encoder.toUint8Array())
487
+ }
488
+
489
+ /**
490
+ * @param {ID} start
491
+ * @param {ID} end
492
+ */
493
+ rejectChanges (start, end = start) {
494
+ this._nextDoc.transact(tr => {
495
+ const { inserts, deletes } = collectSuggestedChanges(tr, this, start, end, false)
496
+ const encoder = new UpdateEncoderV1()
497
+ writeStructsFromIdSet(encoder, this._nextDoc.store, inserts)
498
+ writeIdSet(encoder, deletes)
499
+ const um = new UndoManager(this._nextDoc)
500
+ um.undoStack.push(new StackItem(inserts, deletes))
501
+ um.undo()
502
+ um.destroy()
503
+ })
504
+ this.acceptChanges(start, end)
505
+ }
506
+
507
+ /**
508
+ * @param {Array<AttributedContent<any>>} contents - where to write the result
509
+ * @param {number} client
510
+ * @param {number} clock
511
+ * @param {boolean} deleted
512
+ * @param {AbstractContent} _content
513
+ * @param {0|1|2} shouldRender - whether this should render or just result in a `retain` operation
514
+ */
515
+ readContent (contents, client, clock, deleted, _content, shouldRender) {
516
+ const slice = (deleted ? this.deletes : this.inserts).slice(client, clock, _content.getLength())
517
+ /**
518
+ * @type {AbstractContent?}
519
+ */
520
+ let content = slice.length === 1 ? _content : _content.copy()
521
+ for (let i = 0; i < slice.length; i++) {
522
+ const s = slice[i]
523
+ if (content == null || content instanceof ContentDeleted) {
524
+ if ((!shouldRender && s.attrs == null) || this.inserts.has(client, s.clock)) {
525
+ continue
526
+ }
527
+ // Retrieved item is never more fragmented than the newer item.
528
+ const prevItem = getItem(this._prevDocStore, createID(client, s.clock))
529
+ const diffStart = s.clock - prevItem.id.clock
530
+ content = prevItem.length > 1 ? prevItem.content.copy() : prevItem.content
531
+ // trim itemContent to the correct size.
532
+ if (diffStart > 0) {
533
+ content = content.splice(diffStart)
534
+ }
535
+ }
536
+ const c = /** @type {AbstractContent} */ (content)
537
+ const clen = c.getLength()
538
+ if (clen < s.len) {
539
+ slice.splice(i + 1, 0, createMaybeAttrRange(s.clock + clen, s.len - clen, s.attrs))
540
+ s.len = clen
541
+ }
542
+ content = s.len < clen ? c.splice(s.len) : null
543
+ if (shouldRender || !deleted || s.attrs != null) {
544
+ contents.push(new AttributedContent(c, s.clock, deleted, s.attrs, shouldRender))
545
+ }
546
+ }
547
+ }
548
+
549
+ /**
550
+ * @param {Item} item
551
+ * @return {number}
552
+ */
553
+ contentLength (item) {
554
+ if (!item.deleted) {
555
+ return item.content.isCountable() ? item.length : 0
556
+ }
557
+ /**
558
+ * @type {Array<AttributedContent<any>>}
559
+ */
560
+ const cs = []
561
+ this.readContent(cs, item.id.client, item.id.clock, true, item.content, 0)
562
+ return cs.reduce((cnt, c) => cnt + ((c.attrs != null && c.content.isCountable()) ? c.content.getLength() : 0), 0)
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Attribute changes from ydoc1 to ydoc2.
568
+ *
569
+ * @param {Doc} prevDoc
570
+ * @param {Doc} nextDoc
571
+ * @param {Object} [options] - options for the attribution manager
572
+ * @param {Attributions?} [options.attrs] - the attributes to apply to the diff
573
+ */
574
+ export const createAttributionManagerFromDiff = (prevDoc, nextDoc, options) => new DiffAttributionManager(prevDoc, nextDoc, options)
575
+
576
+ /**
577
+ * Intended for projects that used the v13 snapshot feature. With this AttributionManager you can
578
+ * read content similar to the previous snapshot api. Requires that `ydoc.gc` is turned off.
579
+ *
580
+ * @implements AbstractAttributionManager
581
+ *
582
+ * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
583
+ */
584
+ export class SnapshotAttributionManager extends ObservableV2 {
585
+ /**
586
+ * @param {Snapshot} prevSnapshot
587
+ * @param {Snapshot} nextSnapshot
588
+ * @param {Object} [options] - options for the attribution manager
589
+ * @param {Array<import('./IdMap.js').ContentAttribute<any>>} [options.attrs] - the attributes to apply to the diff
590
+ */
591
+ constructor (prevSnapshot, nextSnapshot) {
592
+ super()
593
+ this.prevSnapshot = prevSnapshot
594
+ this.nextSnapshot = nextSnapshot
595
+ const inserts = createIdMap()
596
+ const deletes = createIdMapFromIdSet(diffIdSet(nextSnapshot.ds, prevSnapshot.ds), [createContentAttribute('change', '')])
597
+ nextSnapshot.sv.forEach((clock, client) => {
598
+ const prevClock = prevSnapshot.sv.get(client) || 0
599
+ inserts.add(client, 0, prevClock, []) // content is included in prevSnapshot is rendered without attributes
600
+ inserts.add(client, prevClock, clock - prevClock, [createContentAttribute('change', '')]) // content is rendered as "inserted"
601
+ })
602
+ this.attrs = mergeIdMaps([diffIdMap(inserts, prevSnapshot.ds), deletes])
603
+ }
604
+
605
+ /**
606
+ * @param {Array<AttributedContent<any>>} contents - where to write the result
607
+ * @param {number} client
608
+ * @param {number} clock
609
+ * @param {boolean} _deleted
610
+ * @param {AbstractContent} content
611
+ * @param {0|1|2} shouldRender - whether this should render or just result in a `retain` operation
612
+ */
613
+ readContent (contents, client, clock, _deleted, content, shouldRender) {
614
+ if ((this.nextSnapshot.sv.get(client) ?? 0) <= clock) return // future item that should not be displayed
615
+ const slice = this.attrs.slice(client, clock, content.getLength())
616
+ content = slice.length === 1 ? content : content.copy()
617
+ slice.forEach(s => {
618
+ const deleted = this.nextSnapshot.ds.has(client, s.clock)
619
+ const nonExistend = (this.nextSnapshot.sv.get(client) ?? 0) <= s.clock
620
+ const c = content
621
+ if (s.len < c.getLength()) {
622
+ content = c.splice(s.len)
623
+ }
624
+ if (nonExistend) return
625
+ if (shouldRender || !deleted || (s.attrs != null && s.attrs.length > 0)) {
626
+ let attrsWithoutChange = s.attrs?.filter(attr => attr.name !== 'change') ?? null
627
+ if (s.attrs?.length === 0) {
628
+ attrsWithoutChange = null
629
+ }
630
+ contents.push(new AttributedContent(c, s.clock, deleted, attrsWithoutChange, shouldRender))
631
+ }
632
+ })
633
+ }
634
+
635
+ /**
636
+ * @param {Item} item
637
+ * @return {number}
638
+ */
639
+ contentLength (item) {
640
+ return item.content.isCountable()
641
+ ? (item.deleted
642
+ ? this.attrs.sliceId(item.id, item.length).reduce((len, s) => s.attrs != null ? len + s.len : len, 0)
643
+ : item.length
644
+ )
645
+ : 0
646
+ }
647
+ }
648
+
649
+ /**
650
+ * @param {Snapshot} prevSnapshot
651
+ * @param {Snapshot} nextSnapshot
652
+ */
653
+ export const createAttributionManagerFromSnapshots = (prevSnapshot, nextSnapshot = prevSnapshot) => new SnapshotAttributionManager(prevSnapshot, nextSnapshot)