mirror of
https://github.com/yjs/yjs.git
synced 2026-08-29 10:09:05 +02:00
flatten, no more circular dependencies
This commit is contained in:
45
global.d.ts
vendored
45
global.d.ts
vendored
@@ -1,9 +1,50 @@
|
||||
declare type GC = import('./src/structs/GC.js').GC
|
||||
declare type Item = import('./src/structs/Item.js').Item
|
||||
declare type IdRange = import('./src/utils/IdSet.js').IdRange
|
||||
declare type IdSet = import('./src/utils/IdSet.js').IdSet
|
||||
declare type Skip = import('./src/structs/Skip.js').Skip
|
||||
declare type IdRange = import('./src/utils/ids.js').IdRange
|
||||
declare type IdSet = import('./src/utils/ids.js').IdSet
|
||||
declare type IdMap<Attrs> = import('./src/utils/ids.js').IdMap<Attrs>
|
||||
declare type AttrRanges<Attrs = any> = import('./src/utils/ids.js').AttrRanges<Attrs>
|
||||
declare type AttrRange<Attrs = any> = import('./src/utils/ids.js').AttrRange<Attrs>
|
||||
declare type ContentAttribute<V=any> = import('./src/utils/ids.js').ContentAttribute<V>
|
||||
|
||||
declare type BlockSet = import('./src/utils/BlockSet.js').BlockSet
|
||||
declare type UpdateDecoderV1 = import('./src/utils/UpdateDecoder.js').UpdateDecoderV1
|
||||
declare type UpdateDecoderV2 = import('./src/utils/UpdateDecoder.js').UpdateDecoderV2
|
||||
declare type UpdateEncoderV1 = import('./src/utils/UpdateEncoder.js').UpdateEncoderV1
|
||||
declare type UpdateEncoderV2 = import('./src/utils/UpdateEncoder.js').UpdateEncoderV2
|
||||
declare type IdSetEncoderV1 = import('./src/utils/UpdateEncoder.js').IdSetEncoderV1
|
||||
declare type IdSetEncoderV2 = import('./src/utils/UpdateEncoder.js').IdSetEncoderV2
|
||||
declare type IdSetDecoderV1 = import('./src/utils/UpdateDecoder.js').IdSetDecoderV1
|
||||
declare type IdSetDecoderV2 = import('./src/utils/UpdateDecoder.js').IdSetDecoderV2
|
||||
|
||||
declare type ID = import('./src/utils/ID.js').ID
|
||||
declare type Transaction = import('./src/utils/Transaction.js').Transaction
|
||||
declare type StructStore = import('./src/utils/StructStore.js').StructStore
|
||||
declare type Doc = import('./src/utils/Doc.js').Doc
|
||||
declare type YType<DConf extends import('lib0/delta').DeltaConf = any> = import('./src/ytype.js').YType<DConf>
|
||||
declare type YEvent<DConf extends import('lib0/delta').DeltaConf> = import('./src/utils/YEvent.js').YEvent<DConf>
|
||||
declare type EventHandler<ARG1=any,ARG2=any> = import('./src/utils/EventHandler.js').EventHandler<ARG1, ARG2>
|
||||
|
||||
declare type AbstractStruct = import('./src/structs/AbstractStruct.js').AbstractStruct
|
||||
declare type AbstractContent = import('./src/structs/Item.js').AbstractContent
|
||||
declare type ContentType = import('./src/structs/Item.js').ContentType
|
||||
declare type ContentAny = import('./src/structs/Item.js').ContentAny
|
||||
declare type ContentDoc = import('./src/structs/Item.js').ContentDoc
|
||||
declare type ContentJSON = import('./src/structs/Item.js').ContentJSON
|
||||
declare type ContentEmbed = import('./src/structs/Item.js').ContentEmbed
|
||||
declare type ContentFormat = import('./src/structs/Item.js').ContentFormat
|
||||
declare type ContentDeleted = import('./src/structs/Item.js').ContentDeleted
|
||||
declare type ContentString = import('./src/structs/Item.js').ContentString
|
||||
|
||||
declare type DeltaConf = import('lib0/delta').DeltaConf
|
||||
declare type Delta<DConf extends DeltaConf> = import('lib0/delta').Delta<DConf>
|
||||
|
||||
// @todo the below should have a separate Y/[mod] export
|
||||
declare type StackItem = import('./src/utils/UndoManager.js').StackItem
|
||||
declare type UndoManager = import('./src/utils/UndoManager.js').UndoManager
|
||||
declare type AbstractAttributionManager = import('./src/utils/attribution-manager-helpers.js').AbstractAttributionManager
|
||||
declare type Attribution = import('./src/utils/attribution-manager-helpers.js').Attribution
|
||||
declare type AttributedContent<T = any> = import('./src/utils/attribution-manager-helpers.js').AttributedContent<T>
|
||||
|
||||
declare type Snapshot = import('./src/utils/Snapshot.js').Snapshot
|
||||
|
||||
567
package-lock.json
generated
567
package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"@types/node": "^22.14.1",
|
||||
"@y/protocols": "^1.0.6-rc.1",
|
||||
"@y/y": ".",
|
||||
"dpdm": "^4.0.1",
|
||||
"markdownlint": "^0.40.0",
|
||||
"markdownlint-cli": "^0.45.0",
|
||||
"standard": "^17.1.2",
|
||||
@@ -736,6 +737,97 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"restore-cursor": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-spinners": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
|
||||
"integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -964,6 +1056,95 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dpdm": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dpdm/-/dpdm-4.0.1.tgz",
|
||||
"integrity": "sha512-9NutC+V6A/4GHgqsqRTMQNDlhipK3lyPWBJ8zIVT8lTncy71d1BzR7sbCO0Dv2zTZ4XSxvbEfMyd0NDDLU0+fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.6.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
"glob": "^13.0.0",
|
||||
"ora": "^9.1.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"dpdm": "lib/bin/dpdm.js"
|
||||
}
|
||||
},
|
||||
"node_modules/dpdm/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/dpdm/node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/dpdm/node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dpdm/node_modules/glob": {
|
||||
"version": "13.0.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
|
||||
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.2.2",
|
||||
"minipass": "^7.1.3",
|
||||
"path-scurry": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/dpdm/node_modules/minimatch": {
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -979,6 +1160,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@@ -1179,6 +1367,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -1893,6 +2091,21 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
|
||||
"integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -1951,6 +2164,16 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
|
||||
@@ -2556,6 +2779,19 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-interactive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-map": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
|
||||
@@ -2708,6 +2944,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-weakmap": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
|
||||
@@ -2870,6 +3119,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonpointer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
|
||||
@@ -3025,6 +3287,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
||||
"integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-unicode-supported": "^2.0.0",
|
||||
"yoctocolors": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -3692,6 +3971,19 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mimic-function": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
@@ -3894,6 +4186,22 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-function": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -3912,6 +4220,42 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ora": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz",
|
||||
"integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.6.2",
|
||||
"cli-cursor": "^5.0.0",
|
||||
"cli-spinners": "^3.2.0",
|
||||
"is-interactive": "^2.0.0",
|
||||
"is-unicode-supported": "^2.1.0",
|
||||
"log-symbols": "^7.0.1",
|
||||
"stdin-discarder": "^0.3.1",
|
||||
"string-width": "^8.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ora/node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
@@ -4338,6 +4682,23 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"onetime": "^7.0.0",
|
||||
"signal-exit": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
@@ -4748,6 +5109,19 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stdin-discarder": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz",
|
||||
"integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||
@@ -4988,6 +5362,13 @@
|
||||
"strip-bom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -5139,6 +5520,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -5274,6 +5665,84 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -5291,6 +5760,91 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^9.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"string-width": "^7.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
@@ -5303,6 +5857,19 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
||||
"integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
package.json
11
package.json
@@ -15,7 +15,7 @@
|
||||
"test-extensive": "node ./tests/index.js --production --repetition-time 10000",
|
||||
"dist": "npm run clean && (tsc --skipLibCheck --noEmit false|| true)",
|
||||
"release": "npm test && PRODUCTION=1 npm run dist && test -e dist/src/index.d.ts && np",
|
||||
"lint": "markdownlint README.md && standard && tsc --skipLibCheck",
|
||||
"lint": "markdownlint README.md && standard && tsc --skipLibCheck && npx dpdm --exit-code circular:1 --warning false --tree false src/index.js",
|
||||
"debug": "npm run gentesthtml && 0serve -o test.html",
|
||||
"debug:node": "node --inspect-brk tests/index.js",
|
||||
"trace-deopt": "clear && node --trace-deopt ./tests/index.js",
|
||||
@@ -27,10 +27,6 @@
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"default": "./src/index.js"
|
||||
},
|
||||
"./internals": {
|
||||
"types": "./dist/src/internals.d.ts",
|
||||
"default": "./src/internals.js"
|
||||
},
|
||||
"./meta": {
|
||||
"types": "./dist/src/utils/meta.d.ts",
|
||||
"default": "./src/utils/meta.js"
|
||||
@@ -81,11 +77,12 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.1",
|
||||
"@y/protocols": "^1.0.6-rc.1",
|
||||
"@y/y": ".",
|
||||
"dpdm": "^4.0.1",
|
||||
"markdownlint": "^0.40.0",
|
||||
"markdownlint-cli": "^0.45.0",
|
||||
"standard": "^17.1.2",
|
||||
"typescript": "^5.9.3",
|
||||
"@y/y": "."
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8.0.0",
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
export default [{
|
||||
input: {
|
||||
yjs: './src/index.js',
|
||||
testHelper: './tests/testHelper.js',
|
||||
internals: './src/internals.js'
|
||||
testHelper: './tests/testHelper.js'
|
||||
},
|
||||
output: {
|
||||
dir: 'dist',
|
||||
|
||||
151
src/index.js
151
src/index.js
@@ -1,132 +1,29 @@
|
||||
/** eslint-env browser */
|
||||
|
||||
export {
|
||||
Doc,
|
||||
Transaction,
|
||||
YType as Type,
|
||||
YEvent,
|
||||
Item,
|
||||
AbstractStruct,
|
||||
GC,
|
||||
Skip,
|
||||
ContentBinary,
|
||||
ContentDeleted,
|
||||
ContentDoc,
|
||||
ContentEmbed,
|
||||
ContentFormat,
|
||||
ContentJSON,
|
||||
ContentAny,
|
||||
ContentString,
|
||||
ContentType,
|
||||
getTypeChildren,
|
||||
createRelativePositionFromTypeIndex,
|
||||
createRelativePositionFromJSON,
|
||||
createAbsolutePositionFromRelativePosition,
|
||||
compareRelativePositions,
|
||||
AbsolutePosition,
|
||||
RelativePosition,
|
||||
ID,
|
||||
createID,
|
||||
compareIDs,
|
||||
getState,
|
||||
Snapshot,
|
||||
createSnapshot,
|
||||
cleanupYTextFormatting,
|
||||
snapshot,
|
||||
emptySnapshot,
|
||||
findRootTypeKey,
|
||||
findIndexSS,
|
||||
getItem,
|
||||
getItemCleanStart,
|
||||
getItemCleanEnd,
|
||||
typeMapGetSnapshot,
|
||||
typeMapGetAllSnapshot,
|
||||
createDocFromSnapshot,
|
||||
applyUpdate,
|
||||
applyUpdateV2,
|
||||
readUpdate,
|
||||
readUpdateV2,
|
||||
encodeStateAsUpdate,
|
||||
encodeStateAsUpdateV2,
|
||||
encodeStateVector,
|
||||
UndoManager,
|
||||
decodeSnapshot,
|
||||
encodeSnapshot,
|
||||
decodeSnapshotV2,
|
||||
encodeSnapshotV2,
|
||||
decodeStateVector,
|
||||
logUpdate,
|
||||
logUpdateV2,
|
||||
decodeUpdate,
|
||||
decodeUpdateV2,
|
||||
relativePositionToJSON,
|
||||
isParentOf,
|
||||
equalSnapshots,
|
||||
tryGc,
|
||||
transact,
|
||||
logType,
|
||||
mergeUpdates,
|
||||
mergeUpdatesV2,
|
||||
encodeStateVectorFromUpdate,
|
||||
encodeStateVectorFromUpdateV2,
|
||||
encodeRelativePosition,
|
||||
decodeRelativePosition,
|
||||
diffUpdate,
|
||||
diffUpdateV2,
|
||||
convertUpdateFormatV1ToV2,
|
||||
convertUpdateFormatV2ToV1,
|
||||
obfuscateUpdate,
|
||||
obfuscateUpdateV2,
|
||||
UpdateEncoderV1,
|
||||
UpdateEncoderV2,
|
||||
UpdateDecoderV1,
|
||||
UpdateDecoderV2,
|
||||
snapshotContainsUpdate,
|
||||
// idset
|
||||
IdSet,
|
||||
equalIdSets,
|
||||
createDeleteSetFromStructStore,
|
||||
IdMap,
|
||||
createIdMap,
|
||||
createContentAttribute,
|
||||
createInsertSetFromStructStore,
|
||||
diffIdMap,
|
||||
diffIdSet,
|
||||
ContentAttribute,
|
||||
encodeIdMap,
|
||||
createIdMapFromIdSet,
|
||||
TwosetAttributionManager,
|
||||
noAttributionsManager,
|
||||
AbstractAttributionManager,
|
||||
iterateStructsByIdSet,
|
||||
createAttributionManagerFromDiff,
|
||||
DiffAttributionManager,
|
||||
createAttributionManagerFromSnapshots,
|
||||
SnapshotAttributionManager,
|
||||
createIdSet,
|
||||
mergeIdSets,
|
||||
cloneDoc,
|
||||
createContentIdsFromUpdate,
|
||||
createContentIdsFromUpdateV2,
|
||||
insertIntoIdMap,
|
||||
insertIntoIdSet,
|
||||
mergeIdMaps,
|
||||
readIdMap,
|
||||
readIdSet,
|
||||
decodeIdMap,
|
||||
diffDocsToDelta,
|
||||
getPathTo,
|
||||
Attributions,
|
||||
filterIdMap,
|
||||
undoContentIds,
|
||||
intersectUpdateWithContentIds,
|
||||
intersectUpdateWithContentIdsV2,
|
||||
createDocFromUpdate,
|
||||
createDocFromUpdateV2,
|
||||
$ytype,
|
||||
$ytypeAny,
|
||||
$attributionManager
|
||||
} from './internals.js'
|
||||
// Order matters: follows internals.js ordering to avoid circular dependency issues
|
||||
|
||||
export { IdSet, equalIdSets, createDeleteSetFromStructStore, createInsertSetFromStructStore, diffIdSet, createIdSet, mergeIdSets, insertIntoIdSet, iterateStructsByIdSet, readIdSet, IdMap, createIdMap, createContentAttribute, ContentAttribute, diffIdMap, encodeIdMap, createIdMapFromIdSet, insertIntoIdMap, mergeIdMaps, readIdMap, decodeIdMap, filterIdMap } from './utils/ids.js'
|
||||
export { Doc } from './utils/Doc.js'
|
||||
export { UpdateDecoderV1, UpdateDecoderV2 } from './utils/UpdateDecoder.js'
|
||||
export { UpdateEncoderV1, UpdateEncoderV2 } from './utils/UpdateEncoder.js'
|
||||
export { applyUpdate, applyUpdateV2, readUpdate, readUpdateV2, encodeStateAsUpdate, encodeStateAsUpdateV2, encodeStateVector, decodeStateVector, diffUpdate, diffUpdateV2, mergeUpdates, mergeUpdatesV2, createDocFromUpdate, createDocFromUpdateV2, cloneDoc } from './utils/encoding.js'
|
||||
export { ID, createID, compareIDs, findRootTypeKey } from './utils/ID.js'
|
||||
export { isParentOf } from './utils/isParentOf.js'
|
||||
export { logType } from './utils/logging.js'
|
||||
export { createRelativePositionFromTypeIndex, createRelativePositionFromJSON, createAbsolutePositionFromRelativePosition, compareRelativePositions, AbsolutePosition, RelativePosition, relativePositionToJSON, encodeRelativePosition, decodeRelativePosition } from './utils/RelativePosition.js'
|
||||
export { Snapshot, createSnapshot, snapshot, emptySnapshot, createDocFromSnapshot, decodeSnapshot, encodeSnapshot, decodeSnapshotV2, encodeSnapshotV2, equalSnapshots, snapshotContainsUpdate } from './utils/Snapshot.js'
|
||||
export { findIndexSS, getItemCleanStart, getItemCleanEnd, tryGc } from './utils/transaction-helpers.js'
|
||||
export { Transaction, transact, cleanupYTextFormatting } from './utils/Transaction.js'
|
||||
export { UndoManager, undoContentIds } from './utils/UndoManager.js'
|
||||
export { logUpdate, logUpdateV2, decodeUpdate, decodeUpdateV2, encodeStateVectorFromUpdate, encodeStateVectorFromUpdateV2, convertUpdateFormatV1ToV2, convertUpdateFormatV2ToV1, obfuscateUpdate, obfuscateUpdateV2, createContentIdsFromUpdate, createContentIdsFromUpdateV2, intersectUpdateWithContentIds, intersectUpdateWithContentIdsV2 } from './utils/updates.js'
|
||||
export { YEvent, getPathTo } from './utils/YEvent.js'
|
||||
export { TwosetAttributionManager, noAttributionsManager, AbstractAttributionManager, createAttributionManagerFromDiff, DiffAttributionManager, createAttributionManagerFromSnapshots, SnapshotAttributionManager, Attributions, $attributionManager } from './utils/AttributionManager.js'
|
||||
export { diffDocsToDelta } from './utils/delta-helpers.js'
|
||||
export { YType as Type, getTypeChildren, typeMapGetSnapshot, typeMapGetAllSnapshot, $ytype, $ytypeAny } from './ytype.js'
|
||||
export { AbstractStruct } from './structs/AbstractStruct.js'
|
||||
export { GC } from './structs/GC.js'
|
||||
export { Item, ContentBinary, ContentDeleted, ContentDoc, ContentEmbed, ContentFormat, ContentJSON, ContentAny, ContentString, ContentType } from './structs/Item.js'
|
||||
export { Skip } from './structs/Skip.js'
|
||||
|
||||
export * from './utils/meta.js'
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
export * from './utils/IdSet.js'
|
||||
export * from './utils/Doc.js'
|
||||
export * from './utils/UpdateDecoder.js'
|
||||
export * from './utils/UpdateEncoder.js'
|
||||
export * from './utils/encoding.js'
|
||||
export * from './utils/EventHandler.js'
|
||||
export * from './utils/ID.js'
|
||||
export * from './utils/isParentOf.js'
|
||||
export * from './utils/logging.js'
|
||||
export * from './utils/RelativePosition.js'
|
||||
export * from './utils/Snapshot.js'
|
||||
export * from './utils/StructStore.js'
|
||||
export * from './utils/Transaction.js'
|
||||
export * from './utils/UndoManager.js'
|
||||
export * from './utils/updates.js'
|
||||
export * from './utils/YEvent.js'
|
||||
export * from './utils/BlockSet.js'
|
||||
export * from './utils/IdMap.js'
|
||||
export * from './utils/AttributionManager.js'
|
||||
export * from './utils/delta-helpers.js'
|
||||
export * from './utils/meta.js'
|
||||
export * from './ytype.js'
|
||||
export * from './structs/AbstractStruct.js'
|
||||
export * from './structs/GC.js'
|
||||
export * from './structs/ContentBinary.js'
|
||||
export * from './structs/ContentDeleted.js'
|
||||
export * from './structs/ContentDoc.js'
|
||||
export * from './structs/ContentEmbed.js'
|
||||
export * from './structs/ContentFormat.js'
|
||||
export * from './structs/ContentJSON.js'
|
||||
export * from './structs/ContentAny.js'
|
||||
export * from './structs/ContentString.js'
|
||||
export * from './structs/ContentType.js'
|
||||
export * from './structs/Item.js'
|
||||
export * from './structs/Skip.js'
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
export class AbstractStruct {
|
||||
@@ -25,35 +21,44 @@ export class AbstractStruct {
|
||||
* Merge this struct with the item to the right.
|
||||
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
|
||||
* Also this method does *not* remove right from StructStore!
|
||||
* @param {AbstractStruct} right
|
||||
* @param {GC|Item|Skip} _right
|
||||
* @return {boolean} whether this merged with right
|
||||
*/
|
||||
mergeWith (right) {
|
||||
mergeWith (_right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||
* @param {number} offset
|
||||
* @param {number} encodingRef
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder The encoder to write data to.
|
||||
* @param {number} _offset
|
||||
* @param {number} _encodingRef
|
||||
*/
|
||||
write (encoder, offset, encodingRef) {
|
||||
write (_encoder, _offset, _encodingRef) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {number} offset
|
||||
* @param {Transaction} _transaction
|
||||
* @param {number} _offset
|
||||
*/
|
||||
integrate (transaction, offset) {
|
||||
integrate (_transaction, _offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} diff
|
||||
* @return {import('../internals.js').GC|import('../internals.js').Item}
|
||||
* @param {number} _diff
|
||||
* @return {GC|Item|Skip}
|
||||
*/
|
||||
splice (diff) {
|
||||
splice (_diff) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} idSet
|
||||
* @param {AbstractStruct} struct
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const addStructToIdSet = (idSet, struct) => idSet.add(struct.id.client, struct.id.clock, struct.length)
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import {
|
||||
UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as env from 'lib0/environment'
|
||||
import * as object from 'lib0/object'
|
||||
|
||||
const isDevMode = env.getVariable('node_env') === 'development'
|
||||
|
||||
export class ContentAny {
|
||||
/**
|
||||
* @param {Array<any>} arr
|
||||
*/
|
||||
constructor (arr) {
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
this.arr = arr
|
||||
isDevMode && object.deepFreeze(arr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.arr.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return this.arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentAny(this.arr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentAny(this.arr.slice(offset))
|
||||
this.arr = this.arr.slice(0, offset)
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentAny} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.arr = this.arr.concat(right.arr)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {Transaction} _tr
|
||||
*/
|
||||
gc (_tr) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
* @param {number} offsetEnd
|
||||
*/
|
||||
write (encoder, offset, offsetEnd) {
|
||||
const end = this.arr.length - offsetEnd
|
||||
encoder.writeLen(end - offset)
|
||||
for (let i = offset; i < end; i++) {
|
||||
const c = this.arr[i]
|
||||
encoder.writeAny(c)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
export const readContentAny = decoder => {
|
||||
const len = decoder.readLen()
|
||||
const cs = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
cs.push(decoder.readAny())
|
||||
}
|
||||
return new ContentAny(cs)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
export class ContentBinary {
|
||||
/**
|
||||
* @param {Uint8Array} content
|
||||
*/
|
||||
constructor (content) {
|
||||
this.content = content
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.content]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentBinary(this.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentBinary} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {Transaction} _tr
|
||||
*/
|
||||
gc (_tr) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} _offset
|
||||
* @param {number} _offsetEnd
|
||||
*/
|
||||
write (encoder, _offset, _offsetEnd) {
|
||||
encoder.writeBuf(this.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
export const readContentBinary = decoder => new ContentBinary(decoder.readBuf())
|
||||
@@ -1,101 +0,0 @@
|
||||
import {
|
||||
addToIdSet,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
export class ContentDeleted {
|
||||
/**
|
||||
* @param {number} len
|
||||
*/
|
||||
constructor (len) {
|
||||
this.len = len
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.len
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentDeleted(this.len)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentDeleted(this.len - offset)
|
||||
this.len = offset
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentDeleted} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.len += right.len
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
addToIdSet(transaction.deleteSet, item.id.client, item.id.clock, this.len)
|
||||
item.markDeleted()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} _transaction
|
||||
*/
|
||||
delete (_transaction) {}
|
||||
/**
|
||||
* @param {Transaction} _tr
|
||||
*/
|
||||
gc (_tr) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
* @param {number} offsetEnd
|
||||
*/
|
||||
write (encoder, offset, offsetEnd) {
|
||||
encoder.writeLen(this.len - offset - offsetEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen())
|
||||
@@ -1,141 +0,0 @@
|
||||
import {
|
||||
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @param {string} guid
|
||||
* @param {Object<string, any>} opts
|
||||
*/
|
||||
const createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false })
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentDoc {
|
||||
/**
|
||||
* @param {Doc} doc
|
||||
*/
|
||||
constructor (doc) {
|
||||
if (doc._item) {
|
||||
console.error('This document was already integrated as a sub-document. You should create a second instance instead with the same guid.')
|
||||
}
|
||||
/**
|
||||
* @type {Doc}
|
||||
*/
|
||||
this.doc = doc
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const opts = {}
|
||||
this.opts = opts
|
||||
if (!doc.gc) {
|
||||
opts.gc = false
|
||||
}
|
||||
if (doc.autoLoad) {
|
||||
opts.autoLoad = true
|
||||
}
|
||||
if (doc.meta !== null) {
|
||||
opts.meta = doc.meta
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.doc]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentDoc}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentDoc(createDocFromOpts(this.doc.guid, this.opts))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentDoc}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentDoc} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
// this needs to be reflected in doc.destroy as well
|
||||
this.doc._item = item
|
||||
transaction.subdocsAdded.add(this.doc)
|
||||
if (this.doc.shouldLoad) {
|
||||
transaction.subdocsLoaded.add(this.doc)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {
|
||||
if (transaction.subdocsAdded.has(this.doc)) {
|
||||
transaction.subdocsAdded.delete(this.doc)
|
||||
} else {
|
||||
transaction.subdocsRemoved.add(this.doc)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} _tr
|
||||
*/
|
||||
gc (_tr) {}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} _offset
|
||||
* @param {number} _offsetEnd
|
||||
*/
|
||||
write (encoder, _offset, _offsetEnd) {
|
||||
encoder.writeString(this.doc.guid)
|
||||
encoder.writeAny(this.opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 9
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentDoc}
|
||||
*/
|
||||
export const readContentDoc = decoder => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny()))
|
||||
@@ -1,98 +0,0 @@
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentEmbed {
|
||||
/**
|
||||
* @param {Object} embed
|
||||
*/
|
||||
constructor (embed) {
|
||||
this.embed = embed
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.embed]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentEmbed(this.embed)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentEmbed} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {Transaction} _tr
|
||||
*/
|
||||
gc (_tr) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} _offset
|
||||
* @param {number} _offsetEnd
|
||||
*/
|
||||
write (encoder, _offset, _offsetEnd) {
|
||||
encoder.writeJSON(this.embed)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
export const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON())
|
||||
@@ -1,105 +0,0 @@
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentFormat {
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Object} value
|
||||
*/
|
||||
constructor (key, value) {
|
||||
this.key = key
|
||||
this.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentFormat(this.key, this.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} _offset
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
splice (_offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentFormat} _right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (_right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} _transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (_transaction, item) {
|
||||
// @todo searchmarker are currently unsupported for rich text documents
|
||||
const p = /** @type {import('../ytype.js').YType<any>} */ (item.parent)
|
||||
p._searchMarker = null
|
||||
p._hasFormatting = true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} _transaction
|
||||
*/
|
||||
delete (_transaction) {}
|
||||
/**
|
||||
* @param {Transaction} _tr
|
||||
*/
|
||||
gc (_tr) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} _offset
|
||||
* @param {number} _offsetEnd
|
||||
*/
|
||||
write (encoder, _offset, _offsetEnd) {
|
||||
encoder.writeKey(this.key)
|
||||
encoder.writeJSON(this.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 6
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
export const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON())
|
||||
@@ -1,119 +0,0 @@
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentJSON {
|
||||
/**
|
||||
* @param {Array<any>} arr
|
||||
*/
|
||||
constructor (arr) {
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
this.arr = arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.arr.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return this.arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentJSON(this.arr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentJSON(this.arr.slice(offset))
|
||||
this.arr = this.arr.slice(0, offset)
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentJSON} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.arr = this.arr.concat(right.arr)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {Transaction} _tr
|
||||
*/
|
||||
gc (_tr) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
* @param {number} offsetEnd
|
||||
*/
|
||||
write (encoder, offset, offsetEnd) {
|
||||
const end = this.arr.length - offsetEnd
|
||||
encoder.writeLen(end - offset)
|
||||
for (let i = offset; i < end; i++) {
|
||||
const c = this.arr[i]
|
||||
encoder.writeString(c === undefined ? 'undefined' : JSON.stringify(c))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
export const readContentJSON = decoder => {
|
||||
const len = decoder.readLen()
|
||||
const cs = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = decoder.readString()
|
||||
if (c === 'undefined') {
|
||||
cs.push(undefined)
|
||||
} else {
|
||||
cs.push(JSON.parse(c))
|
||||
}
|
||||
}
|
||||
return new ContentJSON(cs)
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentString {
|
||||
/**
|
||||
* @param {string} str
|
||||
*/
|
||||
constructor (str) {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this.str = str
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.str.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return this.str.split('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentString}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentString(this.str)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentString}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentString(this.str.slice(offset))
|
||||
this.str = this.str.slice(0, offset)
|
||||
|
||||
// Prevent encoding invalid documents because of splitting of surrogate pairs: https://github.com/yjs/yjs/issues/248
|
||||
const firstCharCode = this.str.charCodeAt(offset - 1)
|
||||
if (firstCharCode >= 0xD800 && firstCharCode <= 0xDBFF) {
|
||||
// Last character of the left split is the start of a surrogate utf16/ucs2 pair.
|
||||
// We don't support splitting of surrogate pairs because this may lead to invalid documents.
|
||||
// Replace the invalid character with a unicode replacement character (<28> / U+FFFD)
|
||||
this.str = this.str.slice(0, offset - 1) + '<27>'
|
||||
// replace right as well
|
||||
right.str = '<27>' + right.str.slice(1)
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentString} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.str += right.str
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {Transaction} _tr
|
||||
*/
|
||||
gc (_tr) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
* @param {number} offsetEnd
|
||||
*/
|
||||
write (encoder, offset, offsetEnd) {
|
||||
encoder.writeString((offset === 0 && offsetEnd === 0) ? this.str : this.str.slice(offset, this.str.length - offsetEnd))
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentString}
|
||||
*/
|
||||
export const readContentString = decoder => new ContentString(decoder.readString())
|
||||
@@ -1,152 +0,0 @@
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
import { readYType } from '../ytype.js'
|
||||
|
||||
export const YArrayRefID = 0
|
||||
export const YMapRefID = 1
|
||||
export const YTextRefID = 2
|
||||
export const YXmlElementRefID = 3
|
||||
export const YXmlFragmentRefID = 4
|
||||
export const YXmlHookRefID = 5
|
||||
export const YXmlTextRefID = 6
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentType {
|
||||
/**
|
||||
* @param {import('../ytype.js').YType} type
|
||||
*/
|
||||
constructor (type) {
|
||||
/**
|
||||
* @type {import('../ytype.js').YType}
|
||||
*/
|
||||
this.type = type
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.type]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentType}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentType(this.type._copy())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} _offset
|
||||
* @return {ContentType}
|
||||
*/
|
||||
splice (_offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentType} _right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (_right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
this.type._integrate(transaction.doc, item)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {
|
||||
let item = this.type._start
|
||||
while (item !== null) {
|
||||
if (!item.deleted) {
|
||||
item.delete(transaction)
|
||||
} else if (!transaction.insertSet.hasId(item.id)) {
|
||||
// This will be gc'd later and we want to merge it if possible
|
||||
// We try to merge all deleted items after each transaction,
|
||||
// but we have no knowledge about that this needs to be merged
|
||||
// since it is not in transaction.ds. Hence we add it to transaction._mergeStructs
|
||||
transaction._mergeStructs.push(item)
|
||||
}
|
||||
item = item.right
|
||||
}
|
||||
this.type._map.forEach(item => {
|
||||
if (!item.deleted) {
|
||||
item.delete(transaction)
|
||||
} else if (!transaction.insertSet.hasId(item.id)) {
|
||||
// same as above
|
||||
transaction._mergeStructs.push(item)
|
||||
}
|
||||
})
|
||||
transaction.changed.delete(this.type)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
*/
|
||||
gc (tr) {
|
||||
let item = this.type._start
|
||||
while (item !== null) {
|
||||
item.gc(tr, true)
|
||||
item = item.right
|
||||
}
|
||||
this.type._start = null
|
||||
this.type._map.forEach(/** @param {Item | null} item */ (item) => {
|
||||
while (item !== null) {
|
||||
item.gc(tr, true)
|
||||
item = item.left
|
||||
}
|
||||
})
|
||||
this.type._map = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} _offset
|
||||
* @param {number} _offsetEnd
|
||||
*/
|
||||
write (encoder, _offset, _offsetEnd) {
|
||||
this.type._write(encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 7
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentType}
|
||||
*/
|
||||
export const readContentType = decoder => new ContentType(readYType(decoder))
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
AbstractStruct,
|
||||
addStruct,
|
||||
addStructToIdSet,
|
||||
addToIdSet,
|
||||
createID,
|
||||
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
import { AbstractStruct, addStructToIdSet } from './AbstractStruct.js'
|
||||
import { createID } from '../utils/ID.js'
|
||||
|
||||
export const structGCRefNumber = 0
|
||||
|
||||
@@ -20,7 +14,7 @@ export class GC extends AbstractStruct {
|
||||
delete () {}
|
||||
|
||||
/**
|
||||
* @param {GC} right
|
||||
* @param {GC | Skip | Item} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
@@ -40,9 +34,9 @@ export class GC extends AbstractStruct {
|
||||
this.id.clock += offset
|
||||
this.length -= offset
|
||||
}
|
||||
addToIdSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
|
||||
transaction.deleteSet.add(this.id.client, this.id.clock, this.length)
|
||||
addStructToIdSet(transaction.insertSet, this)
|
||||
addStruct(transaction.doc.store, this)
|
||||
transaction.doc.store.add(this)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,15 +49,6 @@ export class GC extends AbstractStruct {
|
||||
encoder.writeLen(this.length - offset - offsetEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} _transaction
|
||||
* @param {StructStore} _store
|
||||
* @return {null | number}
|
||||
*/
|
||||
getMissing (_transaction, _store) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* gc structs can't be spliced.
|
||||
*
|
||||
@@ -78,3 +63,13 @@ export class GC extends AbstractStruct {
|
||||
return other
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {0}
|
||||
*/
|
||||
GC.prototype.ref = structGCRefNumber
|
||||
|
||||
/**
|
||||
* @type {false}
|
||||
*/
|
||||
GC.prototype.isItem = false
|
||||
|
||||
1315
src/structs/Item.js
1315
src/structs/Item.js
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,8 @@
|
||||
import {
|
||||
AbstractStruct,
|
||||
addStruct,
|
||||
addToIdSet,
|
||||
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, // eslint-disable-line
|
||||
createID
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding'
|
||||
|
||||
import { AbstractStruct } from './AbstractStruct.js'
|
||||
import { createID } from '../utils/ID.js'
|
||||
|
||||
export const structSkipRefNumber = 10
|
||||
|
||||
/**
|
||||
@@ -41,8 +36,9 @@ export class Skip extends AbstractStruct {
|
||||
this.id.clock += offset
|
||||
this.length -= offset
|
||||
}
|
||||
addToIdSet(transaction.doc.store.skips, this.id.client, this.id.clock, this.length)
|
||||
addStruct(transaction.doc.store, this)
|
||||
const store = transaction.doc.store
|
||||
store.skips.add(this.id.client, this.id.clock, this.length)
|
||||
store.add(this)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,15 +51,6 @@ export class Skip extends AbstractStruct {
|
||||
encoding.writeVarUint(encoder.restEncoder, this.length - offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} _transaction
|
||||
* @param {StructStore} _store
|
||||
* @return {null | number}
|
||||
*/
|
||||
getMissing (_transaction, _store) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} diff
|
||||
*/
|
||||
@@ -73,3 +60,13 @@ export class Skip extends AbstractStruct {
|
||||
return other
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {10}
|
||||
*/
|
||||
Skip.prototype.ref = structSkipRefNumber
|
||||
|
||||
/**
|
||||
* @type {false}
|
||||
*/
|
||||
Skip.prototype.isItem = false
|
||||
|
||||
@@ -1,149 +1,19 @@
|
||||
import {
|
||||
getItem,
|
||||
diffIdSet,
|
||||
createInsertSetFromStructStore,
|
||||
createDeleteSetFromStructStore,
|
||||
createIdMapFromIdSet,
|
||||
ContentDeleted,
|
||||
insertIntoIdMap,
|
||||
insertIntoIdSet,
|
||||
diffIdMap,
|
||||
createIdMap,
|
||||
mergeIdMaps,
|
||||
createID,
|
||||
mergeIdSets,
|
||||
applyUpdate,
|
||||
writeIdSet,
|
||||
UpdateEncoderV1,
|
||||
transact,
|
||||
createMaybeAttrRange,
|
||||
createIdSet,
|
||||
writeStructsFromIdSet,
|
||||
UndoManager,
|
||||
StackItem,
|
||||
getItemCleanStart,
|
||||
intersectSets,
|
||||
intersectMaps,
|
||||
ContentFormat,
|
||||
createContentAttribute,
|
||||
StructStore, Transaction, ID, IdSet, Item, Snapshot, Doc, AbstractContent, IdMap, // eslint-disable-line
|
||||
encodeStateAsUpdate
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as s from 'lib0/schema'
|
||||
|
||||
export const attributionJsonSchema = s.$object({
|
||||
insert: s.$array(s.$string).optional,
|
||||
insertedAt: s.$number.optional,
|
||||
delete: s.$array(s.$string).optional,
|
||||
deletedAt: s.$number.optional,
|
||||
format: s.$record(s.$string, s.$array(s.$string)).optional,
|
||||
formatAt: s.$number.optional
|
||||
})
|
||||
import { getItemCleanStart } from './transaction-helpers.js'
|
||||
import { diffIdSet, createInsertSetFromStructStore, createDeleteSetFromStructStore, insertIntoIdSet, mergeIdSets, intersectSets, createIdSet, writeIdSet, createIdMapFromIdSet, insertIntoIdMap, diffIdMap, createIdMap, mergeIdMaps, intersectMaps, createMaybeAttrRange, createContentAttribute } from './ids.js'
|
||||
import { ContentDeleted, ContentFormat } from '../structs/Item.js'
|
||||
import { createID } from './ID.js'
|
||||
import { writeStructsFromIdSet } from './encoding-helpers.js'
|
||||
import { applyUpdate, encodeStateAsUpdate } from './encoding.js'
|
||||
import { UpdateEncoderV1 } from './UpdateEncoder.js'
|
||||
import { transact } from './Transaction.js'
|
||||
import { UndoManager, StackItem } from './UndoManager.js'
|
||||
|
||||
/**
|
||||
* @todo rename this to `insertBy`, `insertAt`, ..
|
||||
*
|
||||
* @typedef {s.Unwrap<typeof attributionJsonSchema>} Attribution
|
||||
*/
|
||||
import { $attributionManager, AttributedContent } from './attribution-manager-helpers.js'
|
||||
|
||||
/**
|
||||
* @todo SHOULD NOT RETURN AN OBJECT!
|
||||
* @param {Array<import('./IdMap.js').ContentAttribute<any>>?} attrs
|
||||
* @param {boolean} deleted - whether the attributed item is deleted
|
||||
* @return {Attribution?}
|
||||
*/
|
||||
export const createAttributionFromAttributionItems = (attrs, deleted) => {
|
||||
if (attrs == null) {
|
||||
return null
|
||||
}
|
||||
/**
|
||||
* @type {Attribution}
|
||||
*/
|
||||
const attribution = {}
|
||||
if (deleted) {
|
||||
attribution.delete = []
|
||||
} else {
|
||||
attribution.insert = []
|
||||
}
|
||||
attrs.forEach(attr => {
|
||||
switch (attr.name) {
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'insert':
|
||||
case 'delete': {
|
||||
// needs to be non-ambiguous: don't add existing attr if it doesn't match the actual status
|
||||
attribution[attr.name]?.push(attr.val)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (attr.name[0] !== '_') {
|
||||
/** @type {any} */ (attribution)[attr.name] = attr.val
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return attribution
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
export class AttributedContent {
|
||||
/**
|
||||
* @param {AbstractContent} content
|
||||
* @param {number} clock
|
||||
* @param {boolean} deleted
|
||||
* @param {Array<import('./IdMap.js').ContentAttribute<T>> | null} attrs
|
||||
* @param {0|1|2} renderBehavior
|
||||
*/
|
||||
constructor (content, clock, deleted, attrs, renderBehavior) {
|
||||
this.content = content
|
||||
this.clock = clock
|
||||
this.deleted = deleted
|
||||
this.attrs = attrs
|
||||
this.render = renderBehavior === 0 ? false : (renderBehavior === 1 ? (!deleted || attrs != null) : true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for associating Attributions to content / changes
|
||||
*
|
||||
* Should fire an event when the attributions changed _after_ the original change happens. This
|
||||
* Event will be used to update the attribution on the current content.
|
||||
*
|
||||
* @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
|
||||
*/
|
||||
export class AbstractAttributionManager extends ObservableV2 {
|
||||
/**
|
||||
* @param {Array<AttributedContent<any>>} _contents - where to write the result
|
||||
* @param {number} _client
|
||||
* @param {number} _clock
|
||||
* @param {boolean} _deleted
|
||||
* @param {AbstractContent} _content
|
||||
* @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).
|
||||
*/
|
||||
readContent (_contents, _client, _clock, _deleted, _content, _shouldRender) {
|
||||
error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of the attributed content. This is used by iterators that walk through the
|
||||
* content.
|
||||
*
|
||||
* If the content is not countable, it should return 0.
|
||||
*
|
||||
* @param {Item} _item
|
||||
* @return {number}
|
||||
*/
|
||||
contentLength (_item) {
|
||||
error.methodUnimplemented()
|
||||
}
|
||||
}
|
||||
|
||||
export const $attributionManager = AbstractAttributionManager.prototype.$type = s.$type('y:am', AbstractAttributionManager)
|
||||
export { noAttributionsManager, NoAttributionsManager, AbstractAttributionManager, $attributionManager } from './attribution-manager-helpers.js'
|
||||
|
||||
/**
|
||||
* @implements AbstractAttributionManager
|
||||
@@ -200,41 +70,6 @@ export class TwosetAttributionManager extends ObservableV2 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for associating Attributions to content / changes
|
||||
*
|
||||
* @implements AbstractAttributionManager
|
||||
*
|
||||
* @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
|
||||
*/
|
||||
export class NoAttributionsManager extends ObservableV2 {
|
||||
get $type () { return $attributionManager }
|
||||
|
||||
/**
|
||||
* @param {Array<AttributedContent<any>>} contents - where to write the result
|
||||
* @param {number} _client
|
||||
* @param {number} clock
|
||||
* @param {boolean} deleted
|
||||
* @param {AbstractContent} content
|
||||
* @param {0|1|2} shouldRender - whether this should render or just result in a `retain` operation
|
||||
*/
|
||||
readContent (contents, _client, clock, deleted, content, shouldRender) {
|
||||
if (!deleted || shouldRender) {
|
||||
contents.push(new AttributedContent(content, clock, deleted, null, shouldRender))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Item} item
|
||||
* @return {number}
|
||||
*/
|
||||
contentLength (item) {
|
||||
return (item.deleted || !item.content.isCountable()) ? 0 : item.length
|
||||
}
|
||||
}
|
||||
|
||||
export const noAttributionsManager = new NoAttributionsManager()
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
* @param {number} client
|
||||
@@ -243,7 +78,7 @@ export const noAttributionsManager = new NoAttributionsManager()
|
||||
*/
|
||||
const getItemContent = (store, client, clock, len) => {
|
||||
// Retrieved item is never more fragmented than the newer item.
|
||||
const prevItem = getItem(store, createID(client, clock))
|
||||
const prevItem = store.getItem(createID(client, clock))
|
||||
const diffStart = clock - prevItem.id.clock
|
||||
let content = prevItem.length > 1 ? prevItem.content.copy() : prevItem.content
|
||||
// trim itemContent to the correct size.
|
||||
@@ -275,8 +110,8 @@ const collectSuggestedChanges = (tr, am, start, end, collectAll) => {
|
||||
/**
|
||||
* @type {Item?}
|
||||
*/
|
||||
let item = getItem(store, start)
|
||||
const endItem = start === end ? item : (end == null ? null : getItem(store, end))
|
||||
let item = store.getItem(start)
|
||||
const endItem = start === end ? item : (end == null ? null : store.getItem(end))
|
||||
|
||||
// walk to the left and find first un-attributed change that is rendered
|
||||
while (item.left != null) {
|
||||
@@ -533,7 +368,7 @@ export class DiffAttributionManager extends ObservableV2 {
|
||||
continue
|
||||
}
|
||||
// Retrieved item is never more fragmented than the newer item.
|
||||
const prevItem = getItem(this._prevDocStore, createID(client, s.clock))
|
||||
const prevItem = this._prevDocStore.getItem(createID(client, s.clock))
|
||||
const diffStart = s.clock - prevItem.id.clock
|
||||
content = prevItem.length > 1 ? prevItem.content.copy() : prevItem.content
|
||||
// trim itemContent to the correct size.
|
||||
@@ -594,7 +429,7 @@ export class SnapshotAttributionManager extends ObservableV2 {
|
||||
* @param {Snapshot} prevSnapshot
|
||||
* @param {Snapshot} nextSnapshot
|
||||
* @param {Object} [options] - options for the attribution manager
|
||||
* @param {Array<import('./IdMap.js').ContentAttribute<any>>} [options.attrs] - the attributes to apply to the diff
|
||||
* @param {Array<ContentAttribute>} [options.attrs] - the attributes to apply to the diff
|
||||
*/
|
||||
constructor (prevSnapshot, nextSnapshot) {
|
||||
super()
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
import {
|
||||
createID,
|
||||
readItemContent,
|
||||
findIndexCleanStart,
|
||||
Skip,
|
||||
createIdSet,
|
||||
sliceStruct,
|
||||
IdRange,
|
||||
GC, Item, ID,
|
||||
writeStructs
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as binary from 'lib0/binary'
|
||||
import * as map from 'lib0/map'
|
||||
@@ -18,6 +6,16 @@ import * as math from 'lib0/math'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as number from 'lib0/number'
|
||||
|
||||
import { createID, ID } from './ID.js'
|
||||
import { Item } from '../structs/Item.js'
|
||||
import { readItemContent } from '../ytype.js'
|
||||
import { findIndexCleanStart } from './transaction-helpers.js'
|
||||
import { Skip } from '../structs/Skip.js'
|
||||
import { createIdSet, IdRange } from './ids.js'
|
||||
import { sliceStruct } from './updates.js'
|
||||
import { GC } from '../structs/GC.js'
|
||||
import { writeStructs } from './encoding-helpers.js'
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder The decoder object to read data from.
|
||||
* @return {BlockSet}
|
||||
@@ -31,7 +29,7 @@ export const readBlockSet = (decoder) => {
|
||||
for (let i = 0; i < numOfStateUpdates; i++) {
|
||||
const numberOfBlocks = decoding.readVarUint(decoder.restDecoder)
|
||||
/**
|
||||
* @type {Array<GC|Item>}
|
||||
* @type {Array<GC|Item|Skip>}
|
||||
*/
|
||||
const refs = new Array(numberOfBlocks)
|
||||
const client = decoder.readClient()
|
||||
@@ -99,12 +97,12 @@ export const writeBlockSet = (encoder, blocks) => {
|
||||
|
||||
class BlockRange {
|
||||
/**
|
||||
* @param {Array<Item|GC>} refs
|
||||
* @param {Array<Item|GC|Skip>} refs
|
||||
*/
|
||||
constructor (refs) {
|
||||
this.i = 0
|
||||
/**
|
||||
* @type {Array<Item | GC>}
|
||||
* @type {Array<Item | GC | Skip>}
|
||||
*/
|
||||
this.refs = refs
|
||||
}
|
||||
@@ -203,12 +201,12 @@ export class BlockSet {
|
||||
} else {
|
||||
// requires more computation because we need to filter duplicates
|
||||
/**
|
||||
* @type {Array<GC|Item>}
|
||||
* @type {Array<GC|Item|Skip>}
|
||||
*/
|
||||
const result = []
|
||||
let nextExpectedClock = leftRanges[0].id.clock
|
||||
/**
|
||||
* @param {Item|GC} block
|
||||
* @param {Item|GC|Skip} block
|
||||
*/
|
||||
const addToResult = block => {
|
||||
result.push(block)
|
||||
@@ -217,11 +215,11 @@ export class BlockSet {
|
||||
let li = 0
|
||||
let ri = 0
|
||||
/**
|
||||
* @type {Item|GC|undefined}
|
||||
* @type {Item|GC|Skip|undefined}
|
||||
*/
|
||||
let lblock = leftRanges[li]
|
||||
/**
|
||||
* @type {Item|GC|undefined}
|
||||
* @type {Item|GC|Skip|undefined}
|
||||
*/
|
||||
let rblock = rightRanges[ri]
|
||||
const applyLeft = () => {
|
||||
|
||||
@@ -2,22 +2,16 @@
|
||||
* @module Y
|
||||
*/
|
||||
|
||||
import {
|
||||
StructStore,
|
||||
transact,
|
||||
applyUpdate,
|
||||
ContentDoc, Item, Transaction, // eslint-disable-line
|
||||
encodeStateAsUpdate
|
||||
} from '../internals.js'
|
||||
|
||||
import { YType } from '../ytype.js'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
import * as random from 'lib0/random'
|
||||
import * as map from 'lib0/map'
|
||||
import * as array from 'lib0/array'
|
||||
import * as promise from 'lib0/promise'
|
||||
|
||||
export const generateNewClientId = random.uint32
|
||||
import { StructStore } from './StructStore.js'
|
||||
import { transact, generateNewClientId } from './Transaction.js'
|
||||
import { YType } from '../ytype.js'
|
||||
import { $ydoc } from './schemas.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object} DocOpts
|
||||
@@ -179,12 +173,11 @@ export class Doc extends ObservableV2 {
|
||||
* @template T
|
||||
* @param {function(Transaction):T} f The function that should be executed as a transaction
|
||||
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
|
||||
* @param {boolean} [local]
|
||||
* @return T
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
transact (f, origin = null) {
|
||||
return transact(this, f, origin)
|
||||
transact (f, origin = null, local = true) {
|
||||
return transact(this, f, origin, local)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,29 +231,22 @@ export class Doc extends ObservableV2 {
|
||||
if (item !== null) {
|
||||
this._item = null
|
||||
const content = /** @type {ContentDoc} */ (item.content)
|
||||
content.doc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false })
|
||||
content.doc._item = item
|
||||
/**
|
||||
* new content doc which replaces the new one
|
||||
*/
|
||||
const contentDoc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false })
|
||||
content.doc = contentDoc
|
||||
contentDoc._item = item
|
||||
transact(/** @type {any} */ (item).parent.doc, transaction => {
|
||||
const doc = content.doc
|
||||
if (!item.deleted) {
|
||||
transaction.subdocsAdded.add(doc)
|
||||
transaction.subdocsAdded.add(contentDoc)
|
||||
}
|
||||
transaction.subdocsRemoved.add(this)
|
||||
}, null, true)
|
||||
}
|
||||
// @ts-ignore
|
||||
this.emit('destroyed', [true]) // DEPRECATED!
|
||||
this.emit('destroy', [this])
|
||||
super.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Doc} ydoc
|
||||
* @param {DocOpts} [opts]
|
||||
*/
|
||||
export const cloneDoc = (ydoc, opts) => {
|
||||
const clone = new Doc(opts)
|
||||
applyUpdate(clone, encodeStateAsUpdate(ydoc))
|
||||
return clone
|
||||
}
|
||||
Doc.prototype.$type = $ydoc
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { YType } from '../internals.js' // eslint-disable-line
|
||||
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
@@ -1,673 +0,0 @@
|
||||
import {
|
||||
_diffSet,
|
||||
findIndexInIdRanges,
|
||||
findRangeStartInIdRanges,
|
||||
_deleteRangeFromIdSet,
|
||||
IdSetDecoderV1, IdSetDecoderV2, IdSetEncoderV1, IdSetEncoderV2, IdSet, ID, // eslint-disable-line
|
||||
_insertIntoIdSet,
|
||||
_intersectSets,
|
||||
createIdSet,
|
||||
IdRanges
|
||||
} from '../internals.js'
|
||||
|
||||
import * as array from 'lib0/array'
|
||||
import * as map from 'lib0/map'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as buf from 'lib0/buffer'
|
||||
import * as rabin from 'lib0/hash/rabin'
|
||||
|
||||
/**
|
||||
* @todo rename this to `Attribute`
|
||||
* @template V
|
||||
*/
|
||||
export class ContentAttribute {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {V} val
|
||||
*/
|
||||
constructor (name, val) {
|
||||
this.name = name
|
||||
this.val = val
|
||||
}
|
||||
|
||||
hash () {
|
||||
const encoder = encoding.createEncoder()
|
||||
encoding.writeVarString(encoder, this.name)
|
||||
encoding.writeAny(encoder, /** @type {any} */ (this.val))
|
||||
return buf.toBase64(rabin.fingerprint(rabin.StandardIrreducible128, encoding.toUint8Array(encoder)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template V
|
||||
* @param {string} name
|
||||
* @param {V} val
|
||||
* @return {ContentAttribute<V>}
|
||||
*/
|
||||
export const createContentAttribute = (name, val) => new ContentAttribute(name, val)
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} attrs
|
||||
* @param {T} attr
|
||||
*
|
||||
*/
|
||||
const idmapAttrsHas = (attrs, attr) => attrs.find(a => a === attr)
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} a
|
||||
* @param {Array<T>} b
|
||||
*/
|
||||
export const idmapAttrsEqual = (a, b) => a.length === b.length && a.every(v => idmapAttrsHas(b, v))
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} a
|
||||
* @param {Array<T>} b
|
||||
*/
|
||||
const idmapAttrRangeJoin = (a, b) => a.concat(b.filter(attr => !idmapAttrsHas(a, attr)))
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
*/
|
||||
export class AttrRange {
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @param {Array<ContentAttribute<Attrs>>} attrs
|
||||
*/
|
||||
constructor (clock, len, attrs) {
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
this.clock = clock
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
this.len = len
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
this.attrs = attrs
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
*/
|
||||
copyWith (clock, len) {
|
||||
return new AttrRange(clock, len, this.attrs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
* @typedef {{ clock: number, len: number, attrs: Array<ContentAttribute<Attrs>>? }} MaybeAttrRange
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
*
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @param {Array<ContentAttribute<Attrs>>?} attrs
|
||||
* @return {MaybeAttrRange<Attrs>}
|
||||
*/
|
||||
export const createMaybeAttrRange = (clock, len, attrs) => new AttrRange(clock, len, /** @type {any} */ (attrs))
|
||||
|
||||
/**
|
||||
* Whenever this is instantiated, it must receive a fresh array of ops, not something copied.
|
||||
*
|
||||
* @template Attrs
|
||||
*/
|
||||
export class AttrRanges {
|
||||
/**
|
||||
* @param {Array<AttrRange<Attrs>>} ids
|
||||
*/
|
||||
constructor (ids) {
|
||||
this.sorted = false
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this._ids = ids
|
||||
}
|
||||
|
||||
copy () {
|
||||
return new AttrRanges(this._ids.slice())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} length
|
||||
* @param {Array<ContentAttribute<Attrs>>} attrs
|
||||
*/
|
||||
add (clock, length, attrs) {
|
||||
if (length === 0) return
|
||||
this.sorted = false
|
||||
this._ids.push(new AttrRange(clock, length, attrs))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of id ranges, sorted and merged.
|
||||
*/
|
||||
getIds () {
|
||||
const ids = this._ids
|
||||
if (!this.sorted) {
|
||||
this.sorted = true
|
||||
ids.sort((a, b) => a.clock - b.clock)
|
||||
/**
|
||||
* algorithm thoughts:
|
||||
* - sort (by clock AND by length), bigger length is to the right (or not, we can't make
|
||||
* assumptions abouth length after long length has been split)
|
||||
* -- maybe better: sort by clock+length. Then split items from right to left. This way, items are always
|
||||
* in the right order. But I also need to swap if left items is smaller after split
|
||||
* --- thought: there is no way to go around swapping. Unless, for each item from left to
|
||||
* right, when I have to split because one of the look-ahead items is overlapping, i split
|
||||
* it and merge the attributes into the following ones (that I also need to split). Best is
|
||||
* probably left to right with lookahead.
|
||||
* - left to right, split overlapping items so that we can make the assumption that either an
|
||||
* item is overlapping with the next 1-on-1 or it is not overlapping at all (when splitting,
|
||||
* we can already incorporate the attributes)
|
||||
* -- better: for each item, go left to right and add own attributes to overlapping items.
|
||||
* Split them if necessary. After split, i must insert the retainer at a valid position.
|
||||
* - merge items if neighbor has same attributes
|
||||
*/
|
||||
for (let i = 0; i < ids.length - 1;) {
|
||||
const range = ids[i]
|
||||
const nextRange = ids[i + 1]
|
||||
// find out how to split range. it must match with next range.
|
||||
// 1) we have space. Split if necessary.
|
||||
// 2) concat attributes in range to the next range. Split range and splice the remainder at
|
||||
// the correct position.
|
||||
if (range.clock < nextRange.clock) { // might need to split range
|
||||
if (range.clock + range.len > nextRange.clock) {
|
||||
// is overlapping
|
||||
const diff = nextRange.clock - range.clock
|
||||
ids[i] = new AttrRange(range.clock, diff, range.attrs)
|
||||
ids.splice(i + 1, 0, new AttrRange(nextRange.clock, range.len - diff, range.attrs))
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// now we know that range.clock === nextRange.clock
|
||||
// merge range with nextRange
|
||||
const largerRange = range.len > nextRange.len ? range : nextRange
|
||||
const smallerLen = range.len < nextRange.len ? range.len : nextRange.len
|
||||
ids[i] = new AttrRange(range.clock, smallerLen, idmapAttrRangeJoin(range.attrs, nextRange.attrs))
|
||||
if (range.len === nextRange.len) {
|
||||
ids.splice(i + 1, 1)
|
||||
} else {
|
||||
ids[i + 1] = new AttrRange(range.clock + smallerLen, largerRange.len - smallerLen, largerRange.attrs)
|
||||
array.bubblesortItem(ids, i + 1, (a, b) => a.clock - b.clock)
|
||||
}
|
||||
if (smallerLen === 0) i++
|
||||
}
|
||||
while (ids.length > 0 && ids[0].len === 0) {
|
||||
ids.splice(0, 1)
|
||||
}
|
||||
// merge items without filtering or splicing the array.
|
||||
// i is the current pointer
|
||||
// j refers to the current insert position for the pointed item
|
||||
// try to merge dels[i] into dels[j-1] or set dels[j]=dels[i]
|
||||
let i, j
|
||||
for (i = 1, j = 1; i < ids.length; i++) {
|
||||
const left = ids[j - 1]
|
||||
const right = ids[i]
|
||||
if (left.clock + left.len === right.clock && idmapAttrsEqual(left.attrs, right.attrs)) {
|
||||
ids[j - 1] = new AttrRange(left.clock, left.len + right.len, left.attrs)
|
||||
} else if (right.len !== 0) {
|
||||
if (j < i) {
|
||||
ids[j] = right
|
||||
}
|
||||
j++
|
||||
}
|
||||
}
|
||||
ids.length = ids.length === 0 ? 0 : (ids[j - 1].len === 0 ? j - 1 : j)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge multiple idmaps. Ensures that there are no redundant attribution definitions (two
|
||||
* Attributions that describe the same thing).
|
||||
*
|
||||
* @template T
|
||||
* @param {Array<IdMap<T>>} ams
|
||||
* @return {IdMap<T>} A fresh IdSet
|
||||
*/
|
||||
export const mergeIdMaps = ams => {
|
||||
/**
|
||||
* Maps attribution to the attribution of the merged idmap.
|
||||
*
|
||||
* @type {Map<ContentAttribute<any>,ContentAttribute<any>>}
|
||||
*/
|
||||
const attrMapper = new Map()
|
||||
const merged = createIdMap()
|
||||
for (let amsI = 0; amsI < ams.length; amsI++) {
|
||||
ams[amsI].clients.forEach((rangesLeft, client) => {
|
||||
if (!merged.clients.has(client)) {
|
||||
// Write all missing keys from current set and all following.
|
||||
// If merged already contains `client` current ds has already been added.
|
||||
let ids = rangesLeft.getIds().slice()
|
||||
for (let i = amsI + 1; i < ams.length; i++) {
|
||||
const nextIds = ams[i].clients.get(client)
|
||||
if (nextIds) {
|
||||
array.appendTo(ids, nextIds.getIds())
|
||||
}
|
||||
}
|
||||
ids = ids.map(id => new AttrRange(id.clock, id.len, id.attrs.map(attr =>
|
||||
map.setIfUndefined(attrMapper, attr, () =>
|
||||
_ensureAttrs(merged, [attr])[0]
|
||||
)
|
||||
)))
|
||||
merged.clients.set(client, new AttrRanges(ids))
|
||||
}
|
||||
})
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} idset
|
||||
* @param {Array<ContentAttribute<any>>} attrs
|
||||
*/
|
||||
export const createIdMapFromIdSet = (idset, attrs) => {
|
||||
const idmap = createIdMap()
|
||||
// map attrs to idmap
|
||||
attrs = _ensureAttrs(idmap, attrs)
|
||||
// filter out duplicates
|
||||
/**
|
||||
* @type {Array<ContentAttribute<any>>}
|
||||
*/
|
||||
const checkedAttrs = []
|
||||
attrs.forEach(attr => {
|
||||
if (!idmapAttrsHas(checkedAttrs, attr)) {
|
||||
checkedAttrs.push(attr)
|
||||
}
|
||||
})
|
||||
idset.clients.forEach((ranges, client) => {
|
||||
const attrRanges = new AttrRanges(ranges.getIds().map(range => new AttrRange(range.clock, range.len, checkedAttrs)))
|
||||
attrRanges.sorted = true // is sorted because idset is sorted
|
||||
idmap.clients.set(client, attrRanges)
|
||||
})
|
||||
return idmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IdSet from an IdMap by stripping the attributes.
|
||||
*
|
||||
* @param {IdMap<any>} idmap
|
||||
* @return {IdSet}
|
||||
*/
|
||||
export const createIdSetFromIdMap = idmap => {
|
||||
const idset = createIdSet()
|
||||
idmap.clients.forEach((ranges, client) => {
|
||||
const idRanges = new IdRanges([])
|
||||
ranges.getIds().forEach(range => idRanges.add(range.clock, range.len))
|
||||
idset.clients.set(client, idRanges)
|
||||
})
|
||||
return idset
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
*/
|
||||
export class IdMap {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {Map<number,AttrRanges<Attrs>>}
|
||||
*/
|
||||
this.clients = new Map()
|
||||
/**
|
||||
* @type {Map<string, ContentAttribute<Attrs>>}
|
||||
*/
|
||||
this.attrsH = new Map()
|
||||
/**
|
||||
* @type {Set<ContentAttribute<Attrs>>}
|
||||
*/
|
||||
this.attrs = new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(attrRange:AttrRange<Attrs>, client:number) => void} f
|
||||
*/
|
||||
forEach (f) {
|
||||
this.clients.forEach((ranges, client) => {
|
||||
ranges.getIds().forEach((range) => {
|
||||
f(range, client)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
isEmpty () {
|
||||
return this.clients.size === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @return {boolean}
|
||||
*/
|
||||
hasId (id) {
|
||||
return this.has(id.client, id.clock)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @return {boolean}
|
||||
*/
|
||||
has (client, clock) {
|
||||
const dr = this.clients.get(client)
|
||||
if (dr) {
|
||||
return findIndexInIdRanges(dr.getIds(), clock) !== null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return attributions for a slice of ids.
|
||||
*
|
||||
* @param {ID} id
|
||||
* @param {number} len
|
||||
* @return {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
sliceId (id, len) {
|
||||
return this.slice(id.client, id.clock, len)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return attributions for a slice of ids.
|
||||
*
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @return {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
slice (client, clock, len) {
|
||||
const dr = this.clients.get(client)
|
||||
/**
|
||||
* @type {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
const res = []
|
||||
if (dr) {
|
||||
/**
|
||||
* @type {Array<AttrRange<Attrs>>}
|
||||
*/
|
||||
const ranges = dr.getIds()
|
||||
let index = findRangeStartInIdRanges(ranges, clock)
|
||||
if (index !== null) {
|
||||
let prev = null
|
||||
while (index < ranges.length) {
|
||||
let r = ranges[index]
|
||||
if (r.clock < clock) {
|
||||
r = new AttrRange(clock, r.len - (clock - r.clock), r.attrs)
|
||||
}
|
||||
if (r.clock + r.len > clock + len) {
|
||||
r = new AttrRange(r.clock, clock + len - r.clock, r.attrs)
|
||||
}
|
||||
if (r.len <= 0) break
|
||||
const prevEnd = prev != null ? prev.clock + prev.len : clock
|
||||
if (prevEnd < r.clock) {
|
||||
res.push(createMaybeAttrRange(prevEnd, r.clock - prevEnd, null))
|
||||
}
|
||||
prev = r
|
||||
res.push(r)
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
if (res.length > 0) {
|
||||
const last = res[res.length - 1]
|
||||
const end = last.clock + last.len
|
||||
if (end < clock + len) {
|
||||
res.push(createMaybeAttrRange(end, clock + len - end, null))
|
||||
}
|
||||
} else {
|
||||
res.push(createMaybeAttrRange(clock, len, null))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @param {Array<ContentAttribute<Attrs>>} attrs
|
||||
*/
|
||||
add (client, clock, len, attrs) {
|
||||
if (len === 0) return
|
||||
attrs = _ensureAttrs(this, attrs)
|
||||
const ranges = this.clients.get(client)
|
||||
if (ranges == null) {
|
||||
this.clients.set(client, new AttrRanges([new AttrRange(clock, len, attrs)]))
|
||||
} else {
|
||||
ranges.add(clock, len, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
*/
|
||||
delete (client, clock, len) {
|
||||
_deleteRangeFromIdSet(this, client, clock, len)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently encodes IdMap to a binary form. Ensures that information is de-duplicated when
|
||||
* written. Attribute.names are referenced by id. Attributes themselfs are also referenced by id.
|
||||
*
|
||||
* @template Attr
|
||||
* @param {IdSetEncoderV1 | IdSetEncoderV2} encoder
|
||||
* @param {IdMap<Attr>} idmap
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeIdMap = (encoder, idmap) => {
|
||||
encoding.writeVarUint(encoder.restEncoder, idmap.clients.size)
|
||||
let lastWrittenClientId = 0
|
||||
/**
|
||||
* @type {Map<ContentAttribute<Attr>, number>}
|
||||
*/
|
||||
const visitedAttributions = map.create()
|
||||
/**
|
||||
* @type {Map<string, number>}
|
||||
*/
|
||||
const visitedAttrNames = map.create()
|
||||
// Ensure that the ids are written in a deterministic order (smaller clientids first)
|
||||
array.from(idmap.clients.entries())
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.forEach(([client, _idRanges]) => {
|
||||
const attrRanges = _idRanges.getIds()
|
||||
encoder.resetIdSetCurVal()
|
||||
const diff = client - lastWrittenClientId
|
||||
encoding.writeVarUint(encoder.restEncoder, diff)
|
||||
lastWrittenClientId = client
|
||||
const len = attrRanges.length
|
||||
encoding.writeVarUint(encoder.restEncoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const item = attrRanges[i]
|
||||
const attrs = item.attrs
|
||||
const attrLen = attrs.length
|
||||
encoder.writeIdSetClock(item.clock)
|
||||
encoder.writeIdSetLen(item.len)
|
||||
encoding.writeVarUint(encoder.restEncoder, attrLen)
|
||||
for (let j = 0; j < attrLen; j++) {
|
||||
const attr = attrs[j]
|
||||
const attrId = visitedAttributions.get(attr)
|
||||
if (attrId != null) {
|
||||
encoding.writeVarUint(encoder.restEncoder, attrId)
|
||||
} else {
|
||||
const newAttrId = visitedAttributions.size
|
||||
visitedAttributions.set(attr, newAttrId)
|
||||
encoding.writeVarUint(encoder.restEncoder, newAttrId)
|
||||
const attrNameId = visitedAttrNames.get(attr.name)
|
||||
// write attr.name
|
||||
if (attrNameId != null) {
|
||||
encoding.writeVarUint(encoder.restEncoder, attrNameId)
|
||||
} else {
|
||||
const newAttrNameId = visitedAttrNames.size
|
||||
encoding.writeVarUint(encoder.restEncoder, newAttrNameId)
|
||||
encoding.writeVarString(encoder.restEncoder, attr.name)
|
||||
visitedAttrNames.set(attr.name, newAttrNameId)
|
||||
}
|
||||
encoding.writeAny(encoder.restEncoder, /** @type {any} */ (attr.val))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdMap<any>} idmap
|
||||
*/
|
||||
export const encodeIdMap = idmap => {
|
||||
const encoder = new IdSetEncoderV2()
|
||||
writeIdMap(encoder, idmap)
|
||||
return encoder.toUint8Array()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSetDecoderV1 | IdSetDecoderV2} decoder
|
||||
* @return {IdMap<any>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readIdMap = decoder => {
|
||||
const idmap = new IdMap()
|
||||
const numClients = decoding.readVarUint(decoder.restDecoder)
|
||||
/**
|
||||
* @type {Array<ContentAttribute<any>>}
|
||||
*/
|
||||
const visitedAttributions = []
|
||||
/**
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
const visitedAttrNames = []
|
||||
let lastClientId = 0
|
||||
for (let i = 0; i < numClients; i++) {
|
||||
decoder.resetDsCurVal()
|
||||
const client = lastClientId + decoding.readVarUint(decoder.restDecoder)
|
||||
lastClientId = client
|
||||
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
|
||||
/**
|
||||
* @type {Array<AttrRange<any>>}
|
||||
*/
|
||||
const attrRanges = []
|
||||
for (let i = 0; i < numberOfDeletes; i++) {
|
||||
const rangeClock = decoder.readDsClock()
|
||||
const rangeLen = decoder.readDsLen()
|
||||
/**
|
||||
* @type {Array<ContentAttribute<any>>}
|
||||
*/
|
||||
const attrs = []
|
||||
const attrsLen = decoding.readVarUint(decoder.restDecoder)
|
||||
for (let j = 0; j < attrsLen; j++) {
|
||||
const attrId = decoding.readVarUint(decoder.restDecoder)
|
||||
if (attrId >= visitedAttributions.length) {
|
||||
// attrId not known yet
|
||||
const attrNameId = decoding.readVarUint(decoder.restDecoder)
|
||||
if (attrNameId >= visitedAttrNames.length) {
|
||||
visitedAttrNames.push(decoding.readVarString(decoder.restDecoder))
|
||||
}
|
||||
visitedAttributions.push(new ContentAttribute(visitedAttrNames[attrNameId], decoding.readAny(decoder.restDecoder)))
|
||||
}
|
||||
attrs.push(visitedAttributions[attrId])
|
||||
}
|
||||
attrRanges.push(new AttrRange(rangeClock, rangeLen, attrs))
|
||||
}
|
||||
idmap.clients.set(client, new AttrRanges(attrRanges))
|
||||
}
|
||||
visitedAttributions.forEach(attr => {
|
||||
idmap.attrs.add(attr)
|
||||
idmap.attrsH.set(attr.hash(), attr)
|
||||
})
|
||||
return idmap
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} data
|
||||
* @return {IdMap<any>}
|
||||
*/
|
||||
export const decodeIdMap = data => readIdMap(new IdSetDecoderV2(decoding.createDecoder(data)))
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
* @param {IdMap<Attrs>} idmap
|
||||
* @param {Array<ContentAttribute<Attrs>>} attrs
|
||||
* @return {Array<ContentAttribute<Attrs>>}
|
||||
*/
|
||||
const _ensureAttrs = (idmap, attrs) => attrs.map(attr =>
|
||||
idmap.attrs.has(attr)
|
||||
? attr
|
||||
: map.setIfUndefined(idmap.attrsH, attr.hash(), () => {
|
||||
idmap.attrs.add(attr)
|
||||
return attr
|
||||
}))
|
||||
|
||||
export const createIdMap = () => new IdMap()
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {IdMap<T>} dest
|
||||
* @param {IdMap<T>} src
|
||||
*/
|
||||
export const insertIntoIdMap = _insertIntoIdSet
|
||||
|
||||
/**
|
||||
* Remove all ranges from `exclude` from `ds`. The result is a fresh IdMap containing all ranges from `idSet` that are not
|
||||
* in `exclude`.
|
||||
*
|
||||
* @template {IdMap<any>} ISet
|
||||
* @param {ISet} set
|
||||
* @param {IdSet | IdMap<any>} exclude
|
||||
* @return {ISet}
|
||||
*/
|
||||
export const diffIdMap = (set, exclude) => {
|
||||
const diffed = _diffSet(set, exclude)
|
||||
diffed.attrs = set.attrs
|
||||
diffed.attrsH = set.attrsH
|
||||
return diffed
|
||||
}
|
||||
|
||||
export const intersectMaps = _intersectSets
|
||||
|
||||
/**
|
||||
* Filter attributes in an IdMap based on a predicate function.
|
||||
* Returns a new IdMap containing idranges that match the predicate.
|
||||
*
|
||||
* @template Attrs
|
||||
* @param {IdMap<Attrs>} idmap
|
||||
* @param {(attr: Array<ContentAttribute<Attrs>>) => boolean} predicate
|
||||
* @return {IdMap<Attrs>}
|
||||
*/
|
||||
export const filterIdMap = (idmap, predicate) => {
|
||||
const filtered = createIdMap()
|
||||
idmap.clients.forEach((ranges, client) => {
|
||||
/**
|
||||
* @type {Array<AttrRange<Attrs>>}
|
||||
*/
|
||||
const attrRanges = []
|
||||
ranges.getIds().forEach((range) => {
|
||||
if (predicate(range.attrs)) {
|
||||
const rangeCpy = range.copyWith(range.clock, range.len)
|
||||
attrRanges.push(rangeCpy)
|
||||
rangeCpy.attrs.forEach(attr => {
|
||||
filtered.attrs.add(attr)
|
||||
filtered.attrsH.set(attr.hash(), attr)
|
||||
})
|
||||
}
|
||||
})
|
||||
if (attrRanges.length > 0) {
|
||||
filtered.clients.set(client, new AttrRanges(attrRanges))
|
||||
}
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
@@ -1,21 +1,11 @@
|
||||
import {
|
||||
writeID,
|
||||
readID,
|
||||
compareIDs,
|
||||
getState,
|
||||
findRootTypeKey,
|
||||
Item,
|
||||
createID,
|
||||
ContentType,
|
||||
followRedone,
|
||||
getItem,
|
||||
StructStore, ID, Doc, YType, noAttributionsManager, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
import { Item, followRedone, ContentType } from '../structs/Item.js'
|
||||
import { writeID, readID, compareIDs, findRootTypeKey, createID } from './ID.js'
|
||||
import { noAttributionsManager } from './attribution-manager-helpers.js'
|
||||
|
||||
/**
|
||||
* A relative position is based on the Yjs model and is not affected by document changes.
|
||||
* E.g. If you place a relative position before a certain character, it will always point to this character.
|
||||
@@ -261,7 +251,7 @@ export const decodeRelativePosition = uint8Array => readRelativePosition(decodin
|
||||
* @param {ID} id
|
||||
*/
|
||||
const getItemWithOffset = (store, id) => {
|
||||
const item = getItem(store, id)
|
||||
const item = store.getItem(id)
|
||||
const diff = id.clock - item.id.clock
|
||||
return {
|
||||
item, diff
|
||||
@@ -296,7 +286,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndo
|
||||
let type = null
|
||||
let index = 0
|
||||
if (rightID !== null) {
|
||||
if (getState(store, rightID.client) <= rightID.clock) {
|
||||
if (store.getClock(rightID.client) <= rightID.clock) {
|
||||
return null
|
||||
}
|
||||
const res = followUndoneDeletions ? followRedone(store, rightID) : getItemWithOffset(store, rightID)
|
||||
@@ -317,11 +307,11 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndo
|
||||
if (tname !== null) {
|
||||
type = doc.get(tname)
|
||||
} else if (typeID !== null) {
|
||||
if (getState(store, typeID.client) <= typeID.clock) {
|
||||
if (store.getClock(typeID.client) <= typeID.clock) {
|
||||
// type does not exist yet
|
||||
return null
|
||||
}
|
||||
const { item } = followUndoneDeletions ? followRedone(store, typeID) : { item: getItem(store, typeID) }
|
||||
const { item } = followUndoneDeletions ? followRedone(store, typeID) : { item: store.getItem(typeID) }
|
||||
if (item instanceof Item && item.content instanceof ContentType) {
|
||||
type = item.content.type
|
||||
} else {
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import {
|
||||
createDeleteSetFromStructStore,
|
||||
getStateVector,
|
||||
getItemCleanStart,
|
||||
iterateStructsByIdSet,
|
||||
writeIdSet,
|
||||
writeStateVector,
|
||||
readIdSet,
|
||||
readStateVector,
|
||||
createIdSet,
|
||||
createID,
|
||||
getState,
|
||||
findIndexSS,
|
||||
UpdateEncoderV2,
|
||||
applyUpdateV2,
|
||||
LazyStructReader,
|
||||
equalIdSets,
|
||||
UpdateDecoderV1, UpdateDecoderV2, IdSetEncoderV1, IdSetEncoderV2, IdSetDecoderV1, IdSetDecoderV2, Transaction, Doc, IdSet, Item, // eslint-disable-line
|
||||
mergeIdSets
|
||||
} from '../internals.js'
|
||||
|
||||
import * as map from 'lib0/map'
|
||||
import * as set from 'lib0/set'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
|
||||
import { createID } from './ID.js'
|
||||
import { createDeleteSetFromStructStore, createIdSet, iterateStructsByIdSet, equalIdSets, mergeIdSets, writeIdSet, readIdSet } from './ids.js'
|
||||
import { getStateVector } from './StructStore.js'
|
||||
import { findIndexSS, getItemCleanStart } from './transaction-helpers.js'
|
||||
import { writeStateVector, readStateVector, applyUpdateV2 } from './encoding.js'
|
||||
import { UpdateDecoderV1, UpdateDecoderV2, IdSetDecoderV1, IdSetDecoderV2 } from './UpdateDecoder.js'
|
||||
import { UpdateEncoderV2, IdSetEncoderV1, IdSetEncoderV2 } from './UpdateEncoder.js'
|
||||
import { LazyStructReader } from './updates.js'
|
||||
import { Doc } from './Doc.js'
|
||||
|
||||
export class Snapshot {
|
||||
/**
|
||||
* @param {IdSet} ds
|
||||
@@ -108,17 +97,6 @@ export const emptySnapshot = createSnapshot(createIdSet(), new Map())
|
||||
*/
|
||||
export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc.store), getStateVector(doc.store))
|
||||
|
||||
/**
|
||||
* @param {Item} item
|
||||
* @param {Snapshot|undefined} snapshot
|
||||
*
|
||||
* @protected
|
||||
* @function
|
||||
*/
|
||||
export const isVisible = (item, snapshot) => snapshot === undefined
|
||||
? !item.deleted
|
||||
: snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !snapshot.ds.hasId(item.id)
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Snapshot} snapshot
|
||||
@@ -129,7 +107,7 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
||||
// check if we already split for this snapshot
|
||||
if (!meta.has(snapshot)) {
|
||||
snapshot.sv.forEach((clock, client) => {
|
||||
if (clock < getState(store, client)) {
|
||||
if (clock < store.getClock(client)) {
|
||||
getItemCleanStart(transaction, createID(client, clock))
|
||||
}
|
||||
})
|
||||
@@ -173,7 +151,7 @@ export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) =
|
||||
if (clock === 0) {
|
||||
continue
|
||||
}
|
||||
if (clock < getState(originDoc.store, client)) {
|
||||
if (clock < originDoc.store.getClock(client)) {
|
||||
getItemCleanStart(transaction, createID(client, clock))
|
||||
}
|
||||
const structs = originDoc.store.clients.get(client) || []
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import {
|
||||
GC,
|
||||
splitItem,
|
||||
createDeleteSetFromStructStore,
|
||||
createIdSet,
|
||||
Transaction, ID, Item, // eslint-disable-line
|
||||
Skip,
|
||||
createID,
|
||||
splitStruct
|
||||
} from '../internals.js'
|
||||
|
||||
import * as math from 'lib0/math'
|
||||
import * as error from 'lib0/error'
|
||||
import { Skip } from '../structs/Skip.js'
|
||||
import { createID } from './ID.js'
|
||||
import { createDeleteSetFromStructStore, createIdSet } from './ids.js'
|
||||
import { findIndexSS } from './transaction-helpers.js'
|
||||
|
||||
export class StructStore {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {Map<number,Array<GC|Item>>}
|
||||
* @type {Map<number,Array<GC|Item|Skip>>}
|
||||
*/
|
||||
this.clients = new Map()
|
||||
// this.ds = new IdSet()
|
||||
@@ -33,6 +24,90 @@ export class StructStore {
|
||||
get ds () {
|
||||
return createDeleteSetFromStructStore(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GC|Item|Skip} struct
|
||||
* @function
|
||||
*/
|
||||
add (struct) {
|
||||
let structs = this.clients.get(struct.id.client)
|
||||
if (structs === undefined) {
|
||||
structs = []
|
||||
this.clients.set(struct.id.client, structs)
|
||||
} else {
|
||||
const lastStruct = structs[structs.length - 1]
|
||||
if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) {
|
||||
// this replaces an integrated skip
|
||||
let index = findIndexSS(structs, struct.id.clock)
|
||||
const skip = structs[index]
|
||||
const diffStart = struct.id.clock - skip.id.clock
|
||||
const diffEnd = skip.id.clock + skip.length - struct.id.clock - struct.length
|
||||
if (diffStart > 0) {
|
||||
structs.splice(index++, 0, new Skip(createID(struct.id.client, skip.id.clock), diffStart))
|
||||
}
|
||||
if (diffEnd > 0) {
|
||||
structs.splice(index + 1, 0, new Skip(createID(struct.id.client, struct.id.clock + struct.length), diffEnd))
|
||||
}
|
||||
structs[index] = struct
|
||||
this.skips.delete(struct.id.client, struct.id.clock, struct.length)
|
||||
return
|
||||
}
|
||||
}
|
||||
structs.push(struct)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
*
|
||||
* @param {ID} id
|
||||
* @return {GC|Item}
|
||||
*/
|
||||
get (id) {
|
||||
const structs = /** @type {Array<GC|Item>} */ (this.clients.get(id.client))
|
||||
return structs[findIndexSS(structs, id.clock)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
*
|
||||
* @param {ID} id
|
||||
* @return {Item}
|
||||
*/
|
||||
getItem (id) {
|
||||
const structs = /** @type {Array<GC|Item>} */ (this.clients.get(id.client))
|
||||
return /** @type {Item} */ (structs[findIndexSS(structs, id.clock)])
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next expected clock for a specific client.
|
||||
*
|
||||
* @param {number} client
|
||||
* @return {number}
|
||||
*
|
||||
* @public
|
||||
* @function
|
||||
*/
|
||||
getClock (client) {
|
||||
const structs = this.clients.get(client)
|
||||
if (structs === undefined) {
|
||||
return 0
|
||||
}
|
||||
const lastStruct = structs[structs.length - 1]
|
||||
return lastStruct.id.clock + lastStruct.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a binary search on a sorted array
|
||||
* @param {ID} id
|
||||
* @return {{ structs: Array<GC|Item|Skip>, index: number }}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
getIndex (id) {
|
||||
const structs = this.clients.get(id.client) || []
|
||||
const index = findIndexSS(structs, id.clock)
|
||||
return { structs, index }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,23 +132,6 @@ export const getStateVector = store => {
|
||||
return sm
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
* @param {number} client
|
||||
* @return {number}
|
||||
*
|
||||
* @public
|
||||
* @function
|
||||
*/
|
||||
export const getState = (store, client) => {
|
||||
const structs = store.clients.get(client)
|
||||
if (structs === undefined) {
|
||||
return 0
|
||||
}
|
||||
const lastStruct = structs[structs.length - 1]
|
||||
return lastStruct.id.clock + lastStruct.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*
|
||||
@@ -91,199 +149,3 @@ export const integrityCheck = store => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
* @param {GC|Item} struct
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const addStruct = (store, struct) => {
|
||||
let structs = store.clients.get(struct.id.client)
|
||||
if (structs === undefined) {
|
||||
structs = []
|
||||
store.clients.set(struct.id.client, structs)
|
||||
} else {
|
||||
const lastStruct = structs[structs.length - 1]
|
||||
if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) {
|
||||
// this replaces an integrated skip
|
||||
let index = findIndexSS(structs, struct.id.clock)
|
||||
const skip = structs[index]
|
||||
const diffStart = struct.id.clock - skip.id.clock
|
||||
const diffEnd = skip.id.clock + skip.length - struct.id.clock - struct.length
|
||||
if (diffStart > 0) {
|
||||
structs.splice(index++, 0, new Skip(createID(struct.id.client, skip.id.clock), diffStart))
|
||||
}
|
||||
if (diffEnd > 0) {
|
||||
structs.splice(index + 1, 0, new Skip(createID(struct.id.client, struct.id.clock + struct.length), diffEnd))
|
||||
}
|
||||
structs[index] = struct
|
||||
store.skips.delete(struct.id.client, struct.id.clock, struct.length)
|
||||
return
|
||||
}
|
||||
}
|
||||
structs.push(struct)
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a binary search on a sorted array
|
||||
* @param {Array<Item|GC>} structs
|
||||
* @param {number} clock
|
||||
* @return {number}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const findIndexSS = (structs, clock) => {
|
||||
let left = 0
|
||||
let right = structs.length - 1
|
||||
let mid = structs[right]
|
||||
let midclock = mid.id.clock
|
||||
if (midclock === clock) {
|
||||
return right
|
||||
}
|
||||
// @todo does it even make sense to pivot the search?
|
||||
// If a good split misses, it might actually increase the time to find the correct item.
|
||||
// Currently, the only advantage is that search with pivoting might find the item on the first try.
|
||||
let midindex = math.floor((clock / (midclock + mid.length - 1)) * right) // pivoting the search
|
||||
while (left <= right) {
|
||||
mid = structs[midindex]
|
||||
midclock = mid.id.clock
|
||||
if (midclock <= clock) {
|
||||
if (clock < midclock + mid.length) {
|
||||
return midindex
|
||||
}
|
||||
left = midindex + 1
|
||||
} else {
|
||||
right = midindex - 1
|
||||
}
|
||||
midindex = math.floor((left + right) / 2)
|
||||
}
|
||||
// Always check state before looking for a struct in StructStore
|
||||
// Therefore the case of not finding a struct is unexpected
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
*
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {GC|Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const find = (store, id) => {
|
||||
/**
|
||||
* @type {Array<GC|Item>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(id.client)
|
||||
return structs[findIndexSS(structs, id.clock)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const getItem = /** @type {function(StructStore,ID):Item} */ (find)
|
||||
|
||||
/**
|
||||
* @param {Transaction?} transaction
|
||||
* @param {Array<Item|GC>} structs
|
||||
* @param {number} clock
|
||||
*/
|
||||
export const findIndexCleanStart = (transaction, structs, clock) => {
|
||||
const index = findIndexSS(structs, clock)
|
||||
const struct = structs[index]
|
||||
if (struct.id.clock < clock) {
|
||||
structs.splice(index + 1, 0, splitStruct(transaction, struct, clock - struct.id.clock))
|
||||
return index + 1
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {ID} id
|
||||
* @return {Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const getItemCleanStart = (transaction, id) => {
|
||||
const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client))
|
||||
return structs[findIndexCleanStart(transaction, structs, id.clock)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const getItemCleanEnd = (transaction, store, id) => {
|
||||
/**
|
||||
* @type {Array<Item>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(id.client)
|
||||
const index = findIndexSS(structs, id.clock)
|
||||
const struct = structs[index]
|
||||
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
|
||||
structs.splice(index + 1, 0, splitItem(transaction, struct, id.clock - struct.id.clock + 1))
|
||||
}
|
||||
return struct
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace `item` with `newitem` in store
|
||||
* @param {Transaction} tr
|
||||
* @param {GC|Item} struct
|
||||
* @param {GC|Item} newStruct
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const replaceStruct = (tr, struct, newStruct) => {
|
||||
const structs = /** @type {Array<GC|Item>} */ (tr.doc.store.clients.get(struct.id.client))
|
||||
structs[findIndexSS(structs, struct.id.clock)] = newStruct
|
||||
tr._mergeStructs.push(newStruct)
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over a range of structs
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Array<Item|GC>} structs
|
||||
* @param {number} clockStart Inclusive start
|
||||
* @param {number} len
|
||||
* @param {function(GC|Item):void} f
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const iterateStructs = (transaction, structs, clockStart, len, f) => {
|
||||
if (len === 0) {
|
||||
return
|
||||
}
|
||||
const clockEnd = clockStart + len
|
||||
let index = findIndexCleanStart(transaction, structs, clockStart)
|
||||
let struct
|
||||
do {
|
||||
struct = structs[index++]
|
||||
if (clockEnd < struct.id.clock + struct.length) {
|
||||
findIndexCleanStart(transaction, structs, clockEnd)
|
||||
}
|
||||
f(struct)
|
||||
} while (index < structs.length && structs[index].id.clock < clockEnd)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
import {
|
||||
getState,
|
||||
writeStructsFromTransaction,
|
||||
writeIdSet,
|
||||
getStateVector,
|
||||
findIndexSS,
|
||||
callEventHandlerListeners,
|
||||
createIdSet,
|
||||
Item,
|
||||
generateNewClientId,
|
||||
createID,
|
||||
iterateStructsByIdSet,
|
||||
ContentFormat,
|
||||
IdSet, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import { YType } from '../ytype.js' // eslint-disable-line
|
||||
import * as error from 'lib0/error'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
import * as set from 'lib0/set'
|
||||
import * as logging from 'lib0/logging'
|
||||
import { callAll } from 'lib0/function'
|
||||
|
||||
import { ContentFormat } from '../structs/Item.js'
|
||||
import { getStateVector } from './StructStore.js'
|
||||
import { callEventHandlerListeners } from './EventHandler.js'
|
||||
import { createIdSet, iterateStructsByIdSet } from './ids.js'
|
||||
import { GC } from '../structs/GC.js'
|
||||
import { YEvent } from './YEvent.js'
|
||||
import { writeUpdateMessageFromTransaction } from './encoding-helpers.js'
|
||||
import { UpdateEncoderV1, UpdateEncoderV2 } from './UpdateEncoder.js'
|
||||
import { findIndexSS, updateCurrentAttributes, cleanupFormattingGap, tryGcDeleteSet, tryMerge, tryToMergeWithLefts, cleanupContextlessFormattingGap } from './transaction-helpers.js'
|
||||
import * as random from 'lib0/random'
|
||||
|
||||
export const generateNewClientId = random.uint53
|
||||
|
||||
/**
|
||||
* A transaction is created for every change on the Yjs model. It is possible
|
||||
* to bundle changes on the Yjs model in a single transaction to
|
||||
@@ -170,246 +165,6 @@ export class Transaction {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Transaction} transaction
|
||||
* @return {boolean} Whether data was written.
|
||||
*/
|
||||
export const writeUpdateMessageFromTransaction = (encoder, transaction) => {
|
||||
if (transaction.deleteSet.clients.size === 0 && transaction.insertSet.clients.size === 0) {
|
||||
return false
|
||||
}
|
||||
writeStructsFromTransaction(encoder, transaction)
|
||||
writeIdSet(encoder, transaction.deleteSet)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const nextID = transaction => {
|
||||
const y = transaction.doc
|
||||
return createID(y.clientID, getState(y.store, y.clientID))
|
||||
}
|
||||
|
||||
/**
|
||||
* If `type.parent` was added in current transaction, `type` technically
|
||||
* did not change, it was just added and we should not fire events for `type`.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {YType} type
|
||||
* @param {string|null} parentSub
|
||||
*/
|
||||
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
|
||||
const item = type._item
|
||||
if (item === null || (!item.deleted && !transaction.insertSet.hasId(item.id))) {
|
||||
map.setIfUndefined(transaction.changed, type, set.create).add(parentSub)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<AbstractStruct>} structs
|
||||
* @param {number} pos
|
||||
* @return {number} # of merged structs
|
||||
*/
|
||||
const tryToMergeWithLefts = (structs, pos) => {
|
||||
let right = structs[pos]
|
||||
let left = structs[pos - 1]
|
||||
let i = pos
|
||||
for (; i > 0; right = left, left = structs[--i - 1]) {
|
||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||
if (left.mergeWith(right)) {
|
||||
if (right instanceof Item && right.parentSub !== null && /** @type {YType} */ (right.parent)._map.get(right.parentSub) === right) {
|
||||
/** @type {YType} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
const merged = pos - i
|
||||
if (merged) {
|
||||
// remove all merged structs from the array
|
||||
structs.splice(pos + 1 - merged, merged)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {IdSet} ds
|
||||
* @param {function(Item):boolean} gcFilter
|
||||
*/
|
||||
const tryGcDeleteSet = (tr, ds, gcFilter) => {
|
||||
for (const [client, _deleteItems] of ds.clients.entries()) {
|
||||
const deleteItems = _deleteItems.getIds()
|
||||
const structs = /** @type {Array<GC|Item>} */ (tr.doc.store.clients.get(client))
|
||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||
const deleteItem = deleteItems[di]
|
||||
const endDeleteItemClock = deleteItem.clock + deleteItem.len
|
||||
for (
|
||||
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
|
||||
si < structs.length && struct.id.clock < endDeleteItemClock;
|
||||
struct = structs[++si]
|
||||
) {
|
||||
const struct = structs[si]
|
||||
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
|
||||
break
|
||||
}
|
||||
if (struct instanceof Item && struct.deleted && !struct.keep && gcFilter(struct)) {
|
||||
struct.gc(tr, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} ds
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
const tryMerge = (ds, store) => {
|
||||
// try to merge deleted / gc'd items
|
||||
// merge from right to left for better efficiency and so we don't miss any merge targets
|
||||
ds.clients.forEach((_deleteItems, client) => {
|
||||
const deleteItems = _deleteItems.getIds()
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||
const deleteItem = deleteItems[di]
|
||||
// start with merging the item next to the last deleted item
|
||||
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
|
||||
for (
|
||||
let si = mostRightIndexToCheck, struct = structs[si];
|
||||
si > 0 && struct.id.clock >= deleteItem.clock;
|
||||
struct = structs[si]
|
||||
) {
|
||||
si -= 1 + tryToMergeWithLefts(structs, si)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {IdSet} idset
|
||||
* @param {function(Item):boolean} gcFilter
|
||||
*/
|
||||
export const tryGc = (tr, idset, gcFilter) => {
|
||||
tryGcDeleteSet(tr, idset, gcFilter)
|
||||
tryMerge(idset, tr.doc.store)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item | null} item
|
||||
*/
|
||||
const cleanupContextlessFormattingGap = (transaction, item) => {
|
||||
if (!transaction.doc.cleanupFormatting) return 0
|
||||
// iterate until item.right is null or content
|
||||
while (item && item.right && (item.right.deleted || !item.right.countable)) {
|
||||
item = item.right
|
||||
}
|
||||
const attrs = new Set()
|
||||
// iterate back until a content item is found
|
||||
while (item && (item.deleted || !item.countable)) {
|
||||
if (!item.deleted && item.content.constructor === ContentFormat) {
|
||||
const key = /** @type {ContentFormat} */ (item.content).key
|
||||
if (attrs.has(key)) {
|
||||
item.delete(transaction)
|
||||
transaction.cleanUps.add(item.id.client, item.id.clock, item.length)
|
||||
} else {
|
||||
attrs.add(key)
|
||||
}
|
||||
}
|
||||
item = item.left
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {ContentFormat} format
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const updateCurrentAttributes = (currentAttributes, { key, value }) => {
|
||||
if (value === null) {
|
||||
currentAttributes.delete(key)
|
||||
} else {
|
||||
currentAttributes.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function after string content has been deleted in order to
|
||||
* clean up formatting Items.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} start
|
||||
* @param {Item|null} curr exclusive end, automatically iterates to the next Content Item
|
||||
* @param {Map<string,any>} startAttributes
|
||||
* @param {Map<string,any>} currAttributes
|
||||
* @return {number} The amount of formatting Items deleted.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
|
||||
if (!transaction.doc.cleanupFormatting) return 0
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let end = start
|
||||
/**
|
||||
* @type {Map<string,ContentFormat>}
|
||||
*/
|
||||
const endFormats = map.create()
|
||||
while (end && (!end.countable || end.deleted)) {
|
||||
if (!end.deleted && end.content.constructor === ContentFormat) {
|
||||
const cf = /** @type {ContentFormat} */ (end.content)
|
||||
endFormats.set(cf.key, cf)
|
||||
}
|
||||
end = end.right
|
||||
}
|
||||
let cleanups = 0
|
||||
let reachedCurr = false
|
||||
while (start !== end) {
|
||||
if (curr === start) {
|
||||
reachedCurr = true
|
||||
}
|
||||
if (!start.deleted) {
|
||||
const content = start.content
|
||||
switch (content.constructor) {
|
||||
case ContentFormat: {
|
||||
const { key, value } = /** @type {ContentFormat} */ (content)
|
||||
const startAttrValue = startAttributes.get(key) ?? null
|
||||
if (endFormats.get(key) !== content || startAttrValue === value) {
|
||||
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
||||
start.delete(transaction)
|
||||
transaction.cleanUps.add(start.id.client, start.id.clock, start.length)
|
||||
cleanups++
|
||||
if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
|
||||
if (startAttrValue === null) {
|
||||
currAttributes.delete(key)
|
||||
} else {
|
||||
currAttributes.set(key, startAttrValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reachedCurr && !start.deleted) {
|
||||
updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
start = /** @type {Item} */ (start.right)
|
||||
}
|
||||
return cleanups
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is experimental and subject to change / be removed.
|
||||
*
|
||||
@@ -449,50 +204,6 @@ export const cleanupYTextFormatting = type => {
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be called by the transaction once the event handlers are called to potentially cleanup
|
||||
* formatting attributes.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
export const cleanupYTextAfterTransaction = transaction => {
|
||||
/**
|
||||
* @type {Set<YType>}
|
||||
*/
|
||||
const needFullCleanup = new Set()
|
||||
// check if another formatting item was inserted
|
||||
const doc = transaction.doc
|
||||
iterateStructsByIdSet(transaction, transaction.insertSet, (item) => {
|
||||
if (
|
||||
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
|
||||
) {
|
||||
needFullCleanup.add(/** @type {any} */ (item).parent)
|
||||
}
|
||||
})
|
||||
// cleanup in a new transaction
|
||||
transact(doc, (t) => {
|
||||
iterateStructsByIdSet(transaction, transaction.deleteSet, item => {
|
||||
if (item instanceof GC || !(/** @type {YType} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YType} */ (item.parent))) {
|
||||
return
|
||||
}
|
||||
const parent = /** @type {YType} */ (item.parent)
|
||||
if (item.content.constructor === ContentFormat) {
|
||||
needFullCleanup.add(parent)
|
||||
} else {
|
||||
// If no formatting attribute was inserted or deleted, we can make due with contextless
|
||||
// formatting cleanups.
|
||||
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
||||
cleanupContextlessFormattingGap(t, item)
|
||||
}
|
||||
})
|
||||
// If a formatting item was inserted, we simply clean the whole type.
|
||||
// We need to compute currentAttributes for the current position anyway.
|
||||
for (const yText of needFullCleanup) {
|
||||
cleanupYTextFormatting(yText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Transaction>} transactionCleanups
|
||||
* @param {number} i
|
||||
@@ -621,6 +332,50 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be called by the transaction once the event handlers are called to potentially cleanup
|
||||
* formatting attributes.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
export const cleanupYTextAfterTransaction = transaction => {
|
||||
/**
|
||||
* @type {Set<YType>}
|
||||
*/
|
||||
const needFullCleanup = new Set()
|
||||
// check if another formatting item was inserted
|
||||
const doc = transaction.doc
|
||||
iterateStructsByIdSet(transaction, transaction.insertSet, (item) => {
|
||||
if (
|
||||
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
|
||||
) {
|
||||
needFullCleanup.add(/** @type {any} */ (item).parent)
|
||||
}
|
||||
})
|
||||
// cleanup in a new transaction
|
||||
transact(doc, (t) => {
|
||||
iterateStructsByIdSet(transaction, transaction.deleteSet, item => {
|
||||
if (item instanceof GC || !(/** @type {YType} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YType} */ (item.parent))) {
|
||||
return
|
||||
}
|
||||
const parent = /** @type {YType} */ (item.parent)
|
||||
if (item.content.constructor === ContentFormat) {
|
||||
needFullCleanup.add(parent)
|
||||
} else {
|
||||
// If no formatting attribute was inserted or deleted, we can make due with contextless
|
||||
// formatting cleanups.
|
||||
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
||||
cleanupContextlessFormattingGap(t, item)
|
||||
}
|
||||
})
|
||||
// If a formatting item was inserted, we simply clean the whole type.
|
||||
// We need to compute currentAttributes for the current position anyway.
|
||||
for (const yText of needFullCleanup) {
|
||||
cleanupYTextFormatting(yText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the functionality of `y.transact(()=>{..})`
|
||||
*
|
||||
@@ -628,6 +383,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
* @param {Doc} doc
|
||||
* @param {function(Transaction):T} f
|
||||
* @param {any} [origin=true]
|
||||
* @param {boolean} [local=true]
|
||||
* @return {T}
|
||||
*
|
||||
* @function
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import {
|
||||
mergeIdSets,
|
||||
iterateStructsByIdSet,
|
||||
keepItem,
|
||||
transact,
|
||||
createID,
|
||||
redoItem,
|
||||
isParentOf,
|
||||
followRedone,
|
||||
getItemCleanStart,
|
||||
YEvent, Transaction, Doc, Item, GC, IdSet, YType, // eslint-disable-line
|
||||
diffIdSet
|
||||
} from '../internals.js'
|
||||
|
||||
import * as time from 'lib0/time'
|
||||
import * as array from 'lib0/array'
|
||||
import * as logging from 'lib0/logging'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
|
||||
import { mergeIdSets, iterateStructsByIdSet, diffIdSet } from './ids.js'
|
||||
import { Item, followRedone } from '../structs/Item.js'
|
||||
|
||||
import { transact } from './Transaction.js'
|
||||
import { createID } from './ID.js'
|
||||
import { isParentOf } from './isParentOf.js'
|
||||
import { getItemCleanStart } from './transaction-helpers.js'
|
||||
import { Doc } from './Doc.js'
|
||||
import { YType } from '../ytype.js'
|
||||
|
||||
export class StackItem {
|
||||
/**
|
||||
* @param {IdSet} insertions
|
||||
@@ -405,3 +401,142 @@ export const undoContentIds = (ydoc, contentIds, opts = {}) => {
|
||||
um.undoStack.push(new StackItem(diffIdSet(contentIds.inserts, contentIds.deletes), diffIdSet(contentIds.deletes, contentIds.inserts)))
|
||||
um.undo()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<StackItem>} stack
|
||||
* @param {ID} id
|
||||
*/
|
||||
const isDeletedByUndoStack = (stack, id) => array.some(stack, /** @param {StackItem} s */ s => s.deletes.hasId(id))
|
||||
|
||||
/**
|
||||
* Redoes the effect of this operation.
|
||||
*
|
||||
* @param {Transaction} transaction The Yjs instance.
|
||||
* @param {Item} item
|
||||
* @param {Set<Item>} redoitems
|
||||
* @param {IdSet} itemsToDelete
|
||||
* @param {boolean} ignoreRemoteMapChanges
|
||||
* @param {import('../utils/UndoManager.js').UndoManager} um
|
||||
*
|
||||
* @return {Item|null}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) => {
|
||||
const doc = transaction.doc
|
||||
const store = doc.store
|
||||
const ownClientID = doc.clientID
|
||||
const redone = item.redone
|
||||
if (redone !== null) {
|
||||
return getItemCleanStart(transaction, redone)
|
||||
}
|
||||
let parentItem = /** @type {YType} */ (item.parent)._item
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let left = null
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let right
|
||||
// make sure that parent is redone
|
||||
if (parentItem !== null && parentItem.deleted === true) {
|
||||
// try to undo parent if it will be undone anyway
|
||||
if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) === null)) {
|
||||
return null
|
||||
}
|
||||
while (parentItem.redone !== null) {
|
||||
parentItem = getItemCleanStart(transaction, parentItem.redone)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @type {YType}
|
||||
*/
|
||||
const parentType = /** @type {YType} */ (parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type)
|
||||
|
||||
if (item.parentSub === null) {
|
||||
// Is an array item. Insert at the old position
|
||||
left = item.left
|
||||
right = item
|
||||
// find next cloned_redo items
|
||||
while (left !== null) {
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let leftTrace = left
|
||||
// trace redone until parent matches
|
||||
while (leftTrace !== null && /** @type {YType} */ (leftTrace.parent)._item !== parentItem) {
|
||||
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
|
||||
}
|
||||
if (leftTrace !== null && /** @type {YType} */ (leftTrace.parent)._item === parentItem) {
|
||||
left = leftTrace
|
||||
break
|
||||
}
|
||||
left = left.left
|
||||
}
|
||||
while (right !== null) {
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let rightTrace = right
|
||||
// trace redone until parent matches
|
||||
while (rightTrace !== null && /** @type {YType} */ (rightTrace.parent)._item !== parentItem) {
|
||||
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
|
||||
}
|
||||
if (rightTrace !== null && /** @type {YType} */ (rightTrace.parent)._item === parentItem) {
|
||||
right = rightTrace
|
||||
break
|
||||
}
|
||||
right = right.right
|
||||
}
|
||||
} else {
|
||||
right = null
|
||||
if (item.right && !ignoreRemoteMapChanges) {
|
||||
left = item
|
||||
// Iterate right while right is in itemsToDelete
|
||||
// If it is intended to delete right while item is redone, we can expect that item should replace right.
|
||||
while (left !== null && left.right !== null && (left.right.redone || itemsToDelete.hasId(left.right.id) || isDeletedByUndoStack(um.undoStack, left.right.id) || isDeletedByUndoStack(um.redoStack, left.right.id))) {
|
||||
left = left.right
|
||||
// follow redone
|
||||
while (left.redone) left = getItemCleanStart(transaction, left.redone)
|
||||
}
|
||||
if (left && left.right !== null) {
|
||||
// It is not possible to redo this item because it conflicts with a
|
||||
// change from another client
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
left = parentType._map.get(item.parentSub) || null
|
||||
}
|
||||
}
|
||||
const nextClock = store.getClock(ownClientID)
|
||||
const nextId = createID(ownClientID, nextClock)
|
||||
const redoneItem = new Item(
|
||||
nextId,
|
||||
left, left && left.lastId,
|
||||
right, right && right.id,
|
||||
parentType,
|
||||
item.parentSub,
|
||||
item.content.copy()
|
||||
)
|
||||
item.redone = nextId
|
||||
keepItem(redoneItem, true)
|
||||
redoneItem.integrate(transaction, 0)
|
||||
return redoneItem
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that neither item nor any of its parents is ever deleted.
|
||||
*
|
||||
* This property does not persist when storing it into a database or when
|
||||
* sending it to other peers
|
||||
*
|
||||
* @param {Item|null} item
|
||||
* @param {boolean} keep
|
||||
*/
|
||||
export const keepItem = (item, keep) => {
|
||||
while (item !== null && item.keep !== keep) {
|
||||
item.keep = keep
|
||||
item = /** @type {YType} */ (item.parent)._item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as buffer from 'lib0/buffer'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import {
|
||||
ID, createID
|
||||
} from '../internals.js'
|
||||
|
||||
import { ID, createID } from './ID.js'
|
||||
|
||||
/**
|
||||
* @typedef {IdSetDecoderV1 | IdSetDecoderV2} IdSetDecoder
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import * as error from 'lib0/error'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
|
||||
import {
|
||||
ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
export class IdSetEncoderV1 {
|
||||
constructor () {
|
||||
this.restEncoder = encoding.createEncoder()
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import {
|
||||
diffIdSet,
|
||||
mergeIdSets,
|
||||
noAttributionsManager,
|
||||
YType, Doc, AbstractAttributionManager, Item, Transaction, AbstractStruct, // eslint-disable-line
|
||||
createAbsolutePositionFromRelativePosition,
|
||||
createRelativePosition
|
||||
} from '../internals.js'
|
||||
|
||||
import * as map from 'lib0/map'
|
||||
import * as delta from 'lib0/delta' // eslint-disable-line
|
||||
import * as set from 'lib0/set'
|
||||
|
||||
import { diffIdSet, mergeIdSets } from './ids.js'
|
||||
import { noAttributionsManager } from './attribution-manager-helpers.js'
|
||||
import { createAbsolutePositionFromRelativePosition, createRelativePosition } from './RelativePosition.js'
|
||||
|
||||
/**
|
||||
* @template {delta.DeltaConf} DConf
|
||||
* @template {DeltaConf} DConf
|
||||
* YEvent describes the changes on a YType.
|
||||
*/
|
||||
export class YEvent {
|
||||
@@ -38,11 +32,11 @@ export class YEvent {
|
||||
*/
|
||||
this.transaction = transaction
|
||||
/**
|
||||
* @type {delta.Delta<import('../ytype.js').DeltaConfDeltaToYType<DConf>>|null}
|
||||
* @type {Delta<import('../ytype.js').DeltaConfDeltaToYType<DConf>>|null}
|
||||
*/
|
||||
this._delta = null
|
||||
/**
|
||||
* @type {delta.Delta<DConf>|null}
|
||||
* @type {Delta<DConf>|null}
|
||||
*/
|
||||
this._deltaDeep = null
|
||||
/**
|
||||
@@ -94,7 +88,7 @@ export class YEvent {
|
||||
* @param {AbstractAttributionManager} am
|
||||
* @param {object} [opts]
|
||||
* @param {Deep} [opts.deep]
|
||||
* @return {Deep extends true ? delta.Delta<DConf> : delta.Delta<import('../internals.js').DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
|
||||
* @return {Deep extends true ? Delta<DConf> : Delta<import('../ytype.js').DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -133,7 +127,7 @@ export class YEvent {
|
||||
* Compute the changes in the delta format.
|
||||
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
|
||||
*
|
||||
* @type {delta.Delta<import('../internals.js').DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
|
||||
* @type {Delta<import('../ytype.js').DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
|
||||
* @public
|
||||
*/
|
||||
get delta () {
|
||||
@@ -144,7 +138,7 @@ export class YEvent {
|
||||
* Compute the changes in the delta format.
|
||||
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
|
||||
*
|
||||
* @type {delta.Delta<DConf>} The Delta representation of this type.
|
||||
* @type {Delta<DConf>} The Delta representation of this type.
|
||||
* @public
|
||||
*/
|
||||
get deltaDeep () {
|
||||
|
||||
110
src/utils/attribution-manager-helpers.js
Normal file
110
src/utils/attribution-manager-helpers.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as s from 'lib0/schema'
|
||||
import * as error from 'lib0/error'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
|
||||
export const attributionJsonSchema = s.$object({
|
||||
insert: s.$array(s.$string).optional,
|
||||
insertedAt: s.$number.optional,
|
||||
delete: s.$array(s.$string).optional,
|
||||
deletedAt: s.$number.optional,
|
||||
format: s.$record(s.$string, s.$array(s.$string)).optional,
|
||||
formatAt: s.$number.optional
|
||||
})
|
||||
|
||||
/**
|
||||
* @todo rename this to `insertBy`, `insertAt`, ..
|
||||
*
|
||||
* @typedef {s.Unwrap<typeof attributionJsonSchema>} Attribution
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
export class AttributedContent {
|
||||
/**
|
||||
* @param {AbstractContent} content
|
||||
* @param {number} clock
|
||||
* @param {boolean} deleted
|
||||
* @param {Array<ContentAttribute<T>> | null} attrs
|
||||
* @param {0|1|2} renderBehavior
|
||||
*/
|
||||
constructor (content, clock, deleted, attrs, renderBehavior) {
|
||||
this.content = content
|
||||
this.clock = clock
|
||||
this.deleted = deleted
|
||||
this.attrs = attrs
|
||||
this.render = renderBehavior === 0 ? false : (renderBehavior === 1 ? (!deleted || attrs != null) : true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for associating Attributions to content / changes
|
||||
*
|
||||
* Should fire an event when the attributions changed _after_ the original change happens. This
|
||||
* Event will be used to update the attribution on the current content.
|
||||
*
|
||||
* @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
|
||||
*/
|
||||
export class AbstractAttributionManager extends ObservableV2 {
|
||||
/**
|
||||
* @param {Array<AttributedContent<any>>} _contents - where to write the result
|
||||
* @param {number} _client
|
||||
* @param {number} _clock
|
||||
* @param {boolean} _deleted
|
||||
* @param {AbstractContent} _content
|
||||
* @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).
|
||||
*/
|
||||
readContent (_contents, _client, _clock, _deleted, _content, _shouldRender) {
|
||||
error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of the attributed content. This is used by iterators that walk through the
|
||||
* content.
|
||||
*
|
||||
* If the content is not countable, it should return 0.
|
||||
*
|
||||
* @param {Item} _item
|
||||
* @return {number}
|
||||
*/
|
||||
contentLength (_item) {
|
||||
error.methodUnimplemented()
|
||||
}
|
||||
}
|
||||
|
||||
export const $attributionManager = AbstractAttributionManager.prototype.$type = s.$type('y:am', AbstractAttributionManager)
|
||||
|
||||
/**
|
||||
* Abstract class for associating Attributions to content / changes
|
||||
*
|
||||
* @implements AbstractAttributionManager
|
||||
*
|
||||
* @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
|
||||
*/
|
||||
export class NoAttributionsManager extends ObservableV2 {
|
||||
get $type () { return $attributionManager }
|
||||
|
||||
/**
|
||||
* @param {Array<AttributedContent<any>>} contents - where to write the result
|
||||
* @param {number} _client
|
||||
* @param {number} clock
|
||||
* @param {boolean} deleted
|
||||
* @param {AbstractContent} content
|
||||
* @param {0|1|2} shouldRender - whether this should render or just result in a `retain` operation
|
||||
*/
|
||||
readContent (contents, _client, clock, deleted, content, shouldRender) {
|
||||
if (!deleted || shouldRender) {
|
||||
contents.push(new AttributedContent(content, clock, deleted, null, shouldRender))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Item} item
|
||||
* @return {number}
|
||||
*/
|
||||
contentLength (item) {
|
||||
return (item.deleted || !item.content.isCountable()) ? 0 : item.length
|
||||
}
|
||||
}
|
||||
|
||||
export const noAttributionsManager = new NoAttributionsManager()
|
||||
0
src/utils/content-helper.js
Normal file
0
src/utils/content-helper.js
Normal file
@@ -1,17 +1,11 @@
|
||||
import {
|
||||
createInsertSetFromStructStore,
|
||||
createDeleteSetFromStructStore,
|
||||
createAttributionManagerFromDiff,
|
||||
diffIdSet,
|
||||
mergeIdSets,
|
||||
Item,
|
||||
YType, Doc, // eslint-disable-line
|
||||
iterateStructsByIdSet
|
||||
} from '../internals.js'
|
||||
import * as delta from 'lib0/delta'
|
||||
import * as map from 'lib0/map'
|
||||
import * as set from 'lib0/set'
|
||||
|
||||
import { createInsertSetFromStructStore, createDeleteSetFromStructStore, diffIdSet, mergeIdSets, iterateStructsByIdSet } from './ids.js'
|
||||
import { createAttributionManagerFromDiff } from './AttributionManager.js'
|
||||
import { Item } from '../structs/Item.js'
|
||||
|
||||
/**
|
||||
* @param {Doc} v1
|
||||
* @param {Doc} v2
|
||||
|
||||
110
src/utils/encoding-helpers.js
Normal file
110
src/utils/encoding-helpers.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as math from 'lib0/math'
|
||||
import * as array from 'lib0/array'
|
||||
|
||||
import { findIndexSS } from './transaction-helpers.js'
|
||||
import { Skip } from '../structs/Skip.js'
|
||||
import { createID } from './ID.js'
|
||||
import { writeIdSet } from './ids.js'
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Array<GC|Item|Skip>} structs All structs by `client`
|
||||
* @param {number} client
|
||||
* @param {Array<IdRange>} idranges
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeStructs = (encoder, structs, client, idranges) => {
|
||||
let structsToWrite = 0 // this accounts for the skips
|
||||
/**
|
||||
* @type {Array<{ start: number, end: number, startClock: number, endClock: number }>}
|
||||
*/
|
||||
const indexRanges = []
|
||||
const firstPossibleClock = structs[0].id.clock
|
||||
const lastStruct = array.last(structs)
|
||||
const lastPossibleClock = lastStruct.id.clock + lastStruct.length
|
||||
idranges.forEach(idrange => {
|
||||
const startClock = math.max(idrange.clock, firstPossibleClock)
|
||||
const endClock = math.min(idrange.clock + idrange.len, lastPossibleClock)
|
||||
if (startClock >= endClock) return // structs for this range do not exist
|
||||
// inclusive start
|
||||
const start = findIndexSS(structs, startClock)
|
||||
// exclusive end
|
||||
const end = findIndexSS(structs, endClock - 1) + 1
|
||||
structsToWrite += end - start
|
||||
indexRanges.push({
|
||||
start,
|
||||
end,
|
||||
startClock,
|
||||
endClock
|
||||
})
|
||||
})
|
||||
structsToWrite += idranges.length - 1
|
||||
// start writing with this clock. this is updated to the next clock that we expect to write
|
||||
let clock = indexRanges[0].startClock
|
||||
// write # encoded structs
|
||||
encoding.writeVarUint(encoder.restEncoder, structsToWrite)
|
||||
encoder.writeClient(client)
|
||||
// write clock
|
||||
encoding.writeVarUint(encoder.restEncoder, clock)
|
||||
indexRanges.forEach(indexRange => {
|
||||
const skipLen = indexRange.startClock - clock
|
||||
if (skipLen > 0) {
|
||||
new Skip(createID(client, clock), skipLen).write(encoder, 0)
|
||||
clock += skipLen
|
||||
}
|
||||
for (let i = indexRange.start; i < indexRange.end; i++) {
|
||||
const struct = structs[i]
|
||||
const structEnd = struct.id.clock + struct.length
|
||||
const offsetEnd = math.max(structEnd - indexRange.endClock, 0)
|
||||
struct.write(encoder, clock - struct.id.clock, offsetEnd)
|
||||
clock = structEnd - offsetEnd
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {StructStore} store
|
||||
* @param {IdSet} idset
|
||||
*
|
||||
* @todo at the moment this writes the full deleteset range
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStructsFromIdSet = (encoder, store, idset) => {
|
||||
// write # states that were updated
|
||||
encoding.writeVarUint(encoder.restEncoder, idset.clients.size)
|
||||
// Write items with higher client ids first
|
||||
// This heavily improves the conflict algorithm.
|
||||
array.from(idset.clients.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, ids]) => {
|
||||
const idRanges = ids.getIds()
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||
writeStructs(encoder, structs, client, idRanges)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Transaction} transaction
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeStructsFromIdSet(encoder, transaction.doc.store, transaction.insertSet)
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Transaction} transaction
|
||||
* @return {boolean} Whether data was written.
|
||||
*/
|
||||
export const writeUpdateMessageFromTransaction = (encoder, transaction) => {
|
||||
if (transaction.deleteSet.clients.size === 0 && transaction.insertSet.clients.size === 0) {
|
||||
return false
|
||||
}
|
||||
writeStructsFromTransaction(encoder, transaction)
|
||||
writeIdSet(encoder, transaction.deleteSet)
|
||||
return true
|
||||
}
|
||||
@@ -14,94 +14,25 @@
|
||||
* 7: Item with Type
|
||||
*/
|
||||
|
||||
import {
|
||||
findIndexSS,
|
||||
getState,
|
||||
getStateVector,
|
||||
readAndApplyDeleteSet,
|
||||
writeIdSet,
|
||||
transact,
|
||||
UpdateDecoderV1,
|
||||
UpdateDecoderV2,
|
||||
UpdateEncoderV1,
|
||||
UpdateEncoderV2,
|
||||
IdSetEncoderV2,
|
||||
IdSetDecoderV1,
|
||||
IdSetEncoderV1,
|
||||
mergeUpdates,
|
||||
mergeUpdatesV2,
|
||||
Skip,
|
||||
diffUpdateV2,
|
||||
convertUpdateFormatV2ToV1,
|
||||
readBlockSet,
|
||||
createIdSet,
|
||||
BlockSet, IdSet, IdSetDecoderV2, Doc, Transaction, GC, Item, StructStore, // eslint-disable-line
|
||||
createID,
|
||||
IdRange
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
import * as array from 'lib0/array'
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Array<GC|Item>} structs All structs by `client`
|
||||
* @param {number} client
|
||||
* @param {Array<IdRange>} idranges
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeStructs = (encoder, structs, client, idranges) => {
|
||||
let structsToWrite = 0 // this accounts for the skips
|
||||
/**
|
||||
* @type {Array<{ start: number, end: number, startClock: number, endClock: number }>}
|
||||
*/
|
||||
const indexRanges = []
|
||||
const firstPossibleClock = structs[0].id.clock
|
||||
const lastStruct = array.last(structs)
|
||||
const lastPossibleClock = lastStruct.id.clock + lastStruct.length
|
||||
idranges.forEach(idrange => {
|
||||
const startClock = math.max(idrange.clock, firstPossibleClock)
|
||||
const endClock = math.min(idrange.clock + idrange.len, lastPossibleClock)
|
||||
if (startClock >= endClock) return // structs for this range do not exist
|
||||
// inclusive start
|
||||
const start = findIndexSS(structs, startClock)
|
||||
// exclusive end
|
||||
const end = findIndexSS(structs, endClock - 1) + 1
|
||||
structsToWrite += end - start
|
||||
indexRanges.push({
|
||||
start,
|
||||
end,
|
||||
startClock,
|
||||
endClock
|
||||
})
|
||||
})
|
||||
structsToWrite += idranges.length - 1
|
||||
// start writing with this clock. this is updated to the next clock that we expect to write
|
||||
let clock = indexRanges[0].startClock
|
||||
// write # encoded structs
|
||||
encoding.writeVarUint(encoder.restEncoder, structsToWrite)
|
||||
encoder.writeClient(client)
|
||||
// write clock
|
||||
encoding.writeVarUint(encoder.restEncoder, clock)
|
||||
indexRanges.forEach(indexRange => {
|
||||
const skipLen = indexRange.startClock - clock
|
||||
if (skipLen > 0) {
|
||||
new Skip(createID(client, clock), skipLen).write(encoder, 0)
|
||||
clock += skipLen
|
||||
}
|
||||
for (let i = indexRange.start; i < indexRange.end; i++) {
|
||||
const struct = structs[i]
|
||||
const structEnd = struct.id.clock + struct.length
|
||||
const offsetEnd = math.max(structEnd - indexRange.endClock, 0)
|
||||
struct.write(encoder, clock - struct.id.clock, offsetEnd)
|
||||
clock = structEnd - offsetEnd
|
||||
}
|
||||
})
|
||||
}
|
||||
import { getStateVector, StructStore } from './StructStore.js'
|
||||
import { getItemCleanStart, getItemCleanEnd } from './transaction-helpers.js'
|
||||
import { createIdSet, IdRange, readAndApplyDeleteSet, readIdSet, mergeIdSets, writeIdSet } from './ids.js'
|
||||
import { createID, ID } from './ID.js'
|
||||
import { UpdateDecoderV1, UpdateDecoderV2, IdSetDecoderV1 } from './UpdateDecoder.js'
|
||||
import { UpdateEncoderV1, UpdateEncoderV2, IdSetEncoderV1, IdSetEncoderV2 } from './UpdateEncoder.js'
|
||||
import { convertUpdateFormatV2ToV1, LazyStructReader, LazyStructWriter, writeStructToLazyStructWriter, finishLazyStructWriting } from './updates.js'
|
||||
import { readBlockSet, writeBlockSet } from './BlockSet.js'
|
||||
import { Skip } from '../structs/Skip.js'
|
||||
import { Item } from '../structs/Item.js'
|
||||
import { GC } from '../structs/GC.js'
|
||||
import { Doc } from './Doc.js'
|
||||
import { writeStructs } from './encoding-helpers.js'
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
@@ -116,7 +47,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
const sm = new Map()
|
||||
_sm.forEach((clock, client) => {
|
||||
// only write if new structs are available
|
||||
if (getState(store, client) > clock) {
|
||||
if (store.getClock(client) > clock) {
|
||||
sm.set(client, clock)
|
||||
}
|
||||
})
|
||||
@@ -136,28 +67,6 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {StructStore} store
|
||||
* @param {IdSet} idset
|
||||
*
|
||||
* @todo at the moment this writes the full deleteset range
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStructsFromIdSet = (encoder, store, idset) => {
|
||||
// write # states that were updated
|
||||
encoding.writeVarUint(encoder.restEncoder, idset.clients.size)
|
||||
// Write items with higher client ids first
|
||||
// This heavily improves the conflict algorithm.
|
||||
array.from(idset.clients.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, ids]) => {
|
||||
const idRanges = ids.getIds()
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||
writeStructs(encoder, structs, client, idRanges)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume computing structs generated by struct readers.
|
||||
*
|
||||
@@ -187,7 +96,7 @@ export const writeStructsFromIdSet = (encoder, store, idset) => {
|
||||
*/
|
||||
const integrateStructs = (transaction, store, clientsStructRefs) => {
|
||||
/**
|
||||
* @type {Array<Item | GC>}
|
||||
* @type {Array<Item | GC | Skip>}
|
||||
*/
|
||||
const stack = []
|
||||
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
|
||||
@@ -231,7 +140,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @type {GC|Item}
|
||||
* @type {GC|Item|Skip}
|
||||
*/
|
||||
let stackHead = /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++]
|
||||
// caching the state because it is used very often
|
||||
@@ -267,19 +176,19 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
||||
// iterate over all struct readers until we are done
|
||||
while (true) {
|
||||
if (stackHead.constructor !== Skip) {
|
||||
const localClock = map.setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client))
|
||||
const localClock = map.setIfUndefined(state, stackHead.id.client, () => store.getClock(stackHead.id.client))
|
||||
const offset = localClock - stackHead.id.clock
|
||||
const missing = stackHead.getMissing(transaction, store)
|
||||
const missing = getMissing(/** @type {any} */ (stackHead), transaction, store)
|
||||
if (missing !== null) {
|
||||
stack.push(stackHead)
|
||||
// get the struct reader that has the missing struct
|
||||
/**
|
||||
* @type {{ refs: Array<GC|Item>, i: number }}
|
||||
* @type {{ refs: Array<GC|Item|Skip>, i: number }}
|
||||
*/
|
||||
const structRefs = clientsStructRefs.clients.get(/** @type {number} */ (missing)) || { refs: [], i: 0 }
|
||||
if (structRefs.refs.length === structRefs.i || missing === stackHead.id.client || stack.some(s => s.id.client === missing)) { // @todo this could be optimized!
|
||||
// This update message causally depends on another update message that doesn't exist yet
|
||||
updateMissingSv(/** @type {number} */ (missing), getState(store, missing))
|
||||
updateMissingSv(/** @type {number} */ (missing), store.getClock(missing))
|
||||
addStackToRestSS()
|
||||
} else {
|
||||
stackHead = structRefs.refs[structRefs.i++]
|
||||
@@ -322,15 +231,6 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Transaction} transaction
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeStructsFromIdSet(encoder, transaction.doc.store, transaction.insertSet)
|
||||
|
||||
/**
|
||||
* Read and apply a document update.
|
||||
*
|
||||
@@ -344,7 +244,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeStruct
|
||||
* @function
|
||||
*/
|
||||
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
|
||||
transact(ydoc, transaction => {
|
||||
ydoc.transact(transaction => {
|
||||
// force that transaction.local is set to non-local
|
||||
transaction.local = false
|
||||
let retry = false
|
||||
@@ -375,7 +275,7 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
|
||||
if (pending) {
|
||||
// check if we can apply something
|
||||
for (const [client, clock] of pending.missing) {
|
||||
if (ss.clients.has(client) || clock < getState(store, client)) {
|
||||
if (ss.clients.has(client) || clock < store.getClock(client)) {
|
||||
retry = true
|
||||
break
|
||||
}
|
||||
@@ -551,6 +451,94 @@ export const readStateVector = decoder => {
|
||||
return ss
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This function works similarly to `readUpdateV2`.
|
||||
*
|
||||
* @param {Array<Uint8Array<ArrayBuffer>>} updates
|
||||
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
|
||||
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
|
||||
* @return {Uint8Array<ArrayBuffer>}
|
||||
*/
|
||||
export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
|
||||
if (updates.length === 1) {
|
||||
return updates[0]
|
||||
} else if (updates.length === 0) {
|
||||
return encodeStateAsUpdateV2(new Doc(), new Uint8Array([0]), new YEncoder())
|
||||
}
|
||||
const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
|
||||
const blocksets = updateDecoders.map(dec => readBlockSet(dec))
|
||||
|
||||
const mergedBlockset = blocksets[0]
|
||||
for (let i = 1; i < blocksets.length; i++) {
|
||||
mergedBlockset.insertInto(blocksets[i])
|
||||
}
|
||||
const updateEncoder = new YEncoder()
|
||||
writeBlockSet(updateEncoder, mergedBlockset)
|
||||
const dss = updateDecoders.map(decoder => readIdSet(decoder))
|
||||
const ds = mergeIdSets(dss)
|
||||
writeIdSet(updateEncoder, ds)
|
||||
return updateEncoder.toUint8Array()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Uint8Array<ArrayBuffer>>} updates
|
||||
* @return {Uint8Array<ArrayBuffer>}
|
||||
*/
|
||||
export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1)
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param {Uint8Array} update
|
||||
* @param {Uint8Array} sv
|
||||
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
|
||||
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
|
||||
*/
|
||||
export const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
|
||||
const state = decodeStateVector(sv)
|
||||
const encoder = new YEncoder()
|
||||
const lazyStructWriter = new LazyStructWriter(encoder)
|
||||
const decoder = new YDecoder(decoding.createDecoder(update))
|
||||
const reader = new LazyStructReader(decoder, false)
|
||||
while (reader.curr) {
|
||||
const curr = reader.curr
|
||||
const currClient = curr.id.client
|
||||
const svClock = state.get(currClient) || 0
|
||||
if (reader.curr.constructor === Skip) {
|
||||
// the first written struct shouldn't be a skip
|
||||
reader.next()
|
||||
continue
|
||||
}
|
||||
if (curr.id.clock + curr.length > svClock) {
|
||||
writeStructToLazyStructWriter(lazyStructWriter, curr, math.max(svClock - curr.id.clock, 0), 0)
|
||||
reader.next()
|
||||
while (reader.curr && reader.curr.id.client === currClient) {
|
||||
writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0, 0)
|
||||
reader.next()
|
||||
}
|
||||
} else {
|
||||
// read until something new comes up
|
||||
while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) {
|
||||
reader.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
finishLazyStructWriting(lazyStructWriter)
|
||||
// write ds
|
||||
const ds = readIdSet(decoder)
|
||||
writeIdSet(encoder, ds)
|
||||
return encoder.toUint8Array()
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @todo remove this in favor of intersectupdate
|
||||
*
|
||||
* @param {Uint8Array<ArrayBuffer>} update
|
||||
* @param {Uint8Array<ArrayBuffer>} sv
|
||||
*/
|
||||
export const diffUpdate = (update, sv) => diffUpdateV2(update, sv, UpdateDecoderV1, UpdateEncoderV1)
|
||||
|
||||
/**
|
||||
* Read decodedState and return State as Map.
|
||||
*
|
||||
@@ -620,3 +608,90 @@ export const encodeStateVectorV2 = (doc, encoder = new IdSetEncoderV2()) => {
|
||||
* @function
|
||||
*/
|
||||
export const encodeStateVector = doc => encodeStateVectorV2(doc, new IdSetEncoderV1())
|
||||
|
||||
/**
|
||||
* Return the creator clientID of the missing op or define missing items and return null.
|
||||
*
|
||||
* @param {Item} struct
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @return {null | number}
|
||||
*/
|
||||
const getMissing = (struct, transaction, store) => {
|
||||
if (struct.constructor !== Item) return null
|
||||
// we may not access these variables anymore after they have been written!
|
||||
const origin = struct.origin
|
||||
const rightOrigin = struct.rightOrigin
|
||||
const parent = struct.parent
|
||||
if (origin && (origin.clock >= store.getClock(origin.client) || store.skips.hasId(origin))) {
|
||||
return origin.client
|
||||
}
|
||||
if (rightOrigin && (rightOrigin.clock >= store.getClock(rightOrigin.client) || store.skips.hasId(rightOrigin))) {
|
||||
return rightOrigin.client
|
||||
}
|
||||
if (parent && parent.constructor === ID && (parent.clock >= store.getClock(parent.client) || store.skips.hasId(parent))) {
|
||||
return parent.client
|
||||
}
|
||||
// We have all missing ids, now find the items
|
||||
if (origin) {
|
||||
struct.left = getItemCleanEnd(transaction, store, origin)
|
||||
// copy left id to so that the original id can be gc'd
|
||||
struct.origin = struct.left.lastId
|
||||
}
|
||||
if (rightOrigin) {
|
||||
struct.right = getItemCleanStart(transaction, rightOrigin)
|
||||
struct.rightOrigin = struct.right.id
|
||||
}
|
||||
if ((struct.left && struct.left.constructor === GC) || (struct.right && struct.right.constructor === GC)) {
|
||||
struct.parent = null
|
||||
} else if (parent == null) {
|
||||
// only set parent if this shouldn't be garbage collected
|
||||
if (struct.left && struct.left.constructor === Item) {
|
||||
struct.parent = struct.left.parent
|
||||
struct.parentSub = struct.left.parentSub
|
||||
} else if (struct.right && struct.right.constructor === Item) {
|
||||
struct.parent = struct.right.parent
|
||||
struct.parentSub = struct.right.parentSub
|
||||
}
|
||||
} else if (parent.constructor === ID) {
|
||||
const parentItem = store.getItem(parent)
|
||||
if (parentItem.constructor === GC) {
|
||||
struct.parent = null
|
||||
} else {
|
||||
struct.parent = /** @type {ContentType} */ (parentItem.content).type
|
||||
}
|
||||
} else if (typeof parent === 'string') {
|
||||
struct.parent = transaction.doc.get(parent)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
* @param {import('./Doc.js').DocOpts} opts
|
||||
*/
|
||||
export const createDocFromUpdate = (update, opts = {}) => {
|
||||
const ydoc = new Doc(opts)
|
||||
applyUpdate(ydoc, update)
|
||||
return ydoc
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
* @param {import('./Doc.js').DocOpts} opts
|
||||
*/
|
||||
export const createDocFromUpdateV2 = (update, opts = {}) => {
|
||||
const ydoc = new Doc(opts)
|
||||
applyUpdateV2(ydoc, update)
|
||||
return ydoc
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Doc} ydoc
|
||||
* @param {import('./Doc.js').DocOpts} [opts]
|
||||
*/
|
||||
export const cloneDoc = (ydoc, opts) => {
|
||||
const clone = new Doc(opts)
|
||||
applyUpdate(clone, encodeStateAsUpdate(ydoc))
|
||||
return clone
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import {
|
||||
findIndexSS,
|
||||
getState,
|
||||
splitItem,
|
||||
iterateStructs,
|
||||
UpdateEncoderV2,
|
||||
IdMap,
|
||||
AttrRanges,
|
||||
AttrRange,
|
||||
Skip, AbstractStruct, IdSetDecoderV1, IdSetEncoderV1, IdSetDecoderV2, IdSetEncoderV2, Item, GC, StructStore, Transaction, ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as array from 'lib0/array'
|
||||
import * as math from 'lib0/math'
|
||||
import * as traits from 'lib0/traits'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as traits from 'lib0/traits'
|
||||
import * as buf from 'lib0/buffer'
|
||||
import * as rabin from 'lib0/hash/rabin'
|
||||
import * as array from 'lib0/array'
|
||||
import * as map from 'lib0/map'
|
||||
|
||||
import { iterateStructs, findIndexSS } from './transaction-helpers.js'
|
||||
import { UpdateEncoderV2, IdSetEncoderV2 } from './UpdateEncoder.js'
|
||||
import { IdSetDecoderV2 } from './UpdateDecoder.js'
|
||||
|
||||
export class IdRange {
|
||||
/**
|
||||
@@ -43,7 +38,7 @@ export class IdRange {
|
||||
/**
|
||||
* Helper method making this compatible with IdMap.
|
||||
*
|
||||
* @return {Array<import('./IdMap.js').ContentAttribute<any>>}
|
||||
* @return {Array<ContentAttribute<any>>}
|
||||
*/
|
||||
get attrs () {
|
||||
return []
|
||||
@@ -263,7 +258,13 @@ export class IdSet {
|
||||
* @param {number} len
|
||||
*/
|
||||
add (client, clock, len) {
|
||||
addToIdSet(this, client, clock, len)
|
||||
if (len === 0) return
|
||||
const idRanges = this.clients.get(client)
|
||||
if (idRanges) {
|
||||
idRanges.add(clock, len)
|
||||
} else {
|
||||
this.clients.set(client, new IdRanges([new IdRange(clock, len)]))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,6 +284,27 @@ export class IdSet {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} ds1
|
||||
* @param {IdSet} ds2
|
||||
*/
|
||||
export const equalIdSets = (ds1, ds2) => {
|
||||
if (ds1.clients.size !== ds2.clients.size) return false
|
||||
for (const [client, _deleteItems1] of ds1.clients.entries()) {
|
||||
const deleteItems1 = _deleteItems1.getIds()
|
||||
const deleteItems2 = ds2.clients.get(client)?.getIds()
|
||||
if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false
|
||||
for (let i = 0; i < deleteItems1.length; i++) {
|
||||
const di1 = deleteItems1[i]
|
||||
const di2 = deleteItems2[i]
|
||||
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet | IdMap<any>} set
|
||||
* @param {number} client
|
||||
@@ -422,7 +444,7 @@ export const mergeIdSets = idSets => {
|
||||
* @param {S} dest
|
||||
* @param {S} src
|
||||
*/
|
||||
export const _insertIntoIdSet = (dest, src) => {
|
||||
const _insertIntoIdSet = (dest, src) => {
|
||||
src.clients.forEach((srcRanges, client) => {
|
||||
const targetRanges = dest.clients.get(client)
|
||||
if (targetRanges) {
|
||||
@@ -442,6 +464,12 @@ export const _insertIntoIdSet = (dest, src) => {
|
||||
*/
|
||||
export const insertIntoIdSet = _insertIntoIdSet
|
||||
|
||||
/**
|
||||
* @param {IdMap<any>} dest
|
||||
* @param {IdMap<any>|IdSet} src
|
||||
*/
|
||||
export const insertIntoIdMap = _insertIntoIdSet
|
||||
|
||||
/**
|
||||
* @todo rename to excludeIdSet | excludeIdMap
|
||||
*
|
||||
@@ -570,34 +598,6 @@ export const _intersectSets = (setA, setB) => {
|
||||
|
||||
export const intersectSets = _intersectSets
|
||||
|
||||
/**
|
||||
* @param {IdSet} idSet
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @param {number} length
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const addToIdSet = (idSet, client, clock, length) => {
|
||||
if (length === 0) return
|
||||
const idRanges = idSet.clients.get(client)
|
||||
if (idRanges) {
|
||||
idRanges.add(clock, length)
|
||||
} else {
|
||||
idSet.clients.set(client, new IdRanges([new IdRange(clock, length)]))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} idSet
|
||||
* @param {AbstractStruct} struct
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const addStructToIdSet = (idSet, struct) => addToIdSet(idSet, struct.id.client, struct.id.clock, struct.length)
|
||||
|
||||
export const createIdSet = () => new IdSet()
|
||||
|
||||
/**
|
||||
@@ -635,7 +635,7 @@ export const createDeleteSetFromStructStore = ss => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<GC | Item>} structs
|
||||
* @param {Array<GC | Item | Skip>} structs
|
||||
* @param {boolean} filterDeleted
|
||||
*
|
||||
*/
|
||||
@@ -662,7 +662,7 @@ export const _createInsertSliceFromStructs = (structs, filterDeleted) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../internals.js').StructStore} ss
|
||||
* @param {StructStore} ss
|
||||
* @param {boolean} filterDeleted
|
||||
*/
|
||||
export const createInsertSetFromStructStore = (ss, filterDeleted) => {
|
||||
@@ -751,13 +751,13 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
||||
const client = decoding.readVarUint(decoder.restDecoder)
|
||||
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
|
||||
const structs = store.clients.get(client) || []
|
||||
const state = getState(store, client)
|
||||
const state = store.getClock(client)
|
||||
for (let i = 0; i < numberOfDeletes; i++) {
|
||||
const clock = decoder.readDsClock()
|
||||
const clockEnd = clock + decoder.readDsLen()
|
||||
if (clock < state) {
|
||||
if (state < clockEnd) {
|
||||
addToIdSet(unappliedDS, client, state, clockEnd - state)
|
||||
unappliedDS.add(client, state, clockEnd - state)
|
||||
}
|
||||
let index = findIndexSS(structs, clock)
|
||||
/**
|
||||
@@ -766,18 +766,18 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
||||
*/
|
||||
let struct = structs[index]
|
||||
// split the first item if necessary
|
||||
if (!struct.deleted && struct.id.clock < clock && struct instanceof Item) {
|
||||
if (!struct.deleted && struct.id.clock < clock && struct.isItem) {
|
||||
// increment index, we now want to use the next struct
|
||||
structs.splice(++index, 0, splitItem(transaction, struct, clock - struct.id.clock))
|
||||
structs.splice(++index, 0, /** @type {Item} */ (struct).split(transaction, clock - struct.id.clock))
|
||||
}
|
||||
while (index < structs.length) {
|
||||
// @ts-ignore
|
||||
struct = structs[index++]
|
||||
if (struct.id.clock < clockEnd) {
|
||||
if (!struct.deleted) {
|
||||
if (struct instanceof Item) {
|
||||
if (struct.isItem) {
|
||||
if (clockEnd < struct.id.clock + struct.length) {
|
||||
structs.splice(index, 0, splitItem(transaction, struct, clockEnd - struct.id.clock))
|
||||
structs.splice(index, 0, /** @type {Item} */ (struct).split(transaction, clockEnd - struct.id.clock))
|
||||
}
|
||||
struct.delete(transaction)
|
||||
} else { // is a Skip - add range to unappliedDS
|
||||
@@ -790,7 +790,7 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addToIdSet(unappliedDS, client, clock, clockEnd - clock)
|
||||
unappliedDS.add(client, clock, clockEnd - clock)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -804,22 +804,649 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} ds1
|
||||
* @param {IdSet} ds2
|
||||
* @template Attrs
|
||||
*/
|
||||
export const equalIdSets = (ds1, ds2) => {
|
||||
if (ds1.clients.size !== ds2.clients.size) return false
|
||||
for (const [client, _deleteItems1] of ds1.clients.entries()) {
|
||||
const deleteItems1 = _deleteItems1.getIds()
|
||||
const deleteItems2 = ds2.clients.get(client)?.getIds()
|
||||
if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false
|
||||
for (let i = 0; i < deleteItems1.length; i++) {
|
||||
const di1 = deleteItems1[i]
|
||||
const di2 = deleteItems2[i]
|
||||
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
||||
return false
|
||||
export class AttrRange {
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @param {Array<ContentAttribute<Attrs>>} attrs
|
||||
*/
|
||||
constructor (clock, len, attrs) {
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
this.clock = clock
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
this.len = len
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
this.attrs = attrs
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
*/
|
||||
copyWith (clock, len) {
|
||||
return new AttrRange(clock, len, this.attrs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo rename this to `Attribute`
|
||||
* @template V
|
||||
*/
|
||||
export class ContentAttribute {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {V} val
|
||||
*/
|
||||
constructor (name, val) {
|
||||
this.name = name
|
||||
this.val = val
|
||||
}
|
||||
|
||||
hash () {
|
||||
const encoder = encoding.createEncoder()
|
||||
encoding.writeVarString(encoder, this.name)
|
||||
encoding.writeAny(encoder, /** @type {any} */ (this.val))
|
||||
return buf.toBase64(rabin.fingerprint(rabin.StandardIrreducible128, encoding.toUint8Array(encoder)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template V
|
||||
* @param {string} name
|
||||
* @param {V} val
|
||||
* @return {ContentAttribute<V>}
|
||||
*/
|
||||
export const createContentAttribute = (name, val) => new ContentAttribute(name, val)
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
* @typedef {{ clock: number, len: number, attrs: Array<ContentAttribute<Attrs>>? }} MaybeAttrRange
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
*
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @param {Array<ContentAttribute<Attrs>>?} attrs
|
||||
* @return {MaybeAttrRange<Attrs>}
|
||||
*/
|
||||
export const createMaybeAttrRange = (clock, len, attrs) => new AttrRange(clock, len, /** @type {any} */ (attrs))
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} a
|
||||
* @param {Array<T>} b
|
||||
*/
|
||||
const idmapAttrRangeJoin = (a, b) => a.concat(b.filter(attr => !idmapAttrsHas(a, attr)))
|
||||
|
||||
/**
|
||||
* Whenever this is instantiated, it must receive a fresh array of ops, not something copied.
|
||||
*
|
||||
* @template Attrs
|
||||
*/
|
||||
export class AttrRanges {
|
||||
/**
|
||||
* @param {Array<AttrRange<Attrs>>} ids
|
||||
*/
|
||||
constructor (ids) {
|
||||
this.sorted = false
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this._ids = ids
|
||||
}
|
||||
|
||||
copy () {
|
||||
return new AttrRanges(this._ids.slice())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} length
|
||||
* @param {Array<ContentAttribute<Attrs>>} attrs
|
||||
*/
|
||||
add (clock, length, attrs) {
|
||||
if (length === 0) return
|
||||
this.sorted = false
|
||||
this._ids.push(new AttrRange(clock, length, attrs))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of id ranges, sorted and merged.
|
||||
*/
|
||||
getIds () {
|
||||
const ids = this._ids
|
||||
if (!this.sorted) {
|
||||
this.sorted = true
|
||||
ids.sort((a, b) => a.clock - b.clock)
|
||||
/**
|
||||
* algorithm thoughts:
|
||||
* - sort (by clock AND by length), bigger length is to the right (or not, we can't make
|
||||
* assumptions abouth length after long length has been split)
|
||||
* -- maybe better: sort by clock+length. Then split items from right to left. This way, items are always
|
||||
* in the right order. But I also need to swap if left items is smaller after split
|
||||
* --- thought: there is no way to go around swapping. Unless, for each item from left to
|
||||
* right, when I have to split because one of the look-ahead items is overlapping, i split
|
||||
* it and merge the attributes into the following ones (that I also need to split). Best is
|
||||
* probably left to right with lookahead.
|
||||
* - left to right, split overlapping items so that we can make the assumption that either an
|
||||
* item is overlapping with the next 1-on-1 or it is not overlapping at all (when splitting,
|
||||
* we can already incorporate the attributes)
|
||||
* -- better: for each item, go left to right and add own attributes to overlapping items.
|
||||
* Split them if necessary. After split, i must insert the retainer at a valid position.
|
||||
* - merge items if neighbor has same attributes
|
||||
*/
|
||||
for (let i = 0; i < ids.length - 1;) {
|
||||
const range = ids[i]
|
||||
const nextRange = ids[i + 1]
|
||||
// find out how to split range. it must match with next range.
|
||||
// 1) we have space. Split if necessary.
|
||||
// 2) concat attributes in range to the next range. Split range and splice the remainder at
|
||||
// the correct position.
|
||||
if (range.clock < nextRange.clock) { // might need to split range
|
||||
if (range.clock + range.len > nextRange.clock) {
|
||||
// is overlapping
|
||||
const diff = nextRange.clock - range.clock
|
||||
ids[i] = new AttrRange(range.clock, diff, range.attrs)
|
||||
ids.splice(i + 1, 0, new AttrRange(nextRange.clock, range.len - diff, range.attrs))
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// now we know that range.clock === nextRange.clock
|
||||
// merge range with nextRange
|
||||
const largerRange = range.len > nextRange.len ? range : nextRange
|
||||
const smallerLen = range.len < nextRange.len ? range.len : nextRange.len
|
||||
ids[i] = new AttrRange(range.clock, smallerLen, idmapAttrRangeJoin(range.attrs, nextRange.attrs))
|
||||
if (range.len === nextRange.len) {
|
||||
ids.splice(i + 1, 1)
|
||||
} else {
|
||||
ids[i + 1] = new AttrRange(range.clock + smallerLen, largerRange.len - smallerLen, largerRange.attrs)
|
||||
array.bubblesortItem(ids, i + 1, (a, b) => a.clock - b.clock)
|
||||
}
|
||||
if (smallerLen === 0) i++
|
||||
}
|
||||
while (ids.length > 0 && ids[0].len === 0) {
|
||||
ids.splice(0, 1)
|
||||
}
|
||||
// merge items without filtering or splicing the array.
|
||||
// i is the current pointer
|
||||
// j refers to the current insert position for the pointed item
|
||||
// try to merge dels[i] into dels[j-1] or set dels[j]=dels[i]
|
||||
let i, j
|
||||
for (i = 1, j = 1; i < ids.length; i++) {
|
||||
const left = ids[j - 1]
|
||||
const right = ids[i]
|
||||
if (left.clock + left.len === right.clock && idmapAttrsEqual(left.attrs, right.attrs)) {
|
||||
ids[j - 1] = new AttrRange(left.clock, left.len + right.len, left.attrs)
|
||||
} else if (right.len !== 0) {
|
||||
if (j < i) {
|
||||
ids[j] = right
|
||||
}
|
||||
j++
|
||||
}
|
||||
}
|
||||
ids.length = ids.length === 0 ? 0 : (ids[j - 1].len === 0 ? j - 1 : j)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
*/
|
||||
export class IdMap {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {Map<number,AttrRanges<Attrs>>}
|
||||
*/
|
||||
this.clients = new Map()
|
||||
/**
|
||||
* @type {Map<string, ContentAttribute<Attrs>>}
|
||||
*/
|
||||
this.attrsH = new Map()
|
||||
/**
|
||||
* @type {Set<ContentAttribute<Attrs>>}
|
||||
*/
|
||||
this.attrs = new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(attrRange:AttrRange<Attrs>, client:number) => void} f
|
||||
*/
|
||||
forEach (f) {
|
||||
this.clients.forEach((ranges, client) => {
|
||||
ranges.getIds().forEach((range) => {
|
||||
f(range, client)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
isEmpty () {
|
||||
return this.clients.size === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @return {boolean}
|
||||
*/
|
||||
hasId (id) {
|
||||
return this.has(id.client, id.clock)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @return {boolean}
|
||||
*/
|
||||
has (client, clock) {
|
||||
const dr = this.clients.get(client)
|
||||
if (dr) {
|
||||
return findIndexInIdRanges(dr.getIds(), clock) !== null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return attributions for a slice of ids.
|
||||
*
|
||||
* @param {ID} id
|
||||
* @param {number} len
|
||||
* @return {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
sliceId (id, len) {
|
||||
return this.slice(id.client, id.clock, len)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return attributions for a slice of ids.
|
||||
*
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @return {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
slice (client, clock, len) {
|
||||
const dr = this.clients.get(client)
|
||||
/**
|
||||
* @type {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
const res = []
|
||||
if (dr) {
|
||||
/**
|
||||
* @type {Array<AttrRange<Attrs>>}
|
||||
*/
|
||||
const ranges = dr.getIds()
|
||||
let index = findRangeStartInIdRanges(ranges, clock)
|
||||
if (index !== null) {
|
||||
let prev = null
|
||||
while (index < ranges.length) {
|
||||
let r = ranges[index]
|
||||
if (r.clock < clock) {
|
||||
r = new AttrRange(clock, r.len - (clock - r.clock), r.attrs)
|
||||
}
|
||||
if (r.clock + r.len > clock + len) {
|
||||
r = new AttrRange(r.clock, clock + len - r.clock, r.attrs)
|
||||
}
|
||||
if (r.len <= 0) break
|
||||
const prevEnd = prev != null ? prev.clock + prev.len : clock
|
||||
if (prevEnd < r.clock) {
|
||||
res.push(createMaybeAttrRange(prevEnd, r.clock - prevEnd, null))
|
||||
}
|
||||
prev = r
|
||||
res.push(r)
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
if (res.length > 0) {
|
||||
const last = res[res.length - 1]
|
||||
const end = last.clock + last.len
|
||||
if (end < clock + len) {
|
||||
res.push(createMaybeAttrRange(end, clock + len - end, null))
|
||||
}
|
||||
} else {
|
||||
res.push(createMaybeAttrRange(clock, len, null))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @param {Array<ContentAttribute<Attrs>>} attrs
|
||||
*/
|
||||
add (client, clock, len, attrs) {
|
||||
if (len === 0) return
|
||||
attrs = _ensureAttrs(this, attrs)
|
||||
const ranges = this.clients.get(client)
|
||||
if (ranges == null) {
|
||||
this.clients.set(client, new AttrRanges([new AttrRange(clock, len, attrs)]))
|
||||
} else {
|
||||
ranges.add(clock, len, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
*/
|
||||
delete (client, clock, len) {
|
||||
_deleteRangeFromIdSet(this, client, clock, len)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} attrs
|
||||
* @param {T} attr
|
||||
*
|
||||
*/
|
||||
const idmapAttrsHas = (attrs, attr) => attrs.find(a => a === attr)
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} a
|
||||
* @param {Array<T>} b
|
||||
*/
|
||||
export const idmapAttrsEqual = (a, b) => a.length === b.length && a.every(v => idmapAttrsHas(b, v))
|
||||
|
||||
/**
|
||||
* Merge multiple idmaps. Ensures that there are no redundant attribution definitions (two
|
||||
* Attributions that describe the same thing).
|
||||
*
|
||||
* @template T
|
||||
* @param {Array<IdMap<T>>} ams
|
||||
* @return {IdMap<T>} A fresh IdSet
|
||||
*/
|
||||
export const mergeIdMaps = ams => {
|
||||
/**
|
||||
* Maps attribution to the attribution of the merged idmap.
|
||||
*
|
||||
* @type {Map<ContentAttribute<any>,ContentAttribute<any>>}
|
||||
*/
|
||||
const attrMapper = new Map()
|
||||
const merged = createIdMap()
|
||||
for (let amsI = 0; amsI < ams.length; amsI++) {
|
||||
ams[amsI].clients.forEach((rangesLeft, client) => {
|
||||
if (!merged.clients.has(client)) {
|
||||
// Write all missing keys from current set and all following.
|
||||
// If merged already contains `client` current ds has already been added.
|
||||
let ids = rangesLeft.getIds().slice()
|
||||
for (let i = amsI + 1; i < ams.length; i++) {
|
||||
const nextIds = ams[i].clients.get(client)
|
||||
if (nextIds) {
|
||||
array.appendTo(ids, nextIds.getIds())
|
||||
}
|
||||
}
|
||||
ids = ids.map(id => new AttrRange(id.clock, id.len, id.attrs.map(attr =>
|
||||
map.setIfUndefined(attrMapper, attr, () =>
|
||||
_ensureAttrs(merged, [attr])[0]
|
||||
)
|
||||
)))
|
||||
merged.clients.set(client, new AttrRanges(ids))
|
||||
}
|
||||
})
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} idset
|
||||
* @param {Array<ContentAttribute<any>>} attrs
|
||||
*/
|
||||
export const createIdMapFromIdSet = (idset, attrs) => {
|
||||
const idmap = createIdMap()
|
||||
// map attrs to idmap
|
||||
attrs = _ensureAttrs(idmap, attrs)
|
||||
// filter out duplicates
|
||||
/**
|
||||
* @type {Array<ContentAttribute<any>>}
|
||||
*/
|
||||
const checkedAttrs = []
|
||||
attrs.forEach(attr => {
|
||||
if (!idmapAttrsHas(checkedAttrs, attr)) {
|
||||
checkedAttrs.push(attr)
|
||||
}
|
||||
})
|
||||
idset.clients.forEach((ranges, client) => {
|
||||
const attrRanges = new AttrRanges(ranges.getIds().map(range => new AttrRange(range.clock, range.len, checkedAttrs)))
|
||||
attrRanges.sorted = true // is sorted because idset is sorted
|
||||
idmap.clients.set(client, attrRanges)
|
||||
})
|
||||
return idmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IdSet from an IdMap by stripping the attributes.
|
||||
*
|
||||
* @param {IdMap<any>} idmap
|
||||
* @return {IdSet}
|
||||
*/
|
||||
export const createIdSetFromIdMap = idmap => {
|
||||
const idset = createIdSet()
|
||||
idmap.clients.forEach((ranges, client) => {
|
||||
const idRanges = new IdRanges([])
|
||||
ranges.getIds().forEach(range => idRanges.add(range.clock, range.len))
|
||||
idset.clients.set(client, idRanges)
|
||||
})
|
||||
return idset
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently encodes IdMap to a binary form. Ensures that information is de-duplicated when
|
||||
* written. Attribute.names are referenced by id. Attributes themselfs are also referenced by id.
|
||||
*
|
||||
* @template Attr
|
||||
* @param {IdSetEncoderV1 | IdSetEncoderV2} encoder
|
||||
* @param {IdMap<Attr>} idmap
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeIdMap = (encoder, idmap) => {
|
||||
encoding.writeVarUint(encoder.restEncoder, idmap.clients.size)
|
||||
let lastWrittenClientId = 0
|
||||
/**
|
||||
* @type {Map<ContentAttribute<Attr>, number>}
|
||||
*/
|
||||
const visitedAttributions = map.create()
|
||||
/**
|
||||
* @type {Map<string, number>}
|
||||
*/
|
||||
const visitedAttrNames = map.create()
|
||||
// Ensure that the ids are written in a deterministic order (smaller clientids first)
|
||||
array.from(idmap.clients.entries())
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.forEach(([client, _idRanges]) => {
|
||||
const attrRanges = _idRanges.getIds()
|
||||
encoder.resetIdSetCurVal()
|
||||
const diff = client - lastWrittenClientId
|
||||
encoding.writeVarUint(encoder.restEncoder, diff)
|
||||
lastWrittenClientId = client
|
||||
const len = attrRanges.length
|
||||
encoding.writeVarUint(encoder.restEncoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const item = attrRanges[i]
|
||||
const attrs = item.attrs
|
||||
const attrLen = attrs.length
|
||||
encoder.writeIdSetClock(item.clock)
|
||||
encoder.writeIdSetLen(item.len)
|
||||
encoding.writeVarUint(encoder.restEncoder, attrLen)
|
||||
for (let j = 0; j < attrLen; j++) {
|
||||
const attr = attrs[j]
|
||||
const attrId = visitedAttributions.get(attr)
|
||||
if (attrId != null) {
|
||||
encoding.writeVarUint(encoder.restEncoder, attrId)
|
||||
} else {
|
||||
const newAttrId = visitedAttributions.size
|
||||
visitedAttributions.set(attr, newAttrId)
|
||||
encoding.writeVarUint(encoder.restEncoder, newAttrId)
|
||||
const attrNameId = visitedAttrNames.get(attr.name)
|
||||
// write attr.name
|
||||
if (attrNameId != null) {
|
||||
encoding.writeVarUint(encoder.restEncoder, attrNameId)
|
||||
} else {
|
||||
const newAttrNameId = visitedAttrNames.size
|
||||
encoding.writeVarUint(encoder.restEncoder, newAttrNameId)
|
||||
encoding.writeVarString(encoder.restEncoder, attr.name)
|
||||
visitedAttrNames.set(attr.name, newAttrNameId)
|
||||
}
|
||||
encoding.writeAny(encoder.restEncoder, /** @type {any} */ (attr.val))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdMap<any>} idmap
|
||||
*/
|
||||
export const encodeIdMap = idmap => {
|
||||
const encoder = new IdSetEncoderV2()
|
||||
writeIdMap(encoder, idmap)
|
||||
return encoder.toUint8Array()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSetDecoderV1 | IdSetDecoderV2} decoder
|
||||
* @return {IdMap<any>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readIdMap = decoder => {
|
||||
const idmap = new IdMap()
|
||||
const numClients = decoding.readVarUint(decoder.restDecoder)
|
||||
/**
|
||||
* @type {Array<ContentAttribute<any>>}
|
||||
*/
|
||||
const visitedAttributions = []
|
||||
/**
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
const visitedAttrNames = []
|
||||
let lastClientId = 0
|
||||
for (let i = 0; i < numClients; i++) {
|
||||
decoder.resetDsCurVal()
|
||||
const client = lastClientId + decoding.readVarUint(decoder.restDecoder)
|
||||
lastClientId = client
|
||||
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
|
||||
/**
|
||||
* @type {Array<AttrRange<any>>}
|
||||
*/
|
||||
const attrRanges = []
|
||||
for (let i = 0; i < numberOfDeletes; i++) {
|
||||
const rangeClock = decoder.readDsClock()
|
||||
const rangeLen = decoder.readDsLen()
|
||||
/**
|
||||
* @type {Array<ContentAttribute<any>>}
|
||||
*/
|
||||
const attrs = []
|
||||
const attrsLen = decoding.readVarUint(decoder.restDecoder)
|
||||
for (let j = 0; j < attrsLen; j++) {
|
||||
const attrId = decoding.readVarUint(decoder.restDecoder)
|
||||
if (attrId >= visitedAttributions.length) {
|
||||
// attrId not known yet
|
||||
const attrNameId = decoding.readVarUint(decoder.restDecoder)
|
||||
if (attrNameId >= visitedAttrNames.length) {
|
||||
visitedAttrNames.push(decoding.readVarString(decoder.restDecoder))
|
||||
}
|
||||
visitedAttributions.push(new ContentAttribute(visitedAttrNames[attrNameId], decoding.readAny(decoder.restDecoder)))
|
||||
}
|
||||
attrs.push(visitedAttributions[attrId])
|
||||
}
|
||||
attrRanges.push(new AttrRange(rangeClock, rangeLen, attrs))
|
||||
}
|
||||
idmap.clients.set(client, new AttrRanges(attrRanges))
|
||||
}
|
||||
visitedAttributions.forEach(attr => {
|
||||
idmap.attrs.add(attr)
|
||||
idmap.attrsH.set(attr.hash(), attr)
|
||||
})
|
||||
return idmap
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} data
|
||||
* @return {IdMap<any>}
|
||||
*/
|
||||
export const decodeIdMap = data => readIdMap(new IdSetDecoderV2(decoding.createDecoder(data)))
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
* @param {IdMap<Attrs>} idmap
|
||||
* @param {Array<ContentAttribute<Attrs>>} attrs
|
||||
* @return {Array<ContentAttribute<Attrs>>}
|
||||
*/
|
||||
const _ensureAttrs = (idmap, attrs) => attrs.map(attr =>
|
||||
idmap.attrs.has(attr)
|
||||
? attr
|
||||
: map.setIfUndefined(idmap.attrsH, attr.hash(), () => {
|
||||
idmap.attrs.add(attr)
|
||||
return attr
|
||||
}))
|
||||
|
||||
export const createIdMap = () => new IdMap()
|
||||
|
||||
/**
|
||||
* Remove all ranges from `exclude` from `ds`. The result is a fresh IdMap containing all ranges from `idSet` that are not
|
||||
* in `exclude`.
|
||||
*
|
||||
* @template {IdMap<any>} ISet
|
||||
* @param {ISet} set
|
||||
* @param {IdSet | IdMap<any>} exclude
|
||||
* @return {ISet}
|
||||
*/
|
||||
export const diffIdMap = (set, exclude) => {
|
||||
const diffed = _diffSet(set, exclude)
|
||||
diffed.attrs = set.attrs
|
||||
diffed.attrsH = set.attrsH
|
||||
return diffed
|
||||
}
|
||||
|
||||
export const intersectMaps = _intersectSets
|
||||
|
||||
/**
|
||||
* Filter attributes in an IdMap based on a predicate function.
|
||||
* Returns a new IdMap containing idranges that match the predicate.
|
||||
*
|
||||
* @template Attrs
|
||||
* @param {IdMap<Attrs>} idmap
|
||||
* @param {(attr: Array<ContentAttribute<Attrs>>) => boolean} predicate
|
||||
* @return {IdMap<Attrs>}
|
||||
*/
|
||||
export const filterIdMap = (idmap, predicate) => {
|
||||
const filtered = createIdMap()
|
||||
idmap.clients.forEach((ranges, client) => {
|
||||
/**
|
||||
* @type {Array<AttrRange<Attrs>>}
|
||||
*/
|
||||
const attrRanges = []
|
||||
ranges.getIds().forEach((range) => {
|
||||
if (predicate(range.attrs)) {
|
||||
const rangeCpy = range.copyWith(range.clock, range.len)
|
||||
attrRanges.push(rangeCpy)
|
||||
rangeCpy.attrs.forEach(attr => {
|
||||
filtered.attrs.add(attr)
|
||||
filtered.attrsH.set(attr.hash(), attr)
|
||||
})
|
||||
}
|
||||
})
|
||||
if (attrRanges.length > 0) {
|
||||
filtered.clients.set(client, new AttrRanges(attrRanges))
|
||||
}
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Item } from '../internals.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* Check if `parent` is a parent of `child`.
|
||||
*
|
||||
* @param {import('../ytype.js').YType} parent
|
||||
* @param {YType} parent
|
||||
* @param {Item|null} child
|
||||
* @return {Boolean} Whether `parent` is a parent of `child`.
|
||||
*
|
||||
@@ -15,7 +13,7 @@ export const isParentOf = (parent, child) => {
|
||||
if (child.parent === parent) {
|
||||
return true
|
||||
}
|
||||
child = /** @type {import('../ytype.js').YType} */ (child.parent)._item
|
||||
child = /** @type {YType} */ (child.parent)._item
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
YType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* Convenient helper to log type information.
|
||||
*
|
||||
|
||||
@@ -2,40 +2,40 @@
|
||||
* Meta API for describing Yjs documents
|
||||
*/
|
||||
|
||||
import * as idmap from './IdMap.js'
|
||||
import * as idset from './IdSet.js'
|
||||
import { IdSetEncoderV2 } from './UpdateEncoder.js'
|
||||
import { IdSetDecoderV2 } from './UpdateDecoder.js'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
|
||||
import * as ids from './ids.js'
|
||||
import { IdSetEncoderV2 } from './UpdateEncoder.js'
|
||||
import { IdSetDecoderV2 } from './UpdateDecoder.js'
|
||||
|
||||
/**
|
||||
* @typedef {{ inserts: import('./IdSet.js').IdSet, deletes: import('./IdSet.js').IdSet }} ContentIds
|
||||
* @typedef {{ inserts: IdSet, deletes: IdSet }} ContentIds
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ inserts: import('./IdMap.js').IdMap<any>, deletes: import('./IdMap.js').IdMap<any> }} ContentMap
|
||||
* @typedef {{ inserts: IdMap<any>, deletes: IdMap<any> }} ContentMap
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {import('./IdSet.js').IdSet} inserts
|
||||
* @param {import('./IdSet.js').IdSet} deletes
|
||||
* @param {IdSet} inserts
|
||||
* @param {IdSet} deletes
|
||||
*/
|
||||
export const createContentIds = (inserts = idset.createIdSet(), deletes = idset.createIdSet()) => ({ inserts, deletes })
|
||||
export const createContentIds = (inserts = ids.createIdSet(), deletes = ids.createIdSet()) => ({ inserts, deletes })
|
||||
|
||||
/**
|
||||
* @param {ContentMap} contentMap
|
||||
*/
|
||||
export const createContentIdsFromContentMap = contentMap => createContentIds(
|
||||
idmap.createIdSetFromIdMap(contentMap.inserts),
|
||||
idmap.createIdSetFromIdMap(contentMap.deletes)
|
||||
ids.createIdSetFromIdMap(contentMap.inserts),
|
||||
ids.createIdSetFromIdMap(contentMap.deletes)
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {import('./Doc.js').Doc} ydoc
|
||||
*/
|
||||
export const createContentIdsFromDoc = ydoc => createContentIds(
|
||||
idset.createInsertSetFromStructStore(ydoc.store, false),
|
||||
idset.createDeleteSetFromStructStore(ydoc.store)
|
||||
ids.createInsertSetFromStructStore(ydoc.store, false),
|
||||
ids.createDeleteSetFromStructStore(ydoc.store)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -50,47 +50,47 @@ export const createContentIdsFromDocDiff = (ydocPrev, ydocNext) =>
|
||||
* @param {ContentIds} excludeContent
|
||||
*/
|
||||
export const excludeContentIds = (content, excludeContent) =>
|
||||
createContentIds(idset.diffIdSet(content.inserts, excludeContent.inserts), idset.diffIdSet(content.deletes, excludeContent.deletes))
|
||||
createContentIds(ids.diffIdSet(content.inserts, excludeContent.inserts), ids.diffIdSet(content.deletes, excludeContent.deletes))
|
||||
|
||||
/**
|
||||
* @param {ContentMap} content
|
||||
* @param {ContentIds | ContentMap} excludeContent
|
||||
*/
|
||||
export const excludeContentMap = (content, excludeContent) => createContentMap(
|
||||
idmap.diffIdMap(content.inserts, excludeContent.inserts),
|
||||
idmap.diffIdMap(content.deletes, excludeContent.deletes)
|
||||
ids.diffIdMap(content.inserts, excludeContent.inserts),
|
||||
ids.diffIdMap(content.deletes, excludeContent.deletes)
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {Array<ContentMap>} contents
|
||||
*/
|
||||
export const mergeContentMaps = contents => createContentMap(
|
||||
idmap.mergeIdMaps(contents.map(c => c.inserts)),
|
||||
idmap.mergeIdMaps(contents.map(c => c.deletes))
|
||||
ids.mergeIdMaps(contents.map(c => c.inserts)),
|
||||
ids.mergeIdMaps(contents.map(c => c.deletes))
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {Array<ContentIds>} contents
|
||||
*/
|
||||
export const mergeContentIds = contents => createContentIds(
|
||||
idset.mergeIdSets(contents.map(c => c.inserts)),
|
||||
idset.mergeIdSets(contents.map(c => c.deletes))
|
||||
ids.mergeIdSets(contents.map(c => c.inserts)),
|
||||
ids.mergeIdSets(contents.map(c => c.deletes))
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {import('./IdMap.js').IdMap<any>} inserts
|
||||
* @param {import('./IdMap.js').IdMap<any>} deletes
|
||||
* @param {IdMap<any>} inserts
|
||||
* @param {IdMap<any>} deletes
|
||||
*/
|
||||
export const createContentMap = (inserts, deletes) => ({ inserts, deletes })
|
||||
|
||||
/**
|
||||
* @param {ContentIds} contentIds
|
||||
* @param {Array<idmap.ContentAttribute<any>>} insertAttrs
|
||||
* @param {Array<idmap.ContentAttribute<any>>} [deleteAttrs]
|
||||
* @param {Array<ContentAttribute<any>>} insertAttrs
|
||||
* @param {Array<ContentAttribute<any>>} [deleteAttrs]
|
||||
*/
|
||||
export const createContentMapFromContentIds = (contentIds, insertAttrs, deleteAttrs = insertAttrs) => createContentMap(
|
||||
idmap.createIdMapFromIdSet(contentIds.inserts, insertAttrs),
|
||||
idmap.createIdMapFromIdSet(contentIds.deletes, deleteAttrs)
|
||||
ids.createIdMapFromIdSet(contentIds.inserts, insertAttrs),
|
||||
ids.createIdMapFromIdSet(contentIds.deletes, deleteAttrs)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -98,8 +98,8 @@ export const createContentMapFromContentIds = (contentIds, insertAttrs, deleteAt
|
||||
* @param {ContentIds} contentIds
|
||||
*/
|
||||
export const writeContentIds = (encoder, contentIds) => {
|
||||
idset.writeIdSet(encoder, contentIds.inserts)
|
||||
idset.writeIdSet(encoder, contentIds.deletes)
|
||||
ids.writeIdSet(encoder, contentIds.inserts)
|
||||
ids.writeIdSet(encoder, contentIds.deletes)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,8 +118,8 @@ export const encodeContentIds = contentIds => {
|
||||
* @return {ContentIds}
|
||||
*/
|
||||
export const readContentIds = decoder => createContentIds(
|
||||
idset.readIdSet(decoder),
|
||||
idset.readIdSet(decoder)
|
||||
ids.readIdSet(decoder),
|
||||
ids.readIdSet(decoder)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -134,8 +134,8 @@ export const decodeContentIds = buf => readContentIds(new IdSetDecoderV2(decodin
|
||||
* @param {ContentMap} contentMap
|
||||
*/
|
||||
export const writeContentMap = (encoder, contentMap) => {
|
||||
idmap.writeIdMap(encoder, contentMap.inserts)
|
||||
idmap.writeIdMap(encoder, contentMap.deletes)
|
||||
ids.writeIdMap(encoder, contentMap.inserts)
|
||||
ids.writeIdMap(encoder, contentMap.deletes)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,8 +145,8 @@ export const writeContentMap = (encoder, contentMap) => {
|
||||
* @return {ContentMap} contentMap
|
||||
*/
|
||||
export const readContentMap = (decoder) => createContentMap(
|
||||
idmap.readIdMap(decoder),
|
||||
idmap.readIdMap(decoder)
|
||||
ids.readIdMap(decoder),
|
||||
ids.readIdMap(decoder)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -163,8 +163,8 @@ export const encodeContentMap = contentMap => {
|
||||
* @param {ContentMap|ContentIds} mapB
|
||||
*/
|
||||
export const intersectContentMap = (mapA, mapB) => createContentMap(
|
||||
idmap.intersectMaps(mapA.inserts, mapB.inserts),
|
||||
idmap.intersectMaps(mapA.deletes, mapB.deletes)
|
||||
ids.intersectMaps(mapA.inserts, mapB.inserts),
|
||||
ids.intersectMaps(mapA.deletes, mapB.deletes)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -172,8 +172,8 @@ export const intersectContentMap = (mapA, mapB) => createContentMap(
|
||||
* @param {ContentIds|ContentMap} setB
|
||||
*/
|
||||
export const intersectContentIds = (setA, setB) => createContentIds(
|
||||
idset.intersectSets(setA.inserts, setB.inserts),
|
||||
idset.intersectSets(setA.deletes, setB.deletes)
|
||||
ids.intersectSets(setA.inserts, setB.inserts),
|
||||
ids.intersectSets(setA.deletes, setB.deletes)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -184,7 +184,7 @@ export const decodeContentMap = buf => readContentMap(new IdSetDecoderV2(decodin
|
||||
/**
|
||||
* @todo filter by array of content instead
|
||||
* @param {ContentMap} contentMap
|
||||
* @param {(c:Array<idmap.ContentAttribute<any>>)=>boolean} insertPredicate
|
||||
* @param {(c:Array<idmap.ContentAttribute<any>>)=>boolean} deletePredicate
|
||||
* @param {(c:Array<ContentAttribute<any>>)=>boolean} insertPredicate
|
||||
* @param {(c:Array<ContentAttribute<any>>)=>boolean} deletePredicate
|
||||
*/
|
||||
export const filterContentMap = (contentMap, insertPredicate, deletePredicate) => createContentMap(idmap.filterIdMap(contentMap.inserts, insertPredicate), idmap.filterIdMap(contentMap.deletes, deletePredicate))
|
||||
export const filterContentMap = (contentMap, insertPredicate, deletePredicate) => createContentMap(ids.filterIdMap(contentMap.inserts, insertPredicate), ids.filterIdMap(contentMap.deletes, deletePredicate))
|
||||
|
||||
3
src/utils/schemas.js
Normal file
3
src/utils/schemas.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as s from 'lib0/schema'
|
||||
|
||||
export const $ydoc = /** @type {s.Schema<Doc>} */ (s.$type('y:doc', null))
|
||||
386
src/utils/transaction-helpers.js
Normal file
386
src/utils/transaction-helpers.js
Normal file
@@ -0,0 +1,386 @@
|
||||
import * as math from 'lib0/math'
|
||||
import * as error from 'lib0/error'
|
||||
import * as map from 'lib0/map'
|
||||
import * as set from 'lib0/set'
|
||||
|
||||
import { createID } from './ID.js'
|
||||
|
||||
/**
|
||||
* These modules don't require any imports.
|
||||
* These helpers are used by items to integrate themselves
|
||||
*/
|
||||
|
||||
/**
|
||||
* Perform a binary search on a sorted array
|
||||
* @param {Array<Item|GC|Skip>} structs
|
||||
* @param {number} clock
|
||||
* @return {number}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const findIndexSS = (structs, clock) => {
|
||||
let left = 0
|
||||
let right = structs.length - 1
|
||||
let mid = structs[right]
|
||||
let midclock = mid.id.clock
|
||||
if (midclock === clock) {
|
||||
return right
|
||||
}
|
||||
// @todo does it even make sense to pivot the search?
|
||||
// If a good split misses, it might actually increase the time to find the correct item.
|
||||
// Currently, the only advantage is that search with pivoting might find the item on the first try.
|
||||
let midindex = math.floor((clock / (midclock + mid.length - 1)) * right) // pivoting the search
|
||||
while (left <= right) {
|
||||
mid = structs[midindex]
|
||||
midclock = mid.id.clock
|
||||
if (midclock <= clock) {
|
||||
if (clock < midclock + mid.length) {
|
||||
return midindex
|
||||
}
|
||||
left = midindex + 1
|
||||
} else {
|
||||
right = midindex - 1
|
||||
}
|
||||
midindex = math.floor((left + right) / 2)
|
||||
}
|
||||
// Always check state before looking for a struct in StructStore
|
||||
// Therefore the case of not finding a struct is unexpected
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction?} transaction
|
||||
* @param {Array<Item|GC|Skip>} structs
|
||||
* @param {number} clock
|
||||
*/
|
||||
export const findIndexCleanStart = (transaction, structs, clock) => {
|
||||
const index = findIndexSS(structs, clock)
|
||||
const struct = structs[index]
|
||||
if (struct.id.clock < clock) {
|
||||
structs.splice(index + 1, 0, splitStruct(transaction, struct, clock - struct.id.clock))
|
||||
return index + 1
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {ID} id
|
||||
* @return {Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const getItemCleanStart = (transaction, id) => {
|
||||
const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client))
|
||||
return structs[findIndexCleanStart(transaction, structs, id.clock)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const getItemCleanEnd = (transaction, store, id) => {
|
||||
const structs = store.clients.get(id.client) || []
|
||||
const index = findIndexSS(structs, id.clock)
|
||||
const struct = structs[index]
|
||||
if (id.clock !== struct.id.clock + struct.length - 1 && struct.isItem) {
|
||||
structs.splice(index + 1, 0, /** @type {Item} */ (struct).split(transaction, id.clock - struct.id.clock + 1))
|
||||
}
|
||||
return /** @type {Item} */ (struct)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace `item` with `newitem` in store
|
||||
* @param {Transaction} tr
|
||||
* @param {GC|Item} struct
|
||||
* @param {GC|Item} newStruct
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const replaceStruct = (tr, struct, newStruct) => {
|
||||
const structs = /** @type {Array<GC|Item>} */ (tr.doc.store.clients.get(struct.id.client))
|
||||
structs[findIndexSS(structs, struct.id.clock)] = newStruct
|
||||
tr._mergeStructs.push(newStruct)
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over a range of structs
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Array<Item|GC>} structs
|
||||
* @param {number} clockStart Inclusive start
|
||||
* @param {number} len
|
||||
* @param {function(GC|Item):void} f
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const iterateStructs = (transaction, structs, clockStart, len, f) => {
|
||||
if (len === 0) {
|
||||
return
|
||||
}
|
||||
const clockEnd = clockStart + len
|
||||
let index = findIndexCleanStart(transaction, structs, clockStart)
|
||||
let struct
|
||||
do {
|
||||
struct = structs[index++]
|
||||
if (clockEnd < struct.id.clock + struct.length) {
|
||||
findIndexCleanStart(transaction, structs, clockEnd)
|
||||
}
|
||||
f(struct)
|
||||
} while (index < structs.length && structs[index].id.clock < clockEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* More generalized version of splitItem. Split leftStruct into two structs
|
||||
* @param {Transaction?} transaction
|
||||
* @param {Item|GC|Skip} leftStruct
|
||||
* @param {number} diff
|
||||
* @return {GC|Item|Skip}
|
||||
*
|
||||
* @function
|
||||
* @private
|
||||
*/
|
||||
export const splitStruct = (transaction, leftStruct, diff) => {
|
||||
if (leftStruct.isItem) {
|
||||
return /** @type {Item} */ (leftStruct).split(transaction, diff)
|
||||
} else {
|
||||
const rightItem = leftStruct.splice(diff)
|
||||
transaction?._mergeStructs.push(rightItem)
|
||||
return rightItem
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const nextID = transaction => {
|
||||
const y = transaction.doc
|
||||
return createID(y.clientID, y.store.getClock(y.clientID))
|
||||
}
|
||||
|
||||
/**
|
||||
* If `type.parent` was added in current transaction, `type` technically
|
||||
* did not change, it was just added and we should not fire events for `type`.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {YType} type
|
||||
* @param {string|null} parentSub
|
||||
*/
|
||||
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
|
||||
const item = type._item
|
||||
if (item === null || (!item.deleted && !transaction.insertSet.hasId(item.id))) {
|
||||
map.setIfUndefined(transaction.changed, type, set.create).add(parentSub)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<GC | Item | Skip>} structs
|
||||
* @param {number} pos
|
||||
* @return {number} # of merged structs
|
||||
*/
|
||||
export const tryToMergeWithLefts = (structs, pos) => {
|
||||
let right = structs[pos]
|
||||
let left = structs[pos - 1]
|
||||
let i = pos
|
||||
for (; i > 0; right = left, left = structs[--i - 1]) {
|
||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||
if (left.mergeWith(/** @type {any} */ (right))) {
|
||||
if (right.isItem && /** @type {Item} */ (right).parentSub !== null && /** @type {YType} */ (/** @type {Item} */ (right).parent)._map.get(/** @type {Item} */ (right).parentSub) === right) {
|
||||
/** @type {YType} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
const merged = pos - i
|
||||
if (merged) {
|
||||
// remove all merged structs from the array
|
||||
structs.splice(pos + 1 - merged, merged)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {IdSet} ds
|
||||
* @param {function(Item):boolean} gcFilter
|
||||
*/
|
||||
export const tryGcDeleteSet = (tr, ds, gcFilter) => {
|
||||
for (const [client, _deleteItems] of ds.clients.entries()) {
|
||||
const deleteItems = _deleteItems.getIds()
|
||||
const structs = /** @type {Array<Item>} */ (tr.doc.store.clients.get(client))
|
||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||
const deleteItem = deleteItems[di]
|
||||
const endDeleteItemClock = deleteItem.clock + deleteItem.len
|
||||
for (
|
||||
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
|
||||
si < structs.length && struct.id.clock < endDeleteItemClock;
|
||||
struct = structs[++si]
|
||||
) {
|
||||
const struct = structs[si]
|
||||
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
|
||||
break
|
||||
}
|
||||
if (struct.isItem && struct.deleted && !(struct).keep && gcFilter(/** @type {Item} */ (struct))) {
|
||||
/** @type {Item} */ (struct).gc(tr, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} ds
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
export const tryMerge = (ds, store) => {
|
||||
// try to merge deleted / gc'd items
|
||||
// merge from right to left for better efficiency and so we don't miss any merge targets
|
||||
ds.clients.forEach((_deleteItems, client) => {
|
||||
const deleteItems = _deleteItems.getIds()
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||
const deleteItem = deleteItems[di]
|
||||
// start with merging the item next to the last deleted item
|
||||
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
|
||||
for (
|
||||
let si = mostRightIndexToCheck, struct = structs[si];
|
||||
si > 0 && struct.id.clock >= deleteItem.clock;
|
||||
struct = structs[si]
|
||||
) {
|
||||
si -= 1 + tryToMergeWithLefts(structs, si)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {IdSet} idset
|
||||
* @param {function(Item):boolean} gcFilter
|
||||
*/
|
||||
export const tryGc = (tr, idset, gcFilter) => {
|
||||
tryGcDeleteSet(tr, idset, gcFilter)
|
||||
tryMerge(idset, tr.doc.store)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item | null} item
|
||||
*/
|
||||
export const cleanupContextlessFormattingGap = (transaction, item) => {
|
||||
if (!transaction.doc.cleanupFormatting) return 0
|
||||
// iterate until item.right is null or content
|
||||
while (item && item.right && (item.right.deleted || !item.right.countable)) {
|
||||
item = item.right
|
||||
}
|
||||
const attrs = new Set()
|
||||
// iterate back until a content item is found
|
||||
while (item && (item.deleted || !item.countable)) {
|
||||
if (!item.deleted && item.content.getRef() === 6) { // is a ContentFormat
|
||||
const key = /** @type {ContentFormat} */ (item.content).key
|
||||
if (attrs.has(key)) {
|
||||
item.delete(transaction)
|
||||
transaction.cleanUps.add(item.id.client, item.id.clock, item.length)
|
||||
} else {
|
||||
attrs.add(key)
|
||||
}
|
||||
}
|
||||
item = item.left
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {ContentFormat} format
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const updateCurrentAttributes = (currentAttributes, { key, value }) => {
|
||||
if (value === null) {
|
||||
currentAttributes.delete(key)
|
||||
} else {
|
||||
currentAttributes.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function after string content has been deleted in order to
|
||||
* clean up formatting Items.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} start
|
||||
* @param {Item|null} curr exclusive end, automatically iterates to the next Content Item
|
||||
* @param {Map<string,any>} startAttributes
|
||||
* @param {Map<string,any>} currAttributes
|
||||
* @return {number} The amount of formatting Items deleted.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
|
||||
if (!transaction.doc.cleanupFormatting) return 0
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let end = start
|
||||
/**
|
||||
* @type {Map<string,ContentFormat>}
|
||||
*/
|
||||
const endFormats = map.create()
|
||||
while (end && (!end.countable || end.deleted)) {
|
||||
if (!end.deleted && end.content.getRef() === 6) {
|
||||
const cf = /** @type {ContentFormat} */ (end.content)
|
||||
endFormats.set(cf.key, cf)
|
||||
}
|
||||
end = end.right
|
||||
}
|
||||
let cleanups = 0
|
||||
let reachedCurr = false
|
||||
while (start !== end) {
|
||||
if (curr === start) {
|
||||
reachedCurr = true
|
||||
}
|
||||
if (!start.deleted) {
|
||||
const content = start.content
|
||||
if (content.getRef() === 6) { // is ContentFormat
|
||||
const { key, value } = /** @type {ContentFormat} */ (content)
|
||||
const startAttrValue = startAttributes.get(key) ?? null
|
||||
if (endFormats.get(key) !== content || startAttrValue === value) {
|
||||
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
||||
start.delete(transaction)
|
||||
transaction.cleanUps.add(start.id.client, start.id.clock, start.length)
|
||||
cleanups++
|
||||
if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
|
||||
if (startAttrValue === null) {
|
||||
currAttributes.delete(key)
|
||||
} else {
|
||||
currAttributes.set(key, startAttrValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reachedCurr && !start.deleted) {
|
||||
updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content))
|
||||
}
|
||||
}
|
||||
}
|
||||
start = /** @type {Item} */ (start.right)
|
||||
}
|
||||
return cleanups
|
||||
}
|
||||
@@ -5,10 +5,16 @@ import * as error from 'lib0/error'
|
||||
import * as f from 'lib0/function'
|
||||
import * as logging from 'lib0/logging'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
import * as string from 'lib0/string'
|
||||
|
||||
import { readIdSet, writeIdSet, createIdSet, intersectSets } from './ids.js'
|
||||
|
||||
import { createID } from './ID.js'
|
||||
import { IdSetEncoderV1, IdSetEncoderV2, UpdateEncoderV1, UpdateEncoderV2 } from './UpdateEncoder.js'
|
||||
import { UpdateDecoderV1, UpdateDecoderV2 } from './UpdateDecoder.js'
|
||||
import { GC } from '../structs/GC.js'
|
||||
import {
|
||||
Item,
|
||||
ContentAny,
|
||||
ContentBinary,
|
||||
ContentDeleted,
|
||||
@@ -17,32 +23,12 @@ import {
|
||||
ContentFormat,
|
||||
ContentJSON,
|
||||
ContentString,
|
||||
ContentType,
|
||||
createID,
|
||||
decodeStateVector,
|
||||
IdSetEncoderV1,
|
||||
IdSetEncoderV2,
|
||||
GC,
|
||||
Item,
|
||||
mergeIdSets,
|
||||
readIdSet,
|
||||
readItemContent,
|
||||
Skip,
|
||||
UpdateDecoderV1,
|
||||
UpdateDecoderV2,
|
||||
UpdateEncoderV1,
|
||||
UpdateEncoderV2,
|
||||
writeIdSet,
|
||||
createIdSet,
|
||||
Doc,
|
||||
applyUpdate,
|
||||
applyUpdateV2,
|
||||
readBlockSet,
|
||||
writeBlockSet,
|
||||
encodeStateAsUpdateV2
|
||||
} from '../internals.js'
|
||||
|
||||
import * as idset from './IdSet.js'
|
||||
ContentType
|
||||
} from '../structs/Item.js'
|
||||
import {
|
||||
readItemContent
|
||||
} from '../ytype.js'
|
||||
import { Skip } from '../structs/Skip.js'
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
@@ -183,12 +169,6 @@ export class LazyStructWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Uint8Array<ArrayBuffer>>} updates
|
||||
* @return {Uint8Array<ArrayBuffer>}
|
||||
*/
|
||||
export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1)
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
* @param {typeof IdSetEncoderV1 | typeof IdSetEncoderV2} YEncoder
|
||||
@@ -294,7 +274,7 @@ export const createContentIdsFromUpdate = update => createContentIdsFromUpdateV2
|
||||
*
|
||||
* @param {Item | GC | Skip} left
|
||||
* @param {number} diff
|
||||
* @return {Item | GC}
|
||||
* @return {Item | GC | Skip}
|
||||
*/
|
||||
export const sliceStruct = (left, diff) => {
|
||||
if (left.constructor === GC) {
|
||||
@@ -319,88 +299,6 @@ export const sliceStruct = (left, diff) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This function works similarly to `readUpdateV2`.
|
||||
*
|
||||
* @param {Array<Uint8Array<ArrayBuffer>>} updates
|
||||
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
|
||||
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
|
||||
* @return {Uint8Array<ArrayBuffer>}
|
||||
*/
|
||||
export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
|
||||
if (updates.length === 1) {
|
||||
return updates[0]
|
||||
} else if (updates.length === 0) {
|
||||
return encodeStateAsUpdateV2(new Doc(), new Uint8Array([0]), new YEncoder())
|
||||
}
|
||||
const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
|
||||
const blocksets = updateDecoders.map(dec => readBlockSet(dec))
|
||||
|
||||
const mergedBlockset = blocksets[0]
|
||||
for (let i = 1; i < blocksets.length; i++) {
|
||||
mergedBlockset.insertInto(blocksets[i])
|
||||
}
|
||||
const updateEncoder = new YEncoder()
|
||||
writeBlockSet(updateEncoder, mergedBlockset)
|
||||
const dss = updateDecoders.map(decoder => readIdSet(decoder))
|
||||
const ds = mergeIdSets(dss)
|
||||
writeIdSet(updateEncoder, ds)
|
||||
return updateEncoder.toUint8Array()
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param {Uint8Array} update
|
||||
* @param {Uint8Array} sv
|
||||
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
|
||||
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
|
||||
*/
|
||||
export const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
|
||||
const state = decodeStateVector(sv)
|
||||
const encoder = new YEncoder()
|
||||
const lazyStructWriter = new LazyStructWriter(encoder)
|
||||
const decoder = new YDecoder(decoding.createDecoder(update))
|
||||
const reader = new LazyStructReader(decoder, false)
|
||||
while (reader.curr) {
|
||||
const curr = reader.curr
|
||||
const currClient = curr.id.client
|
||||
const svClock = state.get(currClient) || 0
|
||||
if (reader.curr.constructor === Skip) {
|
||||
// the first written struct shouldn't be a skip
|
||||
reader.next()
|
||||
continue
|
||||
}
|
||||
if (curr.id.clock + curr.length > svClock) {
|
||||
writeStructToLazyStructWriter(lazyStructWriter, curr, math.max(svClock - curr.id.clock, 0), 0)
|
||||
reader.next()
|
||||
while (reader.curr && reader.curr.id.client === currClient) {
|
||||
writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0, 0)
|
||||
reader.next()
|
||||
}
|
||||
} else {
|
||||
// read until something new comes up
|
||||
while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) {
|
||||
reader.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
finishLazyStructWriting(lazyStructWriter)
|
||||
// write ds
|
||||
const ds = readIdSet(decoder)
|
||||
writeIdSet(encoder, ds)
|
||||
return encoder.toUint8Array()
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @todo remove this in favor of intersectupdate
|
||||
*
|
||||
* @param {Uint8Array<ArrayBuffer>} update
|
||||
* @param {Uint8Array<ArrayBuffer>} sv
|
||||
*/
|
||||
export const diffUpdate = (update, sv) => diffUpdateV2(update, sv, UpdateDecoderV1, UpdateEncoderV1)
|
||||
|
||||
/**
|
||||
* @param {LazyStructWriter} lazyWriter
|
||||
*/
|
||||
@@ -414,11 +312,11 @@ const flushLazyStructWriter = lazyWriter => {
|
||||
|
||||
/**
|
||||
* @param {LazyStructWriter} lazyWriter
|
||||
* @param {Item | GC} struct
|
||||
* @param {Item | GC | Skip} struct
|
||||
* @param {number} offset
|
||||
* @param {number} offsetEnd
|
||||
*/
|
||||
const writeStructToLazyStructWriter = (lazyWriter, struct, offset, offsetEnd) => {
|
||||
export const writeStructToLazyStructWriter = (lazyWriter, struct, offset, offsetEnd) => {
|
||||
// flush curr if we start another client
|
||||
if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) {
|
||||
flushLazyStructWriter(lazyWriter)
|
||||
@@ -440,7 +338,7 @@ const writeStructToLazyStructWriter = (lazyWriter, struct, offset, offsetEnd) =>
|
||||
*
|
||||
* @param {LazyStructWriter} lazyWriter
|
||||
*/
|
||||
const finishLazyStructWriting = (lazyWriter) => {
|
||||
export const finishLazyStructWriting = (lazyWriter) => {
|
||||
flushLazyStructWriter(lazyWriter)
|
||||
|
||||
// this is a fresh encoder because we called flushCurr
|
||||
@@ -541,7 +439,7 @@ const createObfuscator = ({ formatting = true, subdocs = true, name = true } = {
|
||||
const c = /** @type {ContentDoc} */ (content)
|
||||
if (subdocs) {
|
||||
c.opts = {}
|
||||
c.doc.guid = i + ''
|
||||
c.guid = i + ''
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -662,7 +560,7 @@ export const intersectUpdateWithContentIdsV2 = (update, contentIds, YDecoder = U
|
||||
finishLazyStructWriting(lazyStructWriter)
|
||||
// Filter the delete set to only include entries in contentIds.deletes
|
||||
const ds = readIdSet(decoder)
|
||||
const filteredDs = idset.intersectSets(ds, deletes)
|
||||
const filteredDs = intersectSets(ds, deletes)
|
||||
writeIdSet(encoder, filteredDs)
|
||||
return encoder.toUint8Array()
|
||||
}
|
||||
@@ -676,23 +574,3 @@ export const intersectUpdateWithContentIdsV2 = (update, contentIds, YDecoder = U
|
||||
*/
|
||||
export const intersectUpdateWithContentIds = (update, contentIds) =>
|
||||
intersectUpdateWithContentIdsV2(update, contentIds, UpdateDecoderV1, UpdateEncoderV1)
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
* @param {import('./Doc.js').DocOpts} opts
|
||||
*/
|
||||
export const createDocFromUpdate = (update, opts = {}) => {
|
||||
const ydoc = new Doc(opts)
|
||||
applyUpdate(ydoc, update)
|
||||
return ydoc
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
* @param {import('./Doc.js').DocOpts} opts
|
||||
*/
|
||||
export const createDocFromUpdateV2 = (update, opts = {}) => {
|
||||
const ydoc = new Doc(opts)
|
||||
applyUpdateV2(ydoc, update)
|
||||
return ydoc
|
||||
}
|
||||
|
||||
309
src/ytype.js
309
src/ytype.js
@@ -1,38 +1,38 @@
|
||||
import {
|
||||
cleanupFormattingGap,
|
||||
createIdSet,
|
||||
removeEventHandlerListener,
|
||||
callEventHandlerListeners,
|
||||
addEventHandlerListener,
|
||||
createEventHandler,
|
||||
getState,
|
||||
isVisible,
|
||||
ContentType,
|
||||
createID,
|
||||
ContentAny,
|
||||
ContentFormat,
|
||||
ContentBinary,
|
||||
ContentJSON,
|
||||
ContentDeleted,
|
||||
ContentString,
|
||||
ContentEmbed,
|
||||
getItemCleanStart,
|
||||
noAttributionsManager,
|
||||
transact,
|
||||
ContentDoc, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, createAttributionFromAttributionItems, AbstractAttributionManager // eslint-disable-line
|
||||
} from './internals.js'
|
||||
|
||||
import * as contentType from './structs/ContentType.js'
|
||||
import * as traits from 'lib0/traits'
|
||||
import * as delta from 'lib0/delta'
|
||||
import * as binary from 'lib0/binary'
|
||||
import * as array from 'lib0/array'
|
||||
import * as map from 'lib0/map'
|
||||
import * as iterator from 'lib0/iterator'
|
||||
import * as delta from 'lib0/delta'
|
||||
import * as error from 'lib0/error'
|
||||
import * as math from 'lib0/math'
|
||||
import * as iterator from 'lib0/iterator'
|
||||
import * as log from 'lib0/logging'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
import * as object from 'lib0/object'
|
||||
import * as s from 'lib0/schema'
|
||||
import * as traits from 'lib0/traits'
|
||||
import {
|
||||
Item,
|
||||
ContentAny,
|
||||
ContentBinary,
|
||||
ContentDeleted,
|
||||
ContentEmbed,
|
||||
ContentFormat,
|
||||
ContentJSON,
|
||||
ContentString,
|
||||
ContentType,
|
||||
YXmlFragmentRefID,
|
||||
YXmlElementRefID,
|
||||
YXmlHookRefID,
|
||||
ContentDoc,
|
||||
createContentDocFromDoc
|
||||
} from './structs/Item.js'
|
||||
import { noAttributionsManager } from './utils/attribution-manager-helpers.js'
|
||||
import { removeEventHandlerListener, callEventHandlerListeners, addEventHandlerListener, createEventHandler } from './utils/EventHandler.js'
|
||||
import { createID } from './utils/ID.js'
|
||||
import { createIdSet } from './utils/ids.js'
|
||||
import { getItemCleanStart, cleanupFormattingGap } from './utils/transaction-helpers.js'
|
||||
import { transact } from './utils/Transaction.js'
|
||||
import { YEvent } from './utils/YEvent.js'
|
||||
import { $ydoc } from './utils/schemas.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object<string,any>|Array<any>|number|null|string|Uint8Array|BigInt|YType<any>} YValue
|
||||
@@ -45,6 +45,44 @@ export const warnPrematureAccess = () => { log.warn('Invalid access: Add Yjs typ
|
||||
|
||||
const maxSearchMarker = 80
|
||||
|
||||
/**
|
||||
* @todo SHOULD NOT RETURN AN OBJECT!
|
||||
* @param {Array<ContentAttribute<any>>?} attrs
|
||||
* @param {boolean} deleted - whether the attributed item is deleted
|
||||
* @return {Attribution?}
|
||||
*/
|
||||
export const createAttributionFromAttributionItems = (attrs, deleted) => {
|
||||
if (attrs == null) {
|
||||
return null
|
||||
}
|
||||
/**
|
||||
* @type {Attribution}
|
||||
*/
|
||||
const attribution = {}
|
||||
if (deleted) {
|
||||
attribution.delete = []
|
||||
} else {
|
||||
attribution.insert = []
|
||||
}
|
||||
attrs.forEach(attr => {
|
||||
switch (attr.name) {
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'insert':
|
||||
case 'delete': {
|
||||
// needs to be non-ambiguous: don't add existing attr if it doesn't match the actual status
|
||||
attribution[attr.name]?.push(attr.val)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (attr.name[0] !== '_') {
|
||||
/** @type {any} */ (attribution)[attr.name] = attr.val
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return attribution
|
||||
}
|
||||
|
||||
/**
|
||||
* A unique timestamp that identifies each marker.
|
||||
*
|
||||
@@ -143,7 +181,7 @@ export class ItemTextListPosition {
|
||||
const rightLen = this.am.contentLength(item)
|
||||
if (length < rightLen) {
|
||||
/**
|
||||
* @type {Array<import('./internals.js').AttributedContent<any>>}
|
||||
* @type {Array<AttributedContent<any>>}
|
||||
*/
|
||||
const contents = []
|
||||
this.am.readContent(contents, item.id.client, item.id.clock, item.deleted, item.content, 0)
|
||||
@@ -204,7 +242,7 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
|
||||
negatedAttributes.forEach((val, key) => {
|
||||
const left = currPos.left
|
||||
const right = currPos.right
|
||||
const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
||||
const nextFormat = new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
||||
nextFormat.integrate(transaction, 0)
|
||||
currPos.right = nextFormat
|
||||
currPos.forward()
|
||||
@@ -270,7 +308,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
|
||||
// save negated attribute (set null if currentVal undefined)
|
||||
negatedAttributes.set(key, currentVal)
|
||||
const { left, right } = currPos
|
||||
currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
||||
currPos.right = new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
||||
currPos.right.integrate(transaction, 0)
|
||||
currPos.forward()
|
||||
}
|
||||
@@ -302,7 +340,7 @@ export const insertContent = (transaction, parent, currPos, content, attributes)
|
||||
if (parent._searchMarker) {
|
||||
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
|
||||
}
|
||||
right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
|
||||
right = new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
|
||||
right.integrate(transaction, 0)
|
||||
currPos.right = right
|
||||
currPos.index = index
|
||||
@@ -327,14 +365,14 @@ export const insertContentHelper = (transaction, parent, currPos, insert, attrib
|
||||
if (first instanceof YType) {
|
||||
insertContent(transaction, parent, currPos, new ContentType(first), attributes)
|
||||
i++
|
||||
} else if (first instanceof Doc) {
|
||||
insertContent(transaction, parent, currPos, new ContentDoc(first), attributes)
|
||||
} else if ($ydoc.check(first)) {
|
||||
insertContent(transaction, parent, currPos, createContentDocFromDoc(first), attributes)
|
||||
i++
|
||||
} else {
|
||||
// insert "any" content
|
||||
// compute slice len
|
||||
let j = i + 1
|
||||
for (; j < insert.length && !(insert[j] instanceof YType || insert[j] instanceof Doc); j++) { /* nop */ }
|
||||
for (; j < insert.length && !(insert[j] instanceof YType || $ydoc.check(insert[j])); j++) { /* nop */ }
|
||||
insertContent(transaction, parent, currPos, new ContentAny((i === 0 && j === insert.length) ? insert : insert.slice(i, j)), attributes)
|
||||
i = j
|
||||
}
|
||||
@@ -365,7 +403,7 @@ export const deleteText = (transaction, currPos, length) => {
|
||||
item.delete(transaction)
|
||||
} else if (currPos.am !== noAttributionsManager) {
|
||||
/**
|
||||
* @type {Array<import('./internals.js').AttributedContent<any>>}
|
||||
* @type {Array<AttributedContent<any>>}
|
||||
*/
|
||||
const contents = []
|
||||
currPos.am.readContent(contents, item.id.client, item.id.clock, true, item.content, 0)
|
||||
@@ -640,7 +678,7 @@ export class YType {
|
||||
* @private
|
||||
*/
|
||||
this._content = /** @type {delta.DeltaBuilderAny} */ (delta.create())
|
||||
this._legacyTypeRef = this.name == null ? contentType.YXmlFragmentRefID : contentType.YXmlElementRefID
|
||||
this._legacyTypeRef = this.name == null ? YXmlFragmentRefID : YXmlElementRefID
|
||||
/**
|
||||
* @type {Array<ArraySearchMarker>|null}
|
||||
*/
|
||||
@@ -783,10 +821,10 @@ export class YType {
|
||||
*
|
||||
* @param {AbstractAttributionManager} am
|
||||
* @param {Object} [opts]
|
||||
* @param {import('./utils/IdSet.js').IdSet?} [opts.itemsToRender]
|
||||
* @param {IdSet?} [opts.itemsToRender]
|
||||
* @param {boolean} [opts.retainInserts] - if true, retain rendered inserts with attributions
|
||||
* @param {boolean} [opts.retainDeletes] - if true, retain rendered+attributed deletes only
|
||||
* @param {import('./utils/IdSet.js').IdSet?} [opts.deletedItems] - used for computing prevItem in attributes
|
||||
* @param {IdSet?} [opts.deletedItems] - used for computing prevItem in attributes
|
||||
* @param {Map<YType,Set<string|null>>|null} [opts.modified] - set of types that should be rendered as modified children
|
||||
* @param {Deep} [opts.deep] - render child types as delta
|
||||
* @return {Deep extends true ? delta.Delta<DConf> : delta.Delta<DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
|
||||
@@ -829,7 +867,7 @@ export class YType {
|
||||
*/
|
||||
const previousAttributes = {} // The value before changes
|
||||
/**
|
||||
* @type {Array<import('./internals.js').AttributedContent<any>>}
|
||||
* @type {Array<AttributedContent<any>>}
|
||||
*/
|
||||
const cs = []
|
||||
for (let item = this._start; item !== null; cs.length = 0) {
|
||||
@@ -978,7 +1016,7 @@ export class YType {
|
||||
// # Update Attributions
|
||||
if (attribution != null || object.hasProperty(previousUnattributedAttributes, key)) {
|
||||
/**
|
||||
* @type {import('./utils/AttributionManager.js').Attribution}
|
||||
* @type {Attribution}
|
||||
*/
|
||||
const formattingAttribution = object.assign({}, d.usedAttribution)
|
||||
const changedAttributedAttributes = /** @type {{ [key: string]: Array<any> }} */ (formattingAttribution.format = object.assign({}, formattingAttribution.format ?? {}))
|
||||
@@ -1428,8 +1466,8 @@ export class YType {
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(this._legacyTypeRef)
|
||||
switch (this._legacyTypeRef) {
|
||||
case contentType.YXmlElementRefID:
|
||||
case contentType.YXmlHookRefID: {
|
||||
case YXmlElementRefID:
|
||||
case YXmlHookRefID: {
|
||||
encoder.writeKey(this.name)
|
||||
break
|
||||
}
|
||||
@@ -1445,20 +1483,6 @@ export class YType {
|
||||
export const $ytype = _dconf => s.$instanceOf(YType)
|
||||
export const $ytypeAny = s.$instanceOf(YType)
|
||||
|
||||
/**
|
||||
* @param {import('./utils/UpdateDecoder.js').UpdateDecoderV1 | import('./utils/UpdateDecoder.js').UpdateDecoderV2} decoder
|
||||
* @return {YType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYType = decoder => {
|
||||
const typeRef = decoder.readTypeRef()
|
||||
const ytype = new YType(typeRef === contentType.YXmlElementRefID || typeRef === contentType.YXmlHookRefID ? decoder.readKey() : null)
|
||||
ytype._legacyTypeRef = typeRef
|
||||
return ytype
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} a
|
||||
* @param {any} b
|
||||
@@ -1569,7 +1593,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
let jsonContent = []
|
||||
const packJsonContent = () => {
|
||||
if (jsonContent.length > 0) {
|
||||
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
|
||||
left = new Item(createID(ownClientId, store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
|
||||
left.integrate(transaction, 0)
|
||||
jsonContent = []
|
||||
}
|
||||
@@ -1594,16 +1618,15 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
switch (c.constructor) {
|
||||
case Uint8Array:
|
||||
case ArrayBuffer:
|
||||
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
|
||||
left.integrate(transaction, 0)
|
||||
break
|
||||
case Doc:
|
||||
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c)))
|
||||
left = new Item(createID(ownClientId, store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
|
||||
left.integrate(transaction, 0)
|
||||
break
|
||||
default:
|
||||
if (c instanceof YType) {
|
||||
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(/** @type {any} */ (c)))
|
||||
if ($ydoc.check(c)) {
|
||||
left = new Item(createID(ownClientId, store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, createContentDocFromDoc(/** @type {Doc} */ (c)))
|
||||
left.integrate(transaction, 0)
|
||||
} else if (c instanceof YType) {
|
||||
left = new Item(createID(ownClientId, store.getClock(ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(/** @type {any} */ (c)))
|
||||
left.integrate(transaction, 0)
|
||||
} else {
|
||||
throw new Error('Unexpected content type in insert operation')
|
||||
@@ -1784,18 +1807,17 @@ export const typeMapSet = (transaction, parent, key, value) => {
|
||||
case Uint8Array:
|
||||
content = new ContentBinary(/** @type {Uint8Array} */ (value))
|
||||
break
|
||||
case Doc:
|
||||
content = new ContentDoc(/** @type {Doc} */ (value))
|
||||
break
|
||||
default:
|
||||
if (value instanceof YType) {
|
||||
if ($ydoc.check(value)) {
|
||||
content = createContentDocFromDoc(/** @type {Doc} */ (value))
|
||||
} else if (value instanceof YType) {
|
||||
content = new ContentType(/** @type {any} */ (value))
|
||||
} else {
|
||||
throw new Error('Unexpected content type')
|
||||
}
|
||||
}
|
||||
}
|
||||
new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0)
|
||||
new Item(createID(ownClientId, doc.store.getClock(ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1846,11 +1868,11 @@ export const typeMapGetAll = (parent) => {
|
||||
* @param {TypeDelta} d
|
||||
* @param {YType} parent
|
||||
* @param {Set<string|null>?} attrsToRender
|
||||
* @param {import('./internals.js').AbstractAttributionManager} am
|
||||
* @param {AbstractAttributionManager} am
|
||||
* @param {boolean} deep
|
||||
* @param {Set<YType>|Map<YType,any>|null} [modified] - set of types that should be rendered as modified children
|
||||
* @param {import('./utils/IdSet.js').IdSet?} [deletedItems]
|
||||
* @param {import('./utils/IdSet.js').IdSet?} [itemsToRender]
|
||||
* @param {IdSet?} [deletedItems]
|
||||
* @param {IdSet?} [itemsToRender]
|
||||
* @param {any} [opts]
|
||||
* @param {any} [optsAll]
|
||||
*
|
||||
@@ -1865,7 +1887,7 @@ export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, de
|
||||
*/
|
||||
const renderAttrs = (item, key) => {
|
||||
/**
|
||||
* @type {Array<import('./internals.js').AttributedContent<any>>}
|
||||
* @type {Array<AttributedContent>}
|
||||
*/
|
||||
const cs = []
|
||||
am.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
|
||||
@@ -1914,6 +1936,17 @@ export const typeMapHas = (parent, key) => {
|
||||
return val !== undefined && !val.deleted
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Item} item
|
||||
* @param {Snapshot|undefined} snapshot
|
||||
*
|
||||
* @protected
|
||||
* @function
|
||||
*/
|
||||
export const isVisible = (item, snapshot) => snapshot === undefined
|
||||
? !item.deleted
|
||||
: snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !snapshot.ds.hasId(item.id)
|
||||
|
||||
/**
|
||||
* @param {YType<any>} parent
|
||||
* @param {string} key
|
||||
@@ -1970,3 +2003,127 @@ export const createMapIterator = type => {
|
||||
type.doc ?? warnPrematureAccess()
|
||||
return iterator.iteratorFilter(type._map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentType}
|
||||
*/
|
||||
export const readContentType = decoder => new ContentType(readYType(decoder))
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentString}
|
||||
*/
|
||||
export const readContentString = decoder => new ContentString(decoder.readString())
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
export const readContentJSON = decoder => {
|
||||
const len = decoder.readLen()
|
||||
const cs = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = decoder.readString()
|
||||
if (c === 'undefined') {
|
||||
cs.push(undefined)
|
||||
} else {
|
||||
cs.push(JSON.parse(c))
|
||||
}
|
||||
}
|
||||
return new ContentJSON(cs)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
export const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON())
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
export const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON())
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentDoc}
|
||||
*/
|
||||
export const readContentDoc = decoder => new ContentDoc(decoder.readString(), decoder.readAny())
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
export const readContentAny = decoder => {
|
||||
const len = decoder.readLen()
|
||||
const cs = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
cs.push(decoder.readAny())
|
||||
}
|
||||
return new ContentAny(cs)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
export const readContentBinary = decoder => new ContentBinary(decoder.readBuf())
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen())
|
||||
|
||||
/**
|
||||
* A lookup map for reading Item content.
|
||||
*
|
||||
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractContent>}
|
||||
*/
|
||||
export const contentRefs = [
|
||||
() => { error.unexpectedCase() }, // GC is not ItemContent
|
||||
readContentDeleted, // 1
|
||||
readContentJSON, // 2
|
||||
readContentBinary, // 3
|
||||
readContentString, // 4
|
||||
readContentEmbed, // 5
|
||||
readContentFormat, // 6
|
||||
readContentType, // 7
|
||||
readContentAny, // 8
|
||||
readContentDoc, // 9
|
||||
() => { error.unexpectedCase() } // 10 - Skip is not ItemContent
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @param {number} info
|
||||
*/
|
||||
export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {YType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYType = decoder => {
|
||||
const typeRef = decoder.readTypeRef()
|
||||
const ytype = new YType(typeRef === YXmlElementRefID || typeRef === YXmlHookRefID ? decoder.readKey() : null)
|
||||
ytype._legacyTypeRef = typeRef
|
||||
return ytype
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
{
|
||||
"imports": {
|
||||
"@y/y": "./src/index.js",
|
||||
"@y/y/internals": "./src/internals.js",
|
||||
"@y/y/meta": "./src/utils/meta.js",
|
||||
"@y/y/testHelper": "./tests/testHelper.js",
|
||||
"@y/y/package.json": "./package.json",
|
||||
@@ -22,6 +21,7 @@
|
||||
"lib0/crypto/jwt": "./node_modules/lib0/src/crypto/jwt.js",
|
||||
"lib0/crypto/aes-gcm": "./node_modules/lib0/src/crypto/aes-gcm.js",
|
||||
"lib0/delta": "./node_modules/lib0/src/delta/delta.js",
|
||||
"lib0/delta/transformer": "./node_modules/lib0/src/delta/transformer.js",
|
||||
"lib0/crypto/ecdsa": "./node_modules/lib0/src/crypto/ecdsa.js",
|
||||
"lib0/crypto/rsa-oaep": "./node_modules/lib0/src/crypto/rsa-oaep.js",
|
||||
"lib0/hash/rabin": "./node_modules/lib0/src/hash/rabin.js",
|
||||
@@ -86,6 +86,7 @@
|
||||
"lib0/crypto/jwt": "./node_modules/lib0/src/crypto/jwt.js",
|
||||
"lib0/crypto/aes-gcm": "./node_modules/lib0/src/crypto/aes-gcm.js",
|
||||
"lib0/delta": "./node_modules/lib0/src/delta/delta.js",
|
||||
"lib0/delta/transformer": "./node_modules/lib0/src/delta/transformer.js",
|
||||
"lib0/crypto/ecdsa": "./node_modules/lib0/src/crypto/ecdsa.js",
|
||||
"lib0/crypto/rsa-oaep": "./node_modules/lib0/src/crypto/rsa-oaep.js",
|
||||
"lib0/hash/rabin": "./node_modules/lib0/src/hash/rabin.js",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as t from 'lib0/testing'
|
||||
import * as idmap from '../src/utils/IdMap.js'
|
||||
import * as ids from '../src/utils/ids.js'
|
||||
import * as prng from 'lib0/prng'
|
||||
import * as math from 'lib0/math'
|
||||
import { compareIdmaps as compareIdMaps, createIdMap, ID, createRandomIdSet, createRandomIdMap, createContentAttribute } from './testHelper.js'
|
||||
import * as YY from '../src/internals.js'
|
||||
import * as YY from '../src/index.js'
|
||||
import * as time from 'lib0/time'
|
||||
|
||||
/**
|
||||
@@ -80,16 +80,16 @@ export const testRepeatMergingMultipleIdMaps = tc => {
|
||||
const clients = 4
|
||||
const clockRange = 5
|
||||
/**
|
||||
* @type {Array<idmap.IdMap<number>>}
|
||||
* @type {Array<IdMap<number>>}
|
||||
*/
|
||||
const sets = []
|
||||
for (let i = 0; i < 3; i++) {
|
||||
sets.push(createRandomIdMap(tc.prng, clients, clockRange, [1, 2, 3]))
|
||||
}
|
||||
const merged = idmap.mergeIdMaps(sets)
|
||||
const mergedReverse = idmap.mergeIdMaps(sets.reverse())
|
||||
const merged = ids.mergeIdMaps(sets)
|
||||
const mergedReverse = ids.mergeIdMaps(sets.reverse())
|
||||
compareIdMaps(merged, mergedReverse)
|
||||
const composed = idmap.createIdMap()
|
||||
const composed = ids.createIdMap()
|
||||
for (let iclient = 0; iclient < clients; iclient++) {
|
||||
for (let iclock = 0; iclock < clockRange + 42; iclock++) {
|
||||
const mergedHas = merged.hasId(new ID(iclient, iclock))
|
||||
@@ -115,9 +115,9 @@ export const testRepeatRandomDiffing = tc => {
|
||||
const attrs = [1, 2, 3]
|
||||
const idset1 = createRandomIdMap(tc.prng, clients, clockRange, attrs)
|
||||
const idset2 = createRandomIdMap(tc.prng, clients, clockRange, attrs)
|
||||
const merged = idmap.mergeIdMaps([idset1, idset2])
|
||||
const e1 = idmap.diffIdMap(idset1, idset2)
|
||||
const e2 = idmap.diffIdMap(merged, idset2)
|
||||
const merged = ids.mergeIdMaps([idset1, idset2])
|
||||
const e1 = ids.diffIdMap(idset1, idset2)
|
||||
const e2 = ids.diffIdMap(merged, idset2)
|
||||
compareIdMaps(e1, e2)
|
||||
const copy = YY.decodeIdMap(YY.encodeIdMap(e1))
|
||||
compareIdMaps(e1, copy)
|
||||
@@ -133,11 +133,11 @@ export const testRepeatRandomDiffing2 = tc => {
|
||||
const idmap1 = createRandomIdMap(tc.prng, clients, clockRange, attrs)
|
||||
const idmap2 = createRandomIdMap(tc.prng, clients, clockRange, attrs)
|
||||
const idsExclude = createRandomIdSet(tc.prng, clients, clockRange)
|
||||
const merged = idmap.mergeIdMaps([idmap1, idmap2])
|
||||
const mergedExcluded = idmap.diffIdMap(merged, idsExclude)
|
||||
const e1 = idmap.diffIdMap(idmap1, idsExclude)
|
||||
const e2 = idmap.diffIdMap(idmap2, idsExclude)
|
||||
const excludedMerged = idmap.mergeIdMaps([e1, e2])
|
||||
const merged = ids.mergeIdMaps([idmap1, idmap2])
|
||||
const mergedExcluded = ids.diffIdMap(merged, idsExclude)
|
||||
const e1 = ids.diffIdMap(idmap1, idsExclude)
|
||||
const e2 = ids.diffIdMap(idmap2, idsExclude)
|
||||
const excludedMerged = ids.mergeIdMaps([e1, e2])
|
||||
compareIdMaps(mergedExcluded, excludedMerged)
|
||||
const copy = YY.decodeIdMap(YY.encodeIdMap(mergedExcluded))
|
||||
compareIdMaps(mergedExcluded, copy)
|
||||
@@ -153,9 +153,9 @@ export const testRepeatRandomDeletes = tc => {
|
||||
const client = Array.from(idset.clients.keys())[0]
|
||||
const clock = prng.int31(tc.prng, 0, clockRange)
|
||||
const len = prng.int31(tc.prng, 0, math.round((clockRange - clock) * 1.2)) // allow exceeding range to cover more edge cases
|
||||
const idsetOfDeletes = idmap.createIdMap()
|
||||
const idsetOfDeletes = ids.createIdMap()
|
||||
idsetOfDeletes.add(client, clock, len, [])
|
||||
const diffed = idmap.diffIdMap(idset, idsetOfDeletes)
|
||||
const diffed = ids.diffIdMap(idset, idsetOfDeletes)
|
||||
idset.delete(client, clock, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
t.assert(!idset.has(client, clock + i))
|
||||
@@ -171,7 +171,7 @@ export const testRepeatRandomIntersects = tc => {
|
||||
const clockRange = 100
|
||||
const ids1 = createRandomIdMap(tc.prng, clients, clockRange, [1])
|
||||
const ids2 = createRandomIdMap(tc.prng, clients, clockRange, ['two'])
|
||||
const intersected = idmap.intersectMaps(ids1, ids2)
|
||||
const intersected = ids.intersectMaps(ids1, ids2)
|
||||
for (let client = 0; client < clients; client++) {
|
||||
for (let clock = 0; clock < clockRange; clock++) {
|
||||
t.assert((ids1.has(client, clock) && ids2.has(client, clock)) === intersected.has(client, clock))
|
||||
@@ -191,8 +191,8 @@ export const testRepeatRandomIntersects = tc => {
|
||||
t.assert(attrs?.length === expectedAttrs?.length)
|
||||
}
|
||||
}
|
||||
const diffed1 = idmap.diffIdMap(ids1, ids2)
|
||||
const altDiffed1 = idmap.diffIdMap(ids1, intersected)
|
||||
const diffed1 = ids.diffIdMap(ids1, ids2)
|
||||
const altDiffed1 = ids.diffIdMap(ids1, intersected)
|
||||
compareIdMaps(diffed1, altDiffed1)
|
||||
}
|
||||
|
||||
@@ -209,8 +209,8 @@ export const testUserAttributionEncodingBenchmark = tc => {
|
||||
const currentTime = time.getUnixTime()
|
||||
const ydoc = new YY.Doc()
|
||||
ydoc.on('afterTransaction', tr => {
|
||||
idmap.insertIntoIdMap(attributions, idmap.createIdMapFromIdSet(tr.insertSet, [createContentAttribute('insert', 'userX'), createContentAttribute('insertAt', currentTime)]))
|
||||
idmap.insertIntoIdMap(attributions, idmap.createIdMapFromIdSet(tr.deleteSet, [createContentAttribute('delete', 'userX'), createContentAttribute('deleteAt', currentTime)]))
|
||||
ids.insertIntoIdMap(attributions, ids.createIdMapFromIdSet(tr.insertSet, [createContentAttribute('insert', 'userX'), createContentAttribute('insertAt', currentTime)]))
|
||||
ids.insertIntoIdMap(attributions, ids.createIdMapFromIdSet(tr.deleteSet, [createContentAttribute('delete', 'userX'), createContentAttribute('deleteAt', currentTime)]))
|
||||
})
|
||||
const ytext = ydoc.get()
|
||||
const N = 10000
|
||||
@@ -229,7 +229,7 @@ export const testUserAttributionEncodingBenchmark = tc => {
|
||||
/**
|
||||
* @todo I can optimize size by encoding only the differences to the prev item.
|
||||
*/
|
||||
const encAttributions = idmap.encodeIdMap(attributions)
|
||||
const encAttributions = ids.encodeIdMap(attributions)
|
||||
t.info('encoded size: ' + encAttributions.byteLength)
|
||||
t.info('size per change: ' + math.floor((encAttributions.byteLength / N) * 100) / 100 + ' bytes')
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as t from 'lib0/testing'
|
||||
import * as d from '../src/utils/IdSet.js'
|
||||
import * as d from '../src/utils/ids.js'
|
||||
import * as math from 'lib0/math'
|
||||
import * as prng from 'lib0/prng'
|
||||
import { compareIdSets, createRandomIdSet, ID } from './testHelper.js'
|
||||
@@ -10,7 +10,7 @@ import { compareIdSets, createRandomIdSet, ID } from './testHelper.js'
|
||||
const simpleConstructIdSet = ops => {
|
||||
const idset = d.createIdSet()
|
||||
ops.forEach(op => {
|
||||
d.addToIdSet(idset, op[0], op[1], op[2])
|
||||
idset.add(op[0], op[1], op[2])
|
||||
})
|
||||
return idset
|
||||
}
|
||||
@@ -198,7 +198,7 @@ export const testRepeatMergingMultipleIdsets = tc => {
|
||||
const oneHas = idss.some(ids => ids.hasId(new ID(iclient, iclock)))
|
||||
t.assert(mergedHas === oneHas)
|
||||
if (oneHas) {
|
||||
d.addToIdSet(composed, iclient, iclock, 1)
|
||||
composed.add(iclient, iclock, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as t from 'lib0/testing'
|
||||
|
||||
import {
|
||||
contentRefs,
|
||||
readContentBinary,
|
||||
readContentDeleted,
|
||||
readContentBinary,
|
||||
readContentString,
|
||||
readContentJSON,
|
||||
readContentEmbed,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
readContentFormat,
|
||||
readContentAny,
|
||||
readContentDoc
|
||||
} from '../src/internals.js'
|
||||
} from '../src/ytype.js'
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import * as prng from 'lib0/prng'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
@@ -5,13 +6,10 @@ import * as decoding from 'lib0/decoding'
|
||||
import * as syncProtocol from '@y/protocols/sync'
|
||||
import * as object from 'lib0/object'
|
||||
import * as map from 'lib0/map'
|
||||
import * as Y from '../src/index.js'
|
||||
import * as math from 'lib0/math'
|
||||
import * as list from 'lib0/list'
|
||||
import * as delta from 'lib0/delta'
|
||||
import {
|
||||
createIdSet, createIdMap, addToIdSet, encodeIdMap
|
||||
} from '../src/internals.js'
|
||||
import { createIdSet, createIdMap, encodeIdMap } from '../src/utils/ids.js'
|
||||
|
||||
export * from '../src/index.js'
|
||||
|
||||
@@ -121,14 +119,14 @@ export class TestYInstance extends Y.Doc {
|
||||
if (!this.tc.onlineConns.has(this)) {
|
||||
this.tc.onlineConns.add(this)
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeSyncStep1(encoder, this)
|
||||
syncProtocol.writeSyncStep1(encoder, /** @type {any} */ (this))
|
||||
// publish SyncStep1
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder))
|
||||
this.tc.onlineConns.forEach(remoteYInstance => {
|
||||
if (remoteYInstance !== this) {
|
||||
// remote instance sends instance to this instance
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeSyncStep1(encoder, remoteYInstance)
|
||||
syncProtocol.writeSyncStep1(encoder, /** @type {any} */ (remoteYInstance))
|
||||
this._receive(encoding.toUint8Array(encoder), remoteYInstance)
|
||||
}
|
||||
})
|
||||
@@ -202,7 +200,7 @@ export class TestConnector {
|
||||
const encoder = encoding.createEncoder()
|
||||
// console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver))
|
||||
// do not publish data created when this function is executed (could be ss2 or update message)
|
||||
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc)
|
||||
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, /** @type {any} */ (receiver), receiver.tc)
|
||||
if (encoding.length(encoder) > 0) {
|
||||
// send reply message
|
||||
sender._receive(encoding.toUint8Array(encoder), receiver)
|
||||
@@ -319,7 +317,7 @@ export const compareIdSets = (idSet1, idSet2) => {
|
||||
t.assert(items2 !== undefined && items1.length === items2.length)
|
||||
for (let i = 0; i < items1.length; i++) {
|
||||
const di1 = items1[i]
|
||||
const di2 = /** @type {Array<import('../src/utils/IdSet.js').IdRange>} */ (items2)[i]
|
||||
const di2 = /** @type {Array<import('../src/utils/ids.js').IdRange>} */ (items2)[i]
|
||||
t.assert(di1.clock === di2.clock && di1.len === di2.len)
|
||||
}
|
||||
}
|
||||
@@ -382,7 +380,7 @@ export const compareIdmaps = (idmap1, idmap2) => {
|
||||
t.assert(items2 !== undefined && items1.length === items2.length)
|
||||
for (let i = 0; i < items1.length; i++) {
|
||||
const di1 = items1[i]
|
||||
const di2 = /** @type {Array<import('../src/utils/IdMap.js').AttrRange<T>>} */ (items2)[i]
|
||||
const di2 = /** @type {Array<import('../src/utils/ids.js').AttrRange<T>>} */ (items2)[i]
|
||||
t.assert(di1.clock === di2.clock && di1.len === di2.len && _idmapAttrsEqual(di1.attrs, di2.attrs))
|
||||
}
|
||||
}
|
||||
@@ -403,7 +401,7 @@ export const createRandomIdSet = (gen, clients, clockRange) => {
|
||||
const client = prng.uint32(gen, 0, clients - 1)
|
||||
const clockStart = prng.uint32(gen, 0, clockRange)
|
||||
const len = prng.uint32(gen, 0, clockRange - clockStart)
|
||||
addToIdSet(idset, client, clockStart, len)
|
||||
idset.add(client, clockStart, len)
|
||||
}
|
||||
if (idset.clients.size === clients && clients > 1 && prng.bool(gen)) {
|
||||
idset.clients.delete(prng.uint32(gen, 0, clients))
|
||||
@@ -509,8 +507,8 @@ export const compare = users => {
|
||||
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
|
||||
|
||||
/**
|
||||
* @param {import('../src/internals.js').StructStore} ss1
|
||||
* @param {import('../src/internals.js').StructStore} ss2
|
||||
* @param {StructStore} ss1
|
||||
* @param {StructStore} ss2
|
||||
*/
|
||||
export const compareStructStores = (ss1, ss2) => {
|
||||
t.assert(ss1.clients.size === ss2.clients.size)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as t from 'lib0/testing'
|
||||
import * as Y from '../src/index.js'
|
||||
import { init, compare } from './testHelper.js' // eslint-disable-line
|
||||
import { readBlockSet, readIdSet, UpdateDecoderV2, UpdateEncoderV2, writeIdSet } from '../src/internals.js'
|
||||
import { readBlockSet } from '../src/utils/BlockSet.js'
|
||||
import { readIdSet, writeIdSet } from '../src/utils/ids.js'
|
||||
import { UpdateDecoderV2 } from '../src/utils/UpdateDecoder.js'
|
||||
import { UpdateEncoderV2 } from '../src/utils/UpdateEncoder.js'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as object from 'lib0/object'
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import * as Y from '../src/index.js'
|
||||
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
||||
import {
|
||||
noAttributionsManager,
|
||||
TwosetAttributionManager,
|
||||
createIdMapFromIdSet
|
||||
} from '../src/internals.js'
|
||||
import { noAttributionsManager, TwosetAttributionManager } from '../src/utils/AttributionManager.js'
|
||||
import { createIdMapFromIdSet } from '../src/utils/ids.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import * as prng from 'lib0/prng'
|
||||
import * as delta from 'lib0/delta'
|
||||
|
||||
@@ -3,7 +3,8 @@ import * as t from 'lib0/testing'
|
||||
import * as prng from 'lib0/prng'
|
||||
import * as math from 'lib0/math'
|
||||
import * as delta from 'lib0/delta'
|
||||
import { createIdMapFromIdSet, noAttributionsManager, TwosetAttributionManager, createAttributionManagerFromSnapshots } from '../src/internals.js'
|
||||
import { createIdMapFromIdSet } from '../src/utils/ids.js'
|
||||
import { noAttributionsManager, TwosetAttributionManager, createAttributionManagerFromSnapshots } from '../src/utils/AttributionManager.js'
|
||||
|
||||
const { init, compare } = Y
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"moduleResolution": "nodenext",
|
||||
"paths": {
|
||||
"yjs": ["./src/index.js"],
|
||||
"yjs/internals": ["./src/internals.js"],
|
||||
"yjs/testHelper": ["./tests/testHelper.js"]
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user