From f983fb767b8d5b9b299cde9647188d1ec2861161 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Fri, 27 Mar 2026 01:29:21 +0100 Subject: [PATCH] flatten, no more circular dependencies --- global.d.ts | 45 +- package-lock.json | 567 ++++++++++ package.json | 11 +- rollup.config.js | 3 +- src/index.js | 151 +-- src/internals.js | 35 - src/structs/AbstractStruct.js | 37 +- src/structs/ContentAny.js | 115 -- src/structs/ContentBinary.js | 93 -- src/structs/ContentDeleted.js | 101 -- src/structs/ContentDoc.js | 141 --- src/structs/ContentEmbed.js | 98 -- src/structs/ContentFormat.js | 105 -- src/structs/ContentJSON.js | 119 -- src/structs/ContentString.js | 113 -- src/structs/ContentType.js | 152 --- src/structs/GC.js | 35 +- src/structs/Item.js | 1315 ++++++++++++++++------ src/structs/Skip.js | 35 +- src/utils/AttributionManager.js | 197 +--- src/utils/BlockSet.js | 36 +- src/utils/Doc.js | 44 +- src/utils/ID.js | 2 - src/utils/IdMap.js | 673 ----------- src/utils/RelativePosition.js | 26 +- src/utils/Snapshot.js | 46 +- src/utils/StructStore.js | 316 ++---- src/utils/Transaction.js | 360 +----- src/utils/UndoManager.js | 163 ++- src/utils/UpdateDecoder.js | 5 +- src/utils/UpdateEncoder.js | 4 - src/utils/YEvent.js | 26 +- src/utils/attribution-manager-helpers.js | 110 ++ src/utils/content-helper.js | 0 src/utils/delta-helpers.js | 14 +- src/utils/encoding-helpers.js | 110 ++ src/utils/encoding.js | 319 ++++-- src/utils/{IdSet.js => ids.js} | 763 +++++++++++-- src/utils/isParentOf.js | 6 +- src/utils/logging.js | 4 - src/utils/meta.js | 82 +- src/utils/schemas.js | 3 + src/utils/transaction-helpers.js | 386 +++++++ src/utils/updates.js | 160 +-- src/ytype.js | 309 +++-- test.html | 3 +- tests/IdMap.tests.js | 44 +- tests/IdSet.tests.js | 6 +- tests/encoding.tests.js | 4 +- tests/testHelper.js | 22 +- tests/updates.tests.js | 5 +- tests/y-map.tests.js | 7 +- tests/y-text.tests.js | 3 +- tsconfig.json | 1 - 54 files changed, 3909 insertions(+), 3621 deletions(-) delete mode 100644 src/internals.js delete mode 100644 src/structs/ContentAny.js delete mode 100644 src/structs/ContentBinary.js delete mode 100644 src/structs/ContentDeleted.js delete mode 100644 src/structs/ContentDoc.js delete mode 100644 src/structs/ContentEmbed.js delete mode 100644 src/structs/ContentFormat.js delete mode 100644 src/structs/ContentJSON.js delete mode 100644 src/structs/ContentString.js delete mode 100644 src/structs/ContentType.js delete mode 100644 src/utils/IdMap.js create mode 100644 src/utils/attribution-manager-helpers.js create mode 100644 src/utils/content-helper.js create mode 100644 src/utils/encoding-helpers.js rename src/utils/{IdSet.js => ids.js} (51%) create mode 100644 src/utils/schemas.js create mode 100644 src/utils/transaction-helpers.js diff --git a/global.d.ts b/global.d.ts index f560ebd4..d9a1ee34 100644 --- a/global.d.ts +++ b/global.d.ts @@ -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 = import('./src/utils/ids.js').IdMap +declare type AttrRanges = import('./src/utils/ids.js').AttrRanges +declare type AttrRange = import('./src/utils/ids.js').AttrRange +declare type ContentAttribute = import('./src/utils/ids.js').ContentAttribute + +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 = import('./src/ytype.js').YType +declare type YEvent = import('./src/utils/YEvent.js').YEvent +declare type EventHandler = import('./src/utils/EventHandler.js').EventHandler + +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 = import('lib0/delta').Delta + +// @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 = import('./src/utils/attribution-manager-helpers.js').AttributedContent + +declare type Snapshot = import('./src/utils/Snapshot.js').Snapshot diff --git a/package-lock.json b/package-lock.json index 6ce94818..0611a4ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index cb05e95b..be232b57 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test-extensive": "node ./tests/index.js --production --repetition-time 10000", "dist": "npm run clean && (tsc --skipLibCheck --noEmit false|| true)", "release": "npm test && PRODUCTION=1 npm run dist && test -e dist/src/index.d.ts && np", - "lint": "markdownlint README.md && standard && tsc --skipLibCheck", + "lint": "markdownlint README.md && standard && tsc --skipLibCheck && npx dpdm --exit-code circular:1 --warning false --tree false src/index.js", "debug": "npm run gentesthtml && 0serve -o test.html", "debug:node": "node --inspect-brk tests/index.js", "trace-deopt": "clear && node --trace-deopt ./tests/index.js", @@ -27,10 +27,6 @@ "types": "./dist/src/index.d.ts", "default": "./src/index.js" }, - "./internals": { - "types": "./dist/src/internals.d.ts", - "default": "./src/internals.js" - }, "./meta": { "types": "./dist/src/utils/meta.d.ts", "default": "./src/utils/meta.js" @@ -81,11 +77,12 @@ "devDependencies": { "@types/node": "^22.14.1", "@y/protocols": "^1.0.6-rc.1", + "@y/y": ".", + "dpdm": "^4.0.1", "markdownlint": "^0.40.0", "markdownlint-cli": "^0.45.0", "standard": "^17.1.2", - "typescript": "^5.9.3", - "@y/y": "." + "typescript": "^5.9.3" }, "engines": { "npm": ">=8.0.0", diff --git a/rollup.config.js b/rollup.config.js index 581f488d..c15a928a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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', diff --git a/src/index.js b/src/index.js index 97d9f447..65a2358d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,132 +1,29 @@ /** eslint-env browser */ -export { - Doc, - Transaction, - YType as Type, - YEvent, - Item, - AbstractStruct, - GC, - Skip, - ContentBinary, - ContentDeleted, - ContentDoc, - ContentEmbed, - ContentFormat, - ContentJSON, - ContentAny, - ContentString, - ContentType, - getTypeChildren, - createRelativePositionFromTypeIndex, - createRelativePositionFromJSON, - createAbsolutePositionFromRelativePosition, - compareRelativePositions, - AbsolutePosition, - RelativePosition, - ID, - createID, - compareIDs, - getState, - Snapshot, - createSnapshot, - cleanupYTextFormatting, - snapshot, - emptySnapshot, - findRootTypeKey, - findIndexSS, - getItem, - getItemCleanStart, - getItemCleanEnd, - typeMapGetSnapshot, - typeMapGetAllSnapshot, - createDocFromSnapshot, - applyUpdate, - applyUpdateV2, - readUpdate, - readUpdateV2, - encodeStateAsUpdate, - encodeStateAsUpdateV2, - encodeStateVector, - UndoManager, - decodeSnapshot, - encodeSnapshot, - decodeSnapshotV2, - encodeSnapshotV2, - decodeStateVector, - logUpdate, - logUpdateV2, - decodeUpdate, - decodeUpdateV2, - relativePositionToJSON, - isParentOf, - equalSnapshots, - tryGc, - transact, - logType, - mergeUpdates, - mergeUpdatesV2, - encodeStateVectorFromUpdate, - encodeStateVectorFromUpdateV2, - encodeRelativePosition, - decodeRelativePosition, - diffUpdate, - diffUpdateV2, - convertUpdateFormatV1ToV2, - convertUpdateFormatV2ToV1, - obfuscateUpdate, - obfuscateUpdateV2, - UpdateEncoderV1, - UpdateEncoderV2, - UpdateDecoderV1, - UpdateDecoderV2, - snapshotContainsUpdate, - // idset - IdSet, - equalIdSets, - createDeleteSetFromStructStore, - IdMap, - createIdMap, - createContentAttribute, - createInsertSetFromStructStore, - diffIdMap, - diffIdSet, - ContentAttribute, - encodeIdMap, - createIdMapFromIdSet, - TwosetAttributionManager, - noAttributionsManager, - AbstractAttributionManager, - iterateStructsByIdSet, - createAttributionManagerFromDiff, - DiffAttributionManager, - createAttributionManagerFromSnapshots, - SnapshotAttributionManager, - createIdSet, - mergeIdSets, - cloneDoc, - createContentIdsFromUpdate, - createContentIdsFromUpdateV2, - insertIntoIdMap, - insertIntoIdSet, - mergeIdMaps, - readIdMap, - readIdSet, - decodeIdMap, - diffDocsToDelta, - getPathTo, - Attributions, - filterIdMap, - undoContentIds, - intersectUpdateWithContentIds, - intersectUpdateWithContentIdsV2, - createDocFromUpdate, - createDocFromUpdateV2, - $ytype, - $ytypeAny, - $attributionManager -} from './internals.js' +// Order matters: follows internals.js ordering to avoid circular dependency issues + +export { IdSet, equalIdSets, createDeleteSetFromStructStore, createInsertSetFromStructStore, diffIdSet, createIdSet, mergeIdSets, insertIntoIdSet, iterateStructsByIdSet, readIdSet, IdMap, createIdMap, createContentAttribute, ContentAttribute, diffIdMap, encodeIdMap, createIdMapFromIdSet, insertIntoIdMap, mergeIdMaps, readIdMap, decodeIdMap, filterIdMap } from './utils/ids.js' +export { Doc } from './utils/Doc.js' +export { UpdateDecoderV1, UpdateDecoderV2 } from './utils/UpdateDecoder.js' +export { UpdateEncoderV1, UpdateEncoderV2 } from './utils/UpdateEncoder.js' +export { applyUpdate, applyUpdateV2, readUpdate, readUpdateV2, encodeStateAsUpdate, encodeStateAsUpdateV2, encodeStateVector, decodeStateVector, diffUpdate, diffUpdateV2, mergeUpdates, mergeUpdatesV2, createDocFromUpdate, createDocFromUpdateV2, cloneDoc } from './utils/encoding.js' +export { ID, createID, compareIDs, findRootTypeKey } from './utils/ID.js' +export { isParentOf } from './utils/isParentOf.js' +export { logType } from './utils/logging.js' +export { createRelativePositionFromTypeIndex, createRelativePositionFromJSON, createAbsolutePositionFromRelativePosition, compareRelativePositions, AbsolutePosition, RelativePosition, relativePositionToJSON, encodeRelativePosition, decodeRelativePosition } from './utils/RelativePosition.js' +export { Snapshot, createSnapshot, snapshot, emptySnapshot, createDocFromSnapshot, decodeSnapshot, encodeSnapshot, decodeSnapshotV2, encodeSnapshotV2, equalSnapshots, snapshotContainsUpdate } from './utils/Snapshot.js' +export { findIndexSS, getItemCleanStart, getItemCleanEnd, tryGc } from './utils/transaction-helpers.js' +export { Transaction, transact, cleanupYTextFormatting } from './utils/Transaction.js' +export { UndoManager, undoContentIds } from './utils/UndoManager.js' +export { logUpdate, logUpdateV2, decodeUpdate, decodeUpdateV2, encodeStateVectorFromUpdate, encodeStateVectorFromUpdateV2, convertUpdateFormatV1ToV2, convertUpdateFormatV2ToV1, obfuscateUpdate, obfuscateUpdateV2, createContentIdsFromUpdate, createContentIdsFromUpdateV2, intersectUpdateWithContentIds, intersectUpdateWithContentIdsV2 } from './utils/updates.js' +export { YEvent, getPathTo } from './utils/YEvent.js' +export { TwosetAttributionManager, noAttributionsManager, AbstractAttributionManager, createAttributionManagerFromDiff, DiffAttributionManager, createAttributionManagerFromSnapshots, SnapshotAttributionManager, Attributions, $attributionManager } from './utils/AttributionManager.js' +export { diffDocsToDelta } from './utils/delta-helpers.js' +export { YType as Type, getTypeChildren, typeMapGetSnapshot, typeMapGetAllSnapshot, $ytype, $ytypeAny } from './ytype.js' +export { AbstractStruct } from './structs/AbstractStruct.js' +export { GC } from './structs/GC.js' +export { Item, ContentBinary, ContentDeleted, ContentDoc, ContentEmbed, ContentFormat, ContentJSON, ContentAny, ContentString, ContentType } from './structs/Item.js' +export { Skip } from './structs/Skip.js' export * from './utils/meta.js' diff --git a/src/internals.js b/src/internals.js deleted file mode 100644 index 207a74b7..00000000 --- a/src/internals.js +++ /dev/null @@ -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' diff --git a/src/structs/AbstractStruct.js b/src/structs/AbstractStruct.js index 9324a1b4..96735315 100644 --- a/src/structs/AbstractStruct.js +++ b/src/structs/AbstractStruct.js @@ -1,7 +1,3 @@ -import { - UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line -} from '../internals.js' - import * as error from 'lib0/error' export class AbstractStruct { @@ -25,35 +21,44 @@ export class AbstractStruct { * Merge this struct with the item to the right. * This method is already assuming that `this.id.clock + this.length === this.id.clock`. * Also this method does *not* remove right from StructStore! - * @param {AbstractStruct} right + * @param {GC|Item|Skip} _right * @return {boolean} whether this merged with right */ - mergeWith (right) { + mergeWith (_right) { return false } /** - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. - * @param {number} offset - * @param {number} encodingRef + * @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder The encoder to write data to. + * @param {number} _offset + * @param {number} _encodingRef */ - write (encoder, offset, encodingRef) { + write (_encoder, _offset, _encodingRef) { throw error.methodUnimplemented() } /** - * @param {Transaction} transaction - * @param {number} offset + * @param {Transaction} _transaction + * @param {number} _offset */ - integrate (transaction, offset) { + integrate (_transaction, _offset) { throw error.methodUnimplemented() } /** - * @param {number} diff - * @return {import('../internals.js').GC|import('../internals.js').Item} + * @param {number} _diff + * @return {GC|Item|Skip} */ - splice (diff) { + splice (_diff) { throw error.methodUnimplemented() } } + +/** + * @param {IdSet} idSet + * @param {AbstractStruct} struct + * + * @private + * @function + */ +export const addStructToIdSet = (idSet, struct) => idSet.add(struct.id.client, struct.id.clock, struct.length) diff --git a/src/structs/ContentAny.js b/src/structs/ContentAny.js deleted file mode 100644 index 4e32ac83..00000000 --- a/src/structs/ContentAny.js +++ /dev/null @@ -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} arr - */ - constructor (arr) { - /** - * @type {Array} - */ - this.arr = arr - isDevMode && object.deepFreeze(arr) - } - - /** - * @return {number} - */ - getLength () { - return this.arr.length - } - - /** - * @return {Array} - */ - 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) -} diff --git a/src/structs/ContentBinary.js b/src/structs/ContentBinary.js deleted file mode 100644 index 580b60fe..00000000 --- a/src/structs/ContentBinary.js +++ /dev/null @@ -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} - */ - 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()) diff --git a/src/structs/ContentDeleted.js b/src/structs/ContentDeleted.js deleted file mode 100644 index 57b5504b..00000000 --- a/src/structs/ContentDeleted.js +++ /dev/null @@ -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} - */ - 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()) diff --git a/src/structs/ContentDoc.js b/src/structs/ContentDoc.js deleted file mode 100644 index 6df70556..00000000 --- a/src/structs/ContentDoc.js +++ /dev/null @@ -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} 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} - */ - 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())) diff --git a/src/structs/ContentEmbed.js b/src/structs/ContentEmbed.js deleted file mode 100644 index 6d6ce53a..00000000 --- a/src/structs/ContentEmbed.js +++ /dev/null @@ -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} - */ - 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()) diff --git a/src/structs/ContentFormat.js b/src/structs/ContentFormat.js deleted file mode 100644 index e029957a..00000000 --- a/src/structs/ContentFormat.js +++ /dev/null @@ -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} - */ - 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} */ (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()) diff --git a/src/structs/ContentJSON.js b/src/structs/ContentJSON.js deleted file mode 100644 index f20d7306..00000000 --- a/src/structs/ContentJSON.js +++ /dev/null @@ -1,119 +0,0 @@ -import { - UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line -} from '../internals.js' - -/** - * @private - */ -export class ContentJSON { - /** - * @param {Array} arr - */ - constructor (arr) { - /** - * @type {Array} - */ - this.arr = arr - } - - /** - * @return {number} - */ - getLength () { - return this.arr.length - } - - /** - * @return {Array} - */ - 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) -} diff --git a/src/structs/ContentString.js b/src/structs/ContentString.js deleted file mode 100644 index 6f1064b5..00000000 --- a/src/structs/ContentString.js +++ /dev/null @@ -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} - */ - 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 (� / U+FFFD) - this.str = this.str.slice(0, offset - 1) + '�' - // replace right as well - right.str = '�' + 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()) diff --git a/src/structs/ContentType.js b/src/structs/ContentType.js deleted file mode 100644 index 2a2099ab..00000000 --- a/src/structs/ContentType.js +++ /dev/null @@ -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} - */ - 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)) diff --git a/src/structs/GC.js b/src/structs/GC.js index 7ee1bc33..b478c43f 100644 --- a/src/structs/GC.js +++ b/src/structs/GC.js @@ -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 diff --git a/src/structs/Item.js b/src/structs/Item.js index 3733348e..e612c273 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -1,33 +1,20 @@ -import { - GC, - getState, - AbstractStruct, - replaceStruct, - addStruct, - addToIdSet, - findRootTypeKey, - compareIDs, - getItem, - getItemCleanEnd, - getItemCleanStart, - readContentDeleted, - readContentBinary, - readContentJSON, - readContentAny, - readContentString, - readContentEmbed, - readContentDoc, - createID, - readContentFormat, - readContentType, - addChangedTypeToTransaction, - addStructToIdSet, - IdSet, StackItem, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, YType, Transaction, // eslint-disable-line -} from '../internals.js' - import * as error from 'lib0/error' import * as binary from 'lib0/binary' -import * as array from 'lib0/array' +import * as env from 'lib0/environment' +import * as object from 'lib0/object' + +import { AbstractStruct, addStructToIdSet } from '../structs/AbstractStruct.js' + +import { ID, createID, compareIDs, findRootTypeKey } from '../utils/ID.js' +import { GC } from '../structs/GC.js' + +import { + replaceStruct, + getItemCleanEnd, + addChangedTypeToTransaction +} from '../utils/transaction-helpers.js' + +const isDevMode = env.getVariable('node_env') === 'development' /** * @todo This should return several items @@ -47,227 +34,15 @@ export const followRedone = (store, id) => { if (diff > 0) { nextID = createID(nextID.client, nextID.clock + diff) } - item = getItem(store, nextID) + item = store.getItem(nextID) diff = nextID.clock - item.id.clock nextID = item.redone - } while (nextID !== null && item instanceof Item) + } while (nextID !== null && item.isItem) return { item, diff } } -/** - * 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 - } -} - -/** - * Split leftItem into two items - * @param {Transaction?} transaction - * @param {Item} leftItem - * @param {number} diff - * @return {Item} - * - * @function - * @private - */ -export const splitItem = (transaction, leftItem, diff) => { - // create rightItem - const { client, clock } = leftItem.id - const rightItem = new Item( - createID(client, clock + diff), - leftItem, - createID(client, clock + diff - 1), - leftItem.right, - leftItem.rightOrigin, - leftItem.parent, - leftItem.parentSub, - leftItem.content.splice(diff) - ) - if (leftItem.deleted) { - rightItem.markDeleted() - } - if (leftItem.keep) { - rightItem.keep = true - } - if (leftItem.redone !== null) { - rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff) - } - if (transaction != null) { - // update left (do not set leftItem.rightOrigin as it will lead to problems when syncing) - leftItem.right = rightItem - // update right - if (rightItem.right !== null) { - rightItem.right.left = rightItem - } - // right is more specific. - transaction._mergeStructs.push(rightItem) - // update parent._map - if (rightItem.parentSub !== null && rightItem.right === null) { - /** @type {YType} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem) - } - } else { - rightItem.left = null - rightItem.right = null - } - leftItem.length = diff - return rightItem -} - -/** - * More generalized version of splitItem. Split leftStruct into two structs - * @param {Transaction?} transaction - * @param {AbstractStruct} leftStruct - * @param {number} diff - * @return {GC|Item} - * - * @function - * @private - */ -export const splitStruct = (transaction, leftStruct, diff) => { - if (leftStruct instanceof Item) { - return splitItem(transaction, leftStruct, diff) - } else { - const rightItem = leftStruct.splice(diff) - transaction?._mergeStructs.push(rightItem) - return rightItem - } -} - -/** - * @param {Array} 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} 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 = getState(store, 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 -} - /** * Abstract class that represents any content. */ @@ -386,56 +161,6 @@ export class Item extends AbstractStruct { this.info |= binary.BIT3 } - /** - * Return the creator clientID of the missing op or define missing items and return null. - * - * @param {Transaction} transaction - * @param {StructStore} store - * @return {null | number} - */ - getMissing (transaction, store) { - if (this.origin && (this.origin.clock >= getState(store, this.origin.client) || store.skips.hasId(this.origin))) { - return this.origin.client - } - if (this.rightOrigin && (this.rightOrigin.clock >= getState(store, this.rightOrigin.client) || store.skips.hasId(this.rightOrigin))) { - return this.rightOrigin.client - } - if (this.parent && this.parent.constructor === ID && (this.parent.clock >= getState(store, this.parent.client) || store.skips.hasId(this.parent))) { - return this.parent.client - } - // We have all missing ids, now find the items - if (this.origin) { - this.left = getItemCleanEnd(transaction, store, this.origin) - this.origin = this.left.lastId - } - if (this.rightOrigin) { - this.right = getItemCleanStart(transaction, this.rightOrigin) - this.rightOrigin = this.right.id - } - if ((this.left && this.left.constructor === GC) || (this.right && this.right.constructor === GC)) { - this.parent = null - } else if (this.parent == null) { - // only set parent if this shouldn't be garbage collected - if (this.left && this.left.constructor === Item) { - this.parent = this.left.parent - this.parentSub = this.left.parentSub - } else if (this.right && this.right.constructor === Item) { - this.parent = this.right.parent - this.parentSub = this.right.parentSub - } - } else if (this.parent.constructor === ID) { - const parentItem = getItem(store, this.parent) - if (parentItem.constructor === GC) { - this.parent = null - } else { - this.parent = /** @type {ContentType} */ (parentItem.content).type - } - } else if (typeof this.parent === 'string') { - this.parent = transaction.doc.get(this.parent) - } - return null - } - /** * @param {Transaction} transaction * @param {number} offset @@ -497,9 +222,9 @@ export class Item extends AbstractStruct { // Since this is to the left of o, we can break here break } // else, o might be integrated before an item that this conflicts with. If so, we will find it in the next iterations - } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) { // use getItem instead of getItemCleanEnd because we don't want / need to split items. + } else if (o.origin !== null && itemsBeforeOrigin.has(transaction.doc.store.getItem(o.origin))) { // use getItem instead of getItemCleanEnd because we don't want / need to split items. // case 2 - if (!conflictingItems.has(getItem(transaction.doc.store, o.origin))) { + if (!conflictingItems.has(transaction.doc.store.getItem(o.origin))) { left = o conflictingItems.clear() } @@ -543,7 +268,7 @@ export class Item extends AbstractStruct { /** @type {YType} */ (this.parent)._length += this.length } addStructToIdSet(transaction.insertSet, this) - addStruct(transaction.doc.store, this) + transaction.doc.store.add(this) this.content.integrate(transaction, this) // add parent to transaction.changed addChangedTypeToTransaction(transaction, /** @type {YType} */ (this.parent), this.parentSub) @@ -646,7 +371,7 @@ export class Item extends AbstractStruct { parent._length -= this.length } this.markDeleted() - addToIdSet(transaction.deleteSet, this.id.client, this.id.clock, this.length) + transaction.deleteSet.add(this.id.client, this.id.clock, this.length) addChangedTypeToTransaction(transaction, parent, this.parentSub) this.content.delete(transaction) } @@ -668,6 +393,55 @@ export class Item extends AbstractStruct { } } + /** + * Split this into two items + * @param {Transaction?} transaction + * @param {number} diff + * @return {Item} + */ + split (transaction, diff) { + // create rightItem + const { client, clock } = this.id + const rightItem = new Item( + createID(client, clock + diff), + this, + createID(client, clock + diff - 1), + this.right, + this.rightOrigin, + this.parent, + this.parentSub, + this.content.splice(diff) + ) + if (this.deleted) { + rightItem.markDeleted() + } + if (this.keep) { + rightItem.keep = true + } + if (this.redone !== null) { + rightItem.redone = createID(this.redone.client, this.redone.clock + diff) + } + if (transaction != null) { + // update left (do not set leftItem.rightOrigin as it will lead to problems when syncing) + this.right = rightItem + // update right + if (rightItem.right !== null) { + rightItem.right.left = rightItem + } + // right is more specific. + transaction._mergeStructs.push(rightItem) + // update parent._map + if (rightItem.parentSub !== null && rightItem.right === null) { + /** @type {YType} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem) + } + } else { + rightItem.left = null + rightItem.right = null + } + this.length = diff + return rightItem + } + /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. @@ -694,7 +468,7 @@ export class Item extends AbstractStruct { encoder.writeRightID(rightOrigin) } if (origin === null && rightOrigin === null) { - const parent = /** @type {YType} */ (this.parent) + const parent = /** @type {YType} */ (this.parent) if (parent._item !== undefined) { const parentItem = parent._item if (parentItem === null) { @@ -722,32 +496,16 @@ export class Item extends AbstractStruct { } this.content.write(encoder, offset, offsetEnd) } + + get ref () { + return this.content.getRef() + } } /** - * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder - * @param {number} info + * @type {true} */ -export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder) - -/** - * A lookup map for reading Item content. - * - * @type {Array} - */ -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 -] +Item.prototype.isItem = true /** * Do not implement this class! @@ -835,9 +593,926 @@ export class AbstractContent { } /** - * @return {number} + * @return {1|2|3|4|5|6|7|8|9} */ getRef () { throw error.methodUnimplemented() } } + +export class ContentAny { + /** + * @param {Array} arr + */ + constructor (arr) { + /** + * @type {Array} + */ + this.arr = arr + isDevMode && object.deepFreeze(arr) + } + + /** + * @return {number} + */ + getLength () { + return this.arr.length + } + + /** + * @return {Array} + */ + 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 {8} + */ + getRef () { + return 8 + } +} + +export class ContentBinary { + /** + * @param {Uint8Array} content + */ + constructor (content) { + this.content = content + } + + /** + * @return {number} + */ + getLength () { + return 1 + } + + /** + * @return {Array} + */ + 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 {3} + */ + getRef () { + return 3 + } +} + +export class ContentDeleted { + /** + * @param {number} len + */ + constructor (len) { + this.len = len + } + + /** + * @return {number} + */ + getLength () { + return this.len + } + + /** + * @return {Array} + */ + 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) { + transaction.deleteSet.add(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 {1} + */ + getRef () { + return 1 + } +} + +/** + * @private + */ +export class ContentDoc { + /** + * @param {string} guid + * @param {Object} opts + */ + constructor (guid, opts) { + /** + * @type {Doc?} + */ + this.doc = null + this.guid = guid + this.opts = opts + } + + /** + * @return {number} + */ + getLength () { + return 1 + } + + /** + * @return {Array} + */ + getContent () { + return [this.doc] + } + + /** + * @return {boolean} + */ + isCountable () { + return true + } + + /** + * @return {ContentDoc} + */ + copy () { + return new ContentDoc(this.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) { + const opts = this.opts + if (this.doc == null) { + // we get the constructor from the existing doc to avoid import the doc module, leading to a + // circular dependency + this.doc = /** @type {Doc} */ (new /** @type {any} */ (transaction.doc.constructor)({ guid: this.guid, ...this.opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false })) + } + this.doc._item = item + transaction.subdocsAdded.add(this.doc) + if (this.doc.shouldLoad) { + transaction.subdocsLoaded.add(this.doc) + } + } + + /** + * @param {Transaction} transaction + */ + delete (transaction) { + if (this.doc) { + 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.guid) + encoder.writeAny(this.opts) + } + + /** + * @return {9} + */ + getRef () { + return 9 + } +} + +/** + * @param {Doc} ydoc + */ +export const createContentDocFromDoc = ydoc => { + /** + * @type {any} + */ + const opts = {} + if (!ydoc.gc) { + opts.gc = false + } + if (ydoc.autoLoad) { + opts.autoLoad = true + } + if (ydoc.meta !== null) { + opts.meta = ydoc.meta + } + const c = new ContentDoc(ydoc.guid, opts) + c.doc = ydoc + return c +} + +/** + * @private + */ +export class ContentEmbed { + /** + * @param {Object} embed + */ + constructor (embed) { + this.embed = embed + } + + /** + * @return {number} + */ + getLength () { + return 1 + } + + /** + * @return {Array} + */ + 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 {5} + */ + getRef () { + return 5 + } +} + +/** + * @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} + */ + 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} */ (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 {6} + */ + getRef () { + return 6 + } +} + +/** + * @private + */ +export class ContentJSON { + /** + * @param {Array} arr + */ + constructor (arr) { + /** + * @type {Array} + */ + this.arr = arr + } + + /** + * @return {number} + */ + getLength () { + return this.arr.length + } + + /** + * @return {Array} + */ + 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 {2} + */ + getRef () { + return 2 + } +} + +/** + * @private + */ +export class ContentString { + /** + * @param {string} str + */ + constructor (str) { + /** + * @type {string} + */ + this.str = str + } + + /** + * @return {number} + */ + getLength () { + return this.str.length + } + + /** + * @return {Array} + */ + 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 (� / U+FFFD) + this.str = this.str.slice(0, offset - 1) + '�' + // replace right as well + right.str = '�' + 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 {4} + */ + getRef () { + return 4 + } +} + +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} + */ + 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 {7} + */ + getRef () { + return 7 + } +} diff --git a/src/structs/Skip.js b/src/structs/Skip.js index 1fa81a58..d5962522 100644 --- a/src/structs/Skip.js +++ b/src/structs/Skip.js @@ -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 diff --git a/src/utils/AttributionManager.js b/src/utils/AttributionManager.js index aecd42ed..20f934e4 100644 --- a/src/utils/AttributionManager.js +++ b/src/utils/AttributionManager.js @@ -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} Attribution - */ +import { $attributionManager, AttributedContent } from './attribution-manager-helpers.js' -/** - * @todo SHOULD NOT RETURN AN OBJECT! - * @param {Array>?} 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> | 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>} _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>} 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>} [options.attrs] - the attributes to apply to the diff + * @param {Array} [options.attrs] - the attributes to apply to the diff */ constructor (prevSnapshot, nextSnapshot) { super() diff --git a/src/utils/BlockSet.js b/src/utils/BlockSet.js index dd75d5c4..83a933d8 100644 --- a/src/utils/BlockSet.js +++ b/src/utils/BlockSet.js @@ -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} + * @type {Array} */ const refs = new Array(numberOfBlocks) const client = decoder.readClient() @@ -99,12 +97,12 @@ export const writeBlockSet = (encoder, blocks) => { class BlockRange { /** - * @param {Array} refs + * @param {Array} refs */ constructor (refs) { this.i = 0 /** - * @type {Array} + * @type {Array} */ this.refs = refs } @@ -203,12 +201,12 @@ export class BlockSet { } else { // requires more computation because we need to filter duplicates /** - * @type {Array} + * @type {Array} */ 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 = () => { diff --git a/src/utils/Doc.js b/src/utils/Doc.js index 4e8edf5e..27488357 100644 --- a/src/utils/Doc.js +++ b/src/utils/Doc.js @@ -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 diff --git a/src/utils/ID.js b/src/utils/ID.js index 90352e27..07806824 100644 --- a/src/utils/ID.js +++ b/src/utils/ID.js @@ -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' diff --git a/src/utils/IdMap.js b/src/utils/IdMap.js deleted file mode 100644 index 30f848b7..00000000 --- a/src/utils/IdMap.js +++ /dev/null @@ -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} - */ -export const createContentAttribute = (name, val) => new ContentAttribute(name, val) - -/** - * @template T - * @param {Array} attrs - * @param {T} attr - * - */ -const idmapAttrsHas = (attrs, attr) => attrs.find(a => a === attr) - -/** - * @template T - * @param {Array} a - * @param {Array} b - */ -export const idmapAttrsEqual = (a, b) => a.length === b.length && a.every(v => idmapAttrsHas(b, v)) - -/** - * @template T - * @param {Array} a - * @param {Array} 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>} 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>? }} MaybeAttrRange - */ - -/** - * @template Attrs - * - * @param {number} clock - * @param {number} len - * @param {Array>?} attrs - * @return {MaybeAttrRange} - */ -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>} 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>} 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>} ams - * @return {IdMap} A fresh IdSet - */ -export const mergeIdMaps = ams => { - /** - * Maps attribution to the attribution of the merged idmap. - * - * @type {Map,ContentAttribute>} - */ - 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>} attrs - */ -export const createIdMapFromIdSet = (idset, attrs) => { - const idmap = createIdMap() - // map attrs to idmap - attrs = _ensureAttrs(idmap, attrs) - // filter out duplicates - /** - * @type {Array>} - */ - 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} 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>} - */ - this.clients = new Map() - /** - * @type {Map>} - */ - this.attrsH = new Map() - /** - * @type {Set>} - */ - this.attrs = new Set() - } - - /** - * @param {(attrRange:AttrRange, 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>} - */ - 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>} - */ - slice (client, clock, len) { - const dr = this.clients.get(client) - /** - * @type {Array>} - */ - const res = [] - if (dr) { - /** - * @type {Array>} - */ - 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>} 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} idmap - * - * @private - * @function - */ -export const writeIdMap = (encoder, idmap) => { - encoding.writeVarUint(encoder.restEncoder, idmap.clients.size) - let lastWrittenClientId = 0 - /** - * @type {Map, number>} - */ - const visitedAttributions = map.create() - /** - * @type {Map} - */ - 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} idmap - */ -export const encodeIdMap = idmap => { - const encoder = new IdSetEncoderV2() - writeIdMap(encoder, idmap) - return encoder.toUint8Array() -} - -/** - * @param {IdSetDecoderV1 | IdSetDecoderV2} decoder - * @return {IdMap} - * - * @private - * @function - */ -export const readIdMap = decoder => { - const idmap = new IdMap() - const numClients = decoding.readVarUint(decoder.restDecoder) - /** - * @type {Array>} - */ - const visitedAttributions = [] - /** - * @type {Array} - */ - 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>} - */ - const attrRanges = [] - for (let i = 0; i < numberOfDeletes; i++) { - const rangeClock = decoder.readDsClock() - const rangeLen = decoder.readDsLen() - /** - * @type {Array>} - */ - 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} - */ -export const decodeIdMap = data => readIdMap(new IdSetDecoderV2(decoding.createDecoder(data))) - -/** - * @template Attrs - * @param {IdMap} idmap - * @param {Array>} attrs - * @return {Array>} - */ -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} dest - * @param {IdMap} 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} ISet - * @param {ISet} set - * @param {IdSet | IdMap} 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} idmap - * @param {(attr: Array>) => boolean} predicate - * @return {IdMap} - */ -export const filterIdMap = (idmap, predicate) => { - const filtered = createIdMap() - idmap.clients.forEach((ranges, client) => { - /** - * @type {Array>} - */ - 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 -} diff --git a/src/utils/RelativePosition.js b/src/utils/RelativePosition.js index 9820c231..d51227e7 100644 --- a/src/utils/RelativePosition.js +++ b/src/utils/RelativePosition.js @@ -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 { diff --git a/src/utils/Snapshot.js b/src/utils/Snapshot.js index 0f57a2fd..98b79303 100644 --- a/src/utils/Snapshot.js +++ b/src/utils/Snapshot.js @@ -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) || [] diff --git a/src/utils/StructStore.js b/src/utils/StructStore.js index c4ea2bb3..3b2acfa6 100644 --- a/src/utils/StructStore.js +++ b/src/utils/StructStore.js @@ -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>} + * @type {Map>} */ 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} */ (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} */ (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, 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} 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} - */ - // @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} 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} */ (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} - */ - // @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} */ (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} 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) -} diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index d1a3a4e7..f8b3c92d 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -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} 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} */ (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} */ (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} 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} startAttributes - * @param {Map} 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} - */ - 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} - */ - 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} 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} + */ + 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 diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index 29443051..48116199 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -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} 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} 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 + } +} diff --git a/src/utils/UpdateDecoder.js b/src/utils/UpdateDecoder.js index c269e6ac..dc1338c3 100644 --- a/src/utils/UpdateDecoder.js +++ b/src/utils/UpdateDecoder.js @@ -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 diff --git a/src/utils/UpdateEncoder.js b/src/utils/UpdateEncoder.js index 30291ed6..9d7361db 100644 --- a/src/utils/UpdateEncoder.js +++ b/src/utils/UpdateEncoder.js @@ -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() diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index 8348c313..8cbb36a8 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -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>|null} + * @type {Delta>|null} */ this._delta = null /** - * @type {delta.Delta|null} + * @type {Delta|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 : delta.Delta>} The Delta representation of this type. + * @return {Deep extends true ? Delta : Delta>} 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>} The Delta representation of this type. + * @type {Delta>} 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} The Delta representation of this type. + * @type {Delta} The Delta representation of this type. * @public */ get deltaDeep () { diff --git a/src/utils/attribution-manager-helpers.js b/src/utils/attribution-manager-helpers.js new file mode 100644 index 00000000..fc9d44ee --- /dev/null +++ b/src/utils/attribution-manager-helpers.js @@ -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} Attribution + */ + +/** + * @template T + */ +export class AttributedContent { + /** + * @param {AbstractContent} content + * @param {number} clock + * @param {boolean} deleted + * @param {Array> | 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>} _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>} 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() diff --git a/src/utils/content-helper.js b/src/utils/content-helper.js new file mode 100644 index 00000000..e69de29b diff --git a/src/utils/delta-helpers.js b/src/utils/delta-helpers.js index 1d620eb7..55c594b1 100644 --- a/src/utils/delta-helpers.js +++ b/src/utils/delta-helpers.js @@ -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 diff --git a/src/utils/encoding-helpers.js b/src/utils/encoding-helpers.js new file mode 100644 index 00000000..096aa49d --- /dev/null +++ b/src/utils/encoding-helpers.js @@ -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} structs All structs by `client` + * @param {number} client + * @param {Array} 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} */ (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 +} diff --git a/src/utils/encoding.js b/src/utils/encoding.js index 1cb6bf25..cf0c143f 100644 --- a/src/utils/encoding.js +++ b/src/utils/encoding.js @@ -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} structs All structs by `client` - * @param {number} client - * @param {Array} 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} */ (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} + * @type {Array} */ 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, i: number }} + * @type {{ refs: Array, 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>} updates + * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] + * @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder] + * @return {Uint8Array} + */ +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>} updates + * @return {Uint8Array} + */ +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} update + * @param {Uint8Array} 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 +} diff --git a/src/utils/IdSet.js b/src/utils/ids.js similarity index 51% rename from src/utils/IdSet.js rename to src/utils/ids.js index 7e73b87e..7265a6a6 100644 --- a/src/utils/IdSet.js +++ b/src/utils/ids.js @@ -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>} + * @return {Array>} */ 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} 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} dest + * @param {IdMap|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} structs + * @param {Array} 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>} 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} + */ +export const createContentAttribute = (name, val) => new ContentAttribute(name, val) + +/** + * @template Attrs + * @typedef {{ clock: number, len: number, attrs: Array>? }} MaybeAttrRange + */ + +/** + * @template Attrs + * + * @param {number} clock + * @param {number} len + * @param {Array>?} attrs + * @return {MaybeAttrRange} + */ +export const createMaybeAttrRange = (clock, len, attrs) => new AttrRange(clock, len, /** @type {any} */ (attrs)) + +/** + * @template T + * @param {Array} a + * @param {Array} 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>} 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>} 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>} + */ + this.clients = new Map() + /** + * @type {Map>} + */ + this.attrsH = new Map() + /** + * @type {Set>} + */ + this.attrs = new Set() + } + + /** + * @param {(attrRange:AttrRange, 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>} + */ + 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>} + */ + slice (client, clock, len) { + const dr = this.clients.get(client) + /** + * @type {Array>} + */ + const res = [] + if (dr) { + /** + * @type {Array>} + */ + 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>} 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} attrs + * @param {T} attr + * + */ +const idmapAttrsHas = (attrs, attr) => attrs.find(a => a === attr) + +/** + * @template T + * @param {Array} a + * @param {Array} 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>} ams + * @return {IdMap} A fresh IdSet + */ +export const mergeIdMaps = ams => { + /** + * Maps attribution to the attribution of the merged idmap. + * + * @type {Map,ContentAttribute>} + */ + 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>} attrs + */ +export const createIdMapFromIdSet = (idset, attrs) => { + const idmap = createIdMap() + // map attrs to idmap + attrs = _ensureAttrs(idmap, attrs) + // filter out duplicates + /** + * @type {Array>} + */ + 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} 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} idmap + * + * @private + * @function + */ +export const writeIdMap = (encoder, idmap) => { + encoding.writeVarUint(encoder.restEncoder, idmap.clients.size) + let lastWrittenClientId = 0 + /** + * @type {Map, number>} + */ + const visitedAttributions = map.create() + /** + * @type {Map} + */ + 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} idmap + */ +export const encodeIdMap = idmap => { + const encoder = new IdSetEncoderV2() + writeIdMap(encoder, idmap) + return encoder.toUint8Array() +} + +/** + * @param {IdSetDecoderV1 | IdSetDecoderV2} decoder + * @return {IdMap} + * + * @private + * @function + */ +export const readIdMap = decoder => { + const idmap = new IdMap() + const numClients = decoding.readVarUint(decoder.restDecoder) + /** + * @type {Array>} + */ + const visitedAttributions = [] + /** + * @type {Array} + */ + 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>} + */ + const attrRanges = [] + for (let i = 0; i < numberOfDeletes; i++) { + const rangeClock = decoder.readDsClock() + const rangeLen = decoder.readDsLen() + /** + * @type {Array>} + */ + 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} + */ +export const decodeIdMap = data => readIdMap(new IdSetDecoderV2(decoding.createDecoder(data))) + +/** + * @template Attrs + * @param {IdMap} idmap + * @param {Array>} attrs + * @return {Array>} + */ +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} ISet + * @param {ISet} set + * @param {IdSet | IdMap} 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} idmap + * @param {(attr: Array>) => boolean} predicate + * @return {IdMap} + */ +export const filterIdMap = (idmap, predicate) => { + const filtered = createIdMap() + idmap.clients.forEach((ranges, client) => { + /** + * @type {Array>} + */ + 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 } diff --git a/src/utils/isParentOf.js b/src/utils/isParentOf.js index cdf8ff62..3f138394 100644 --- a/src/utils/isParentOf.js +++ b/src/utils/isParentOf.js @@ -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 } diff --git a/src/utils/logging.js b/src/utils/logging.js index dee4e3b4..5f3459b1 100644 --- a/src/utils/logging.js +++ b/src/utils/logging.js @@ -1,7 +1,3 @@ -import { - YType // eslint-disable-line -} from '../internals.js' - /** * Convenient helper to log type information. * diff --git a/src/utils/meta.js b/src/utils/meta.js index ce17bbd8..72f76c69 100644 --- a/src/utils/meta.js +++ b/src/utils/meta.js @@ -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, deletes: import('./IdMap.js').IdMap }} ContentMap + * @typedef {{ inserts: IdMap, deletes: IdMap }} 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} 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} 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} inserts - * @param {import('./IdMap.js').IdMap} deletes + * @param {IdMap} inserts + * @param {IdMap} deletes */ export const createContentMap = (inserts, deletes) => ({ inserts, deletes }) /** * @param {ContentIds} contentIds - * @param {Array>} insertAttrs - * @param {Array>} [deleteAttrs] + * @param {Array>} insertAttrs + * @param {Array>} [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>)=>boolean} insertPredicate - * @param {(c:Array>)=>boolean} deletePredicate + * @param {(c:Array>)=>boolean} insertPredicate + * @param {(c:Array>)=>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)) diff --git a/src/utils/schemas.js b/src/utils/schemas.js new file mode 100644 index 00000000..b7ac0076 --- /dev/null +++ b/src/utils/schemas.js @@ -0,0 +1,3 @@ +import * as s from 'lib0/schema' + +export const $ydoc = /** @type {s.Schema} */ (s.$type('y:doc', null)) diff --git a/src/utils/transaction-helpers.js b/src/utils/transaction-helpers.js new file mode 100644 index 00000000..204f9070 --- /dev/null +++ b/src/utils/transaction-helpers.js @@ -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} 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} 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} */ (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} */ (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} 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} 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} */ (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} */ (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} 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} startAttributes + * @param {Map} 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} + */ + 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 +} diff --git a/src/utils/updates.js b/src/utils/updates.js index a8d0598a..f1c3e9df 100644 --- a/src/utils/updates.js +++ b/src/utils/updates.js @@ -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>} updates - * @return {Uint8Array} - */ -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>} updates - * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] - * @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder] - * @return {Uint8Array} - */ -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} update - * @param {Uint8Array} 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 -} diff --git a/src/ytype.js b/src/ytype.js index b7c6283b..db2117c2 100644 --- a/src/ytype.js +++ b/src/ytype.js @@ -1,38 +1,38 @@ -import { - cleanupFormattingGap, - createIdSet, - removeEventHandlerListener, - callEventHandlerListeners, - addEventHandlerListener, - createEventHandler, - getState, - isVisible, - ContentType, - createID, - ContentAny, - ContentFormat, - ContentBinary, - ContentJSON, - ContentDeleted, - ContentString, - ContentEmbed, - getItemCleanStart, - noAttributionsManager, - transact, - ContentDoc, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, createAttributionFromAttributionItems, AbstractAttributionManager // eslint-disable-line -} from './internals.js' - -import * as contentType from './structs/ContentType.js' -import * as traits from 'lib0/traits' -import * as delta from 'lib0/delta' +import * as binary from 'lib0/binary' import * as array from 'lib0/array' -import * as map from 'lib0/map' -import * as iterator from 'lib0/iterator' +import * as delta from 'lib0/delta' import * as error from 'lib0/error' -import * as math from 'lib0/math' +import * as iterator from 'lib0/iterator' import * as log from 'lib0/logging' +import * as map from 'lib0/map' +import * as math from 'lib0/math' import * as object from 'lib0/object' import * as s from 'lib0/schema' +import * as traits from 'lib0/traits' +import { + Item, + ContentAny, + ContentBinary, + ContentDeleted, + ContentEmbed, + ContentFormat, + ContentJSON, + ContentString, + ContentType, + YXmlFragmentRefID, + YXmlElementRefID, + YXmlHookRefID, + ContentDoc, + createContentDocFromDoc +} from './structs/Item.js' +import { noAttributionsManager } from './utils/attribution-manager-helpers.js' +import { removeEventHandlerListener, callEventHandlerListeners, addEventHandlerListener, createEventHandler } from './utils/EventHandler.js' +import { createID } from './utils/ID.js' +import { createIdSet } from './utils/ids.js' +import { getItemCleanStart, cleanupFormattingGap } from './utils/transaction-helpers.js' +import { transact } from './utils/Transaction.js' +import { YEvent } from './utils/YEvent.js' +import { $ydoc } from './utils/schemas.js' /** * @typedef {Object|Array|number|null|string|Uint8Array|BigInt|YType} 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>?} 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>} + * @type {Array>} */ 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>} + * @type {Array>} */ 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|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>|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 : delta.Delta>} The Delta representation of this type. @@ -829,7 +867,7 @@ export class YType { */ const previousAttributes = {} // The value before changes /** - * @type {Array>} + * @type {Array>} */ 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 }} */ (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?} attrsToRender - * @param {import('./internals.js').AbstractAttributionManager} am + * @param {AbstractAttributionManager} am * @param {boolean} deep * @param {Set|Map|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>} + * @type {Array} */ 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} 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} + */ +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 +} diff --git a/test.html b/test.html index e10d7824..5229c5c0 100644 --- a/test.html +++ b/test.html @@ -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", diff --git a/tests/IdMap.tests.js b/tests/IdMap.tests.js index bf4c8006..409cabab 100644 --- a/tests/IdMap.tests.js +++ b/tests/IdMap.tests.js @@ -1,9 +1,9 @@ import * as t from 'lib0/testing' -import * as idmap from '../src/utils/IdMap.js' +import * as ids from '../src/utils/ids.js' import * as prng from 'lib0/prng' import * as math from 'lib0/math' import { compareIdmaps as compareIdMaps, createIdMap, ID, createRandomIdSet, createRandomIdMap, createContentAttribute } from './testHelper.js' -import * as YY from '../src/internals.js' +import * as YY from '../src/index.js' import * as time from 'lib0/time' /** @@ -80,16 +80,16 @@ export const testRepeatMergingMultipleIdMaps = tc => { const clients = 4 const clockRange = 5 /** - * @type {Array>} + * @type {Array>} */ 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') }) diff --git a/tests/IdSet.tests.js b/tests/IdSet.tests.js index 018971d5..a61ae8f2 100644 --- a/tests/IdSet.tests.js +++ b/tests/IdSet.tests.js @@ -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) } } } diff --git a/tests/encoding.tests.js b/tests/encoding.tests.js index e1c692f7..cce85fe6 100644 --- a/tests/encoding.tests.js +++ b/tests/encoding.tests.js @@ -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' diff --git a/tests/testHelper.js b/tests/testHelper.js index 26be9b23..89349c00 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -1,3 +1,4 @@ +import * as Y from '../src/index.js' import * as t from 'lib0/testing' import * as prng from 'lib0/prng' import * as encoding from 'lib0/encoding' @@ -5,13 +6,10 @@ import * as decoding from 'lib0/decoding' import * as syncProtocol from '@y/protocols/sync' import * as object from 'lib0/object' import * as map from 'lib0/map' -import * as Y from '../src/index.js' import * as math from 'lib0/math' import * as list from 'lib0/list' import * as delta from 'lib0/delta' -import { - createIdSet, createIdMap, addToIdSet, encodeIdMap -} from '../src/internals.js' +import { createIdSet, createIdMap, encodeIdMap } from '../src/utils/ids.js' export * from '../src/index.js' @@ -121,14 +119,14 @@ export class TestYInstance extends Y.Doc { if (!this.tc.onlineConns.has(this)) { this.tc.onlineConns.add(this) const encoder = encoding.createEncoder() - syncProtocol.writeSyncStep1(encoder, this) + syncProtocol.writeSyncStep1(encoder, /** @type {any} */ (this)) // publish SyncStep1 broadcastMessage(this, encoding.toUint8Array(encoder)) this.tc.onlineConns.forEach(remoteYInstance => { if (remoteYInstance !== this) { // remote instance sends instance to this instance const encoder = encoding.createEncoder() - syncProtocol.writeSyncStep1(encoder, remoteYInstance) + syncProtocol.writeSyncStep1(encoder, /** @type {any} */ (remoteYInstance)) this._receive(encoding.toUint8Array(encoder), remoteYInstance) } }) @@ -202,7 +200,7 @@ export class TestConnector { const encoder = encoding.createEncoder() // console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver)) // do not publish data created when this function is executed (could be ss2 or update message) - syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc) + syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, /** @type {any} */ (receiver), receiver.tc) if (encoding.length(encoder) > 0) { // send reply message sender._receive(encoding.toUint8Array(encoder), receiver) @@ -319,7 +317,7 @@ export const compareIdSets = (idSet1, idSet2) => { t.assert(items2 !== undefined && items1.length === items2.length) for (let i = 0; i < items1.length; i++) { const di1 = items1[i] - const di2 = /** @type {Array} */ (items2)[i] + const di2 = /** @type {Array} */ (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>} */ (items2)[i] + const di2 = /** @type {Array>} */ (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) diff --git a/tests/updates.tests.js b/tests/updates.tests.js index 255f225f..107b94c9 100644 --- a/tests/updates.tests.js +++ b/tests/updates.tests.js @@ -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' diff --git a/tests/y-map.tests.js b/tests/y-map.tests.js index 9c43cb72..cb97105d 100644 --- a/tests/y-map.tests.js +++ b/tests/y-map.tests.js @@ -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' diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index 293d727d..b7c073a1 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -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 diff --git a/tsconfig.json b/tsconfig.json index 99b70473..f5a725b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,6 @@ "moduleResolution": "nodenext", "paths": { "yjs": ["./src/index.js"], - "yjs/internals": ["./src/internals.js"], "yjs/testHelper": ["./tests/testHelper.js"] } },