@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.
- package/README.md +7 -5
- package/dist/src/index.d.ts +2 -1
- package/dist/src/internals.d.ts +2 -9
- package/dist/src/structs/ContentType.d.ts +6 -12
- package/dist/src/structs/ContentType.d.ts.map +1 -1
- package/dist/src/structs/Item.d.ts +5 -6
- package/dist/src/structs/Item.d.ts.map +1 -1
- package/dist/src/utils/AttributionManager.d.ts +16 -14
- package/dist/src/utils/AttributionManager.d.ts.map +1 -1
- package/dist/src/utils/Doc.d.ts +7 -70
- package/dist/src/utils/Doc.d.ts.map +1 -1
- package/dist/src/utils/ID.d.ts +2 -2
- package/dist/src/utils/ID.d.ts.map +1 -1
- package/dist/src/utils/IdMap.d.ts +22 -19
- package/dist/src/utils/IdMap.d.ts.map +1 -1
- package/dist/src/utils/IdSet.d.ts +6 -6
- package/dist/src/utils/IdSet.d.ts.map +1 -1
- package/dist/src/utils/RelativePosition.d.ts +8 -8
- package/dist/src/utils/RelativePosition.d.ts.map +1 -1
- package/dist/src/utils/Snapshot.d.ts +3 -3
- package/dist/src/utils/Snapshot.d.ts.map +1 -1
- package/dist/src/utils/Transaction.d.ts +9 -5
- package/dist/src/utils/Transaction.d.ts.map +1 -1
- package/dist/src/utils/UndoManager.d.ts +14 -12
- package/dist/src/utils/UndoManager.d.ts.map +1 -1
- package/dist/src/utils/UpdateDecoder.d.ts +8 -4
- package/dist/src/utils/UpdateDecoder.d.ts.map +1 -1
- package/dist/src/utils/UpdateEncoder.d.ts +2 -0
- package/dist/src/utils/UpdateEncoder.d.ts.map +1 -1
- package/dist/src/utils/YEvent.d.ts +21 -42
- package/dist/src/utils/YEvent.d.ts.map +1 -1
- package/dist/src/utils/encoding.d.ts +3 -3
- package/dist/src/utils/encoding.d.ts.map +1 -1
- package/dist/src/utils/isParentOf.d.ts +1 -1
- package/dist/src/utils/isParentOf.d.ts.map +1 -1
- package/dist/src/utils/logging.d.ts +2 -2
- package/dist/src/utils/logging.d.ts.map +1 -1
- package/dist/src/utils/meta.d.ts +71 -0
- package/dist/src/utils/meta.d.ts.map +1 -0
- package/dist/src/utils/ts.d.ts +4 -0
- package/dist/src/utils/ts.d.ts.map +1 -0
- package/dist/src/utils/updates.d.ts +11 -11
- package/dist/src/utils/updates.d.ts.map +1 -1
- package/dist/src/ytype.d.ts +498 -0
- package/dist/src/ytype.d.ts.map +1 -0
- package/dist/tests/IdMap.tests.d.ts.map +1 -1
- package/dist/tests/attribution.tests.d.ts +1 -0
- package/dist/tests/attribution.tests.d.ts.map +1 -1
- package/dist/tests/compatibility.tests.d.ts.map +1 -1
- package/dist/tests/doc.tests.d.ts.map +1 -1
- package/dist/tests/relativePositions.tests.d.ts +9 -9
- package/dist/tests/relativePositions.tests.d.ts.map +1 -1
- package/dist/tests/snapshot.tests.d.ts.map +1 -1
- package/dist/tests/testHelper.d.ts +28 -27
- package/dist/tests/testHelper.d.ts.map +1 -1
- package/dist/tests/undo-redo.tests.d.ts.map +1 -1
- package/dist/tests/updates.tests.d.ts +2 -1
- package/dist/tests/updates.tests.d.ts.map +1 -1
- package/dist/tests/y-array.tests.d.ts +0 -2
- package/dist/tests/y-array.tests.d.ts.map +1 -1
- package/dist/tests/y-map.tests.d.ts +0 -3
- package/dist/tests/y-map.tests.d.ts.map +1 -1
- package/dist/tests/y-text.tests.d.ts +1 -1
- package/dist/tests/y-text.tests.d.ts.map +1 -1
- package/dist/tests/y-xml.tests.d.ts +0 -1
- package/dist/tests/y-xml.tests.d.ts.map +1 -1
- package/package.json +16 -16
- package/src/index.js +156 -0
- package/src/internals.js +35 -0
- package/src/structs/AbstractStruct.js +59 -0
- package/src/structs/ContentAny.js +115 -0
- package/src/structs/ContentBinary.js +93 -0
- package/src/structs/ContentDeleted.js +101 -0
- package/src/structs/ContentDoc.js +141 -0
- package/src/structs/ContentEmbed.js +98 -0
- package/src/structs/ContentFormat.js +105 -0
- package/src/structs/ContentJSON.js +119 -0
- package/src/structs/ContentString.js +113 -0
- package/src/structs/ContentType.js +152 -0
- package/src/structs/GC.js +80 -0
- package/src/structs/Item.js +841 -0
- package/src/structs/Skip.js +75 -0
- package/src/utils/AttributionManager.js +653 -0
- package/src/utils/Doc.js +266 -0
- package/src/utils/EventHandler.js +87 -0
- package/src/utils/ID.js +89 -0
- package/src/utils/IdMap.js +673 -0
- package/src/utils/IdSet.js +825 -0
- package/src/utils/RelativePosition.js +352 -0
- package/src/utils/Snapshot.js +220 -0
- package/src/utils/StructSet.js +137 -0
- package/src/utils/StructStore.js +289 -0
- package/src/utils/Transaction.js +671 -0
- package/src/utils/UndoManager.js +406 -0
- package/src/utils/UpdateDecoder.js +285 -0
- package/src/utils/UpdateEncoder.js +327 -0
- package/src/utils/YEvent.js +189 -0
- package/src/utils/delta-helpers.js +54 -0
- package/src/utils/encoding.js +623 -0
- package/src/utils/isParentOf.js +21 -0
- package/src/utils/logging.js +21 -0
- package/src/utils/meta.js +190 -0
- package/src/utils/ts.js +3 -0
- package/src/utils/updates.js +802 -0
- package/src/ytype.js +1962 -0
- package/dist/Skip-CE05BUF8.js +0 -11875
- package/dist/Skip-CE05BUF8.js.map +0 -1
- package/dist/index-C21sDQ5u.js +0 -163
- package/dist/index-C21sDQ5u.js.map +0 -1
- package/dist/internals.js +0 -25
- package/dist/internals.js.map +0 -1
- package/dist/src/types/AbstractType.d.ts +0 -239
- package/dist/src/types/AbstractType.d.ts.map +0 -1
- package/dist/src/types/YArray.d.ts +0 -128
- package/dist/src/types/YArray.d.ts.map +0 -1
- package/dist/src/types/YMap.d.ts +0 -112
- package/dist/src/types/YMap.d.ts.map +0 -1
- package/dist/src/types/YText.d.ts +0 -216
- package/dist/src/types/YText.d.ts.map +0 -1
- package/dist/src/types/YXmlElement.d.ts +0 -106
- package/dist/src/types/YXmlElement.d.ts.map +0 -1
- package/dist/src/types/YXmlFragment.d.ts +0 -143
- package/dist/src/types/YXmlFragment.d.ts.map +0 -1
- package/dist/src/types/YXmlHook.d.ts +0 -32
- package/dist/src/types/YXmlHook.d.ts.map +0 -1
- package/dist/src/types/YXmlText.d.ts +0 -34
- package/dist/src/types/YXmlText.d.ts.map +0 -1
- package/dist/src/utils/AbstractConnector.d.ts +0 -20
- package/dist/src/utils/AbstractConnector.d.ts.map +0 -1
- package/dist/src/utils/types.d.ts +0 -7
- package/dist/src/utils/types.d.ts.map +0 -1
- package/dist/testHelper.js +0 -617
- package/dist/testHelper.js.map +0 -1
- package/dist/yjs.js +0 -26
- 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
|
+
}
|