flatten, no more circular dependencies

This commit is contained in:
Kevin Jahns
2026-03-27 01:29:21 +01:00
parent 44bddadcdd
commit f983fb767b
54 changed files with 3909 additions and 3621 deletions

45
global.d.ts vendored
View File

@@ -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
View File

@@ -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"
}
}
}
}

View File

@@ -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",

View File

@@ -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',

View File

@@ -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'

View File

@@ -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'

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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())

View File

@@ -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())

View File

@@ -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()))

View File

@@ -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())

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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())

View File

@@ -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))

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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()

View File

@@ -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 = () => {

View File

@@ -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

View File

@@ -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'

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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) || []

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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 () {

View 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()

View File

View 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

View 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,7 +1,3 @@
import {
YType // eslint-disable-line
} from '../internals.js'
/**
* Convenient helper to log type information.
*

View File

@@ -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
View File

@@ -0,0 +1,3 @@
import * as s from 'lib0/schema'
export const $ydoc = /** @type {s.Schema<Doc>} */ (s.$type('y:doc', null))

View 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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')
})

View File

@@ -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)
}
}
}

View File

@@ -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'

View File

@@ -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)

View File

@@ -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'

View File

@@ -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'

View File

@@ -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

View File

@@ -16,7 +16,6 @@
"moduleResolution": "nodenext",
"paths": {
"yjs": ["./src/index.js"],
"yjs/internals": ["./src/internals.js"],
"yjs/testHelper": ["./tests/testHelper.js"]
}
},