├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .mocharc.json ├── .prettierignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── implementations │ ├── golf_string_fugue.ts │ ├── index.ts │ ├── opt_string_fugue.ts │ ├── string_fugue.ts │ └── tree_fugue.ts ├── index.ts ├── lex_ud_total_order.ts ├── uniquely_dense_total_order.ts └── utils.ts ├── test ├── implementations.ts ├── manual.test.ts ├── tsconfig.json └── utils.ts ├── tsconfig.json └── typedoc ├── .nojekyll ├── assets ├── highlight.css ├── icons.css ├── icons.png ├── icons@2x.png ├── main.js ├── search.js ├── style.css ├── widgets.png └── widgets@2x.png ├── classes ├── GolfStringFugue.html ├── LexUDTotalOrder.html ├── OptStringFugue.html ├── StringFugue.html └── TreeFugue.html ├── index.html ├── interfaces ├── TreePosition.html └── UniquelyDenseTotalOrder.html └── modules.html /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | typedoc 4 | test 5 | *.js 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | project: ["./tsconfig.json"], 11 | }, 12 | plugins: ["@typescript-eslint", "import"], 13 | extends: [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 17 | "plugin:import/typescript", 18 | "prettier", 19 | ], 20 | rules: { 21 | // Allow inference in function return type. 22 | "@typescript-eslint/explicit-function-return-type": "off", 23 | "@typescript-eslint/explicit-module-boundary-types": "off", 24 | // Remove "object" from ban-types banned list. 25 | // If eslint v8 worked with Atom, we could just upgrade 26 | // to that, which already unbans it. 27 | "@typescript-eslint/ban-types": [ 28 | "error", 29 | { 30 | types: { 31 | object: false, 32 | }, 33 | }, 34 | ], 35 | // I like non-null assertions. 36 | "@typescript-eslint/no-non-null-assertion": "off", 37 | // Disallow default exports; only allow named exports. 38 | "import/no-default-export": "error", 39 | // Impose alphabetically ordered imports. 40 | "import/order": "error", 41 | // Allow implicit string casts in template literals. 42 | "@typescript-eslint/restrict-template-expressions": "off", 43 | "@typescript-eslint/ban-ts-comment": [ 44 | "error", 45 | { 46 | // Need this for some mixins. 47 | "ts-expect-error": "allow-with-description", 48 | }, 49 | ], 50 | "@typescript-eslint/no-explicit-any": [ 51 | "error", 52 | { 53 | // Needed for mixin constructor args. See 54 | // https://github.com/typescript-eslint/typescript-eslint/issues/2408 55 | ignoreRestArgs: true, 56 | }, 57 | ], 58 | "@typescript-eslint/no-unused-vars": [ 59 | "warn", 60 | { 61 | // Allow unused parameter names that start with _, 62 | // like TypeScript does. 63 | argsIgnorePattern: "^_", 64 | }, 65 | ], 66 | "@typescript-eslint/no-this-alias": [ 67 | "error", 68 | { 69 | // Occasionally we need to reference an outer "this" 70 | // inside an object literal. 71 | allowedNames: ["outerThis"], 72 | }, 73 | ], 74 | "prefer-const": [ 75 | "error", 76 | { 77 | // Allow "let" destructuring assignments when not all 78 | // vars can be const. 79 | destructuring: "all", 80 | }, 81 | ], 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode/ 3 | .idea/ 4 | *.tsbuildinfo 5 | node_modules 6 | build 7 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": "test/**/*.test.ts", 4 | "require": "ts-node/register" 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | /typedoc/* 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Collabs contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uniquely Dense Total Orders 2 | 3 | Interface and implementations for a **uniquely dense total order** abstract data type. This is a concept similar to fractional indexing, but resilient to concurrent insertions. A uniquely dense total order can be used as the core of a list/text CRDT. 4 | 5 | This repo is a companion to the blog post [Fugue: A Basic List CRDT](https://mattweidner.com/2022/10/21/basic-list-crdt.html), which gives more info about the `UniquelyDenseTotalOrder` interface and the `Fugue` implementations. 6 | 7 | **Caution**: I have only minimally tested the implementations. 8 | 9 | For published versions of these implementations, see: 10 | 11 | - Tree-based: the [Collabs library's](https://collabs.readthedocs.io/en/latest/) list CRDTs ([CValueList](https://collabs.readthedocs.io/en/latest/api/collabs/classes/CValueList.html), [CText](https://collabs.readthedocs.io/en/latest/api/collabs/classes/CText.html), [CList](https://collabs.readthedocs.io/en/latest/api/collabs/classes/CList.html), [CRichText](https://collabs.readthedocs.io/en/latest/api/collabs/classes/CRichText.html)). 12 | - String-based: [position-strings](https://www.npmjs.com/package/position-strings) npm package. 13 | 14 | ## Docs 15 | 16 | Open [typedoc/index.html](typedoc/index.html). 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uniquely-dense-total-order", 3 | "description": "Uniquely Dense Total Orders for List/Text CRDTs", 4 | "author": "Matthew Weidner", 5 | "homepage": "https://github.com/mweidner037/uniquely-dense-total-order", 6 | "license": "Apache 2.0", 7 | "dependencies": { 8 | "seedrandom": "^3.0.5", 9 | "tslib": "^2.4.0" 10 | }, 11 | "devDependencies": { 12 | "@types/chai": "^4.3.1", 13 | "@types/mocha": "^9.1.0", 14 | "@types/seedrandom": "^3.0.2", 15 | "@typescript-eslint/eslint-plugin": "^5.23.0", 16 | "@typescript-eslint/parser": "^5.23.0", 17 | "chai": "^4.3.6", 18 | "cross-env": "^7.0.3", 19 | "eslint": "^7.32.0", 20 | "eslint-config-prettier": "^8.5.0", 21 | "eslint-plugin-import": "^2.26.0", 22 | "mocha": "^9.2.0", 23 | "npm-run-all": "^4.1.5", 24 | "pkg-ok": "^3.0.0", 25 | "prettier": "^2.6.2", 26 | "rimraf": "^3.0.2", 27 | "ts-node": "^10.7.0", 28 | "typedoc": "^0.22.15", 29 | "typescript": "^4.6.4" 30 | }, 31 | "scripts": { 32 | "build": "npm-run-all build:*", 33 | "build:tsc": "tsc -p tsconfig.json", 34 | "build:docs": "typedoc --out typedoc src/index.ts --tsconfig tsconfig.json", 35 | "test": "npm-run-all test:*", 36 | "test:lint": "eslint --ext .ts,.js .", 37 | "test:unit": "cross-env TS_NODE_PROJECT='./test/tsconfig.json' TS_NODE_SCOPE=true mocha", 38 | "test:pkg": "pkg-ok", 39 | "test:format": "prettier --check .", 40 | "fix": "npm-run-all fix:*", 41 | "fix:format": "prettier --write .", 42 | "clean": "rimraf build" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/implementations/golf_string_fugue.ts: -------------------------------------------------------------------------------- 1 | import { LexUDTotalOrder } from "../lex_ud_total_order"; 2 | import { randomReplicaID } from "../utils"; 3 | 4 | /** 5 | * A code golf version of [[StringFugue]]. 6 | * 7 | * Its positions are the same as those of [[StringFugue]] 8 | * except that they all start with an extra "R". 9 | * This implementation is just for code golf; [[StringFugue]] 10 | * has clearer code and 1-character-shorter positions. 11 | * 12 | * For a description of the algorithm, see 13 | * [https://mattweidner.com/2022/10/05/basic-list-crdt.html#intro-string-implementation](https://mattweidner.com/2022/10/05/basic-list-crdt.html#intro-string-implementation) 14 | */ 15 | export class GolfStringFugue extends LexUDTotalOrder { 16 | readonly id: string; 17 | private c = 0; 18 | 19 | /** 20 | * @param options.replicaID A unique replicaID. Must be unique among all 21 | * collaborating replicas, including past or concurrent replicas for the 22 | * same device or user. All collaborating replicas' replicaIDs must be the 23 | * same length. 24 | */ 25 | constructor(options: { replicaID?: string } = {}) { 26 | super(); 27 | this.id = options.replicaID ?? randomReplicaID(); 28 | } 29 | 30 | createBetween(a = "R", b = "Z"): string { 31 | return ( 32 | // "One line" List CRDT :) 33 | (b.startsWith(a) ? b.slice(0, -1) + "L" : a) + `${this.id}${this.c++}R` 34 | ); 35 | } 36 | 37 | // ... 38 | } 39 | -------------------------------------------------------------------------------- /src/implementations/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./golf_string_fugue"; 2 | export * from "./opt_string_fugue"; 3 | export * from "./string_fugue"; 4 | export * from "./tree_fugue"; 5 | -------------------------------------------------------------------------------- /src/implementations/opt_string_fugue.ts: -------------------------------------------------------------------------------- 1 | import { LexUDTotalOrder } from "../lex_ud_total_order"; 2 | import { randomReplicaID } from "../utils"; 3 | 4 | /** 5 | * An optimized version of [[StringFugue]]. 6 | * 7 | * For a description of the algorithm, see 8 | * [https://mattweidner.com/2022/10/05/basic-list-crdt.html#intro-string-implementation](https://mattweidner.com/2022/10/05/basic-list-crdt.html#intro-string-implementation) 9 | * 10 | * The difference from [[StringFugue]] (the linked algorithm) 11 | * is a common-case optimization: left-to-right insertions 12 | * by the same replica reuse the same (replicaID, counter) 13 | * pair (we call this a _waypoint_), just using 14 | * an extra _valueIndex_ to distinguish positions 15 | * within the sequence, instead of creating a long 16 | * rightward path in the tree. In this way, 17 | * a sequence of m left-to-right insertions see their 18 | * positions grow by O(log(m)) length (the size of 19 | * valueIndex) instead of O(m) length (the size of 20 | * a path containing one node per insertion). 21 | * 22 | * In more detail, the underlying tree consists of alternating 23 | * layers: 24 | * - Nodes in even layers (starting with the root's children) 25 | * are __waypoints__, each labeled by a pair (replicaID, counter). A waypoint can be either a left or right 26 | * child of its parent, except that the root only has right 27 | * children. Waypoint same-siblings siblings are sorted the 28 | * same as nodes in [[LexSimpleTotalOrder]]. 29 | * - Nodes in odd layers are __value indices__, each labelled 30 | * by a nonnegative integer. A value index is always a right 31 | * child of its parent. Value indices are sorted 32 | * *lexicographically*; we use a subset of numbers for which 33 | * this coincides with the usual order by magnitude. 34 | * 35 | * Each position corresponds to a value index node in the tree 36 | * whose parent waypoint's replicaID equals the position's 37 | * creator. A position is a string description of the path 38 | * from the root to its node (excluding the root). 39 | * Each pair of nodes (waypoint = (replicaID, counter), valueIndex) 40 | * is represented by the substring (here written like a template literal): 41 | * ``` 42 | * ${replicaID},${counter},${valueIndex}${L or R} 43 | * ``` 44 | * where the final value is L if the next node is a left 45 | * child, else R (including if this pair is the final node pair 46 | * to ensure that a terminal node is sorted in between its 47 | * left and right children). 48 | */ 49 | export class OptStringFugue extends LexUDTotalOrder { 50 | /** 51 | * Local replica ID, set in constructor. 52 | * All replicaIDs have the same length. 53 | */ 54 | readonly replicaID: string; 55 | /** 56 | * Maps counter to the most recently used 57 | * valueIndex for the waypoint (this.replicaID, counter). 58 | */ 59 | private lastValueIndices: number[] = []; 60 | 61 | /** 62 | * @param options.replicaID A unique replicaID. Must be unique among all 63 | * collaborating replicas, including past or concurrent replicas for the 64 | * same device or user. All collaborating replicas' replicaIDs must be the 65 | * same length. 66 | */ 67 | constructor(options: { replicaID?: string } = {}) { 68 | super(); 69 | this.replicaID = options.replicaID ?? randomReplicaID(); 70 | } 71 | 72 | createBetween(a: string | undefined, b: string | undefined): string { 73 | if (b !== undefined && (a === undefined || b.startsWith(a))) { 74 | // Left child of b. 75 | return b.slice(0, -1) + "L" + this.newWaypointNode(); 76 | } else { 77 | // Right child of a. 78 | if (a === undefined) return this.newWaypointNode(); 79 | else { 80 | // Check if we can reuse a's leaf waypoint. 81 | // For this to happen, a's leaf waypoint must have also 82 | // been sent by us, and its next valueIndex must not 83 | // have been used already (i.e., the node matches 84 | // this.lastValueIndices). 85 | const lastComma = a.lastIndexOf(","); 86 | const secondLastComma = a.lastIndexOf(",", lastComma); 87 | const leafSender = a.slice( 88 | secondLastComma - this.replicaID.length, 89 | secondLastComma 90 | ); 91 | if (leafSender === this.replicaID) { 92 | const leafCounter = Number.parseInt( 93 | a.slice(secondLastComma + 1, lastComma) 94 | ); 95 | const leafValueIndex = Number.parseInt(a.slice(lastComma + 1, -1)); 96 | if (this.lastValueIndices[leafCounter] === leafValueIndex) { 97 | // Success; reuse a's leaf waypoint. 98 | const valueIndex = lexSucc(leafValueIndex); 99 | this.lastValueIndices[leafCounter] = valueIndex; 100 | return a.slice(0, lastComma + 1) + valueIndex.toString() + "R"; 101 | } 102 | } 103 | // Failure; cannot reuse a's leaf waypoint. 104 | return a + this.newWaypointNode(); 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Returns a node corresponding to a new waypoint, also 111 | * updating this.lastValueIndices accordingly. 112 | */ 113 | private newWaypointNode(): string { 114 | const counter = this.lastValueIndices.length; 115 | this.lastValueIndices.push(0); 116 | return `${this.replicaID},${counter},0R`; 117 | } 118 | } 119 | 120 | /** 121 | * Returns the successor of n in an enumeration of a special 122 | * set of numbers. 123 | * 124 | * That enumeration has the following properties: 125 | * 1. Each number is a nonnegative integer (however, not all 126 | * nonnegative integers are enumerated). 127 | * 2. The number's decimal representations are enumerated in 128 | * lexicographic order, with no prefixes (i.e., no decimal 129 | * representation is a prefix of another). 130 | * 3. The n-th enumerated number has O(log(n)) decimal digits. 131 | * 132 | * Properties (2) and (3) are analogous to normal counting, 133 | * with the usual order by magnitude; the novelty here is that 134 | * we instead use the lexicographic order on decimal representations. 135 | * It is also the case that 136 | * the numbers are in order by magnitude, although we do not 137 | * use this property. 138 | * 139 | * The specific enumeration is: 140 | * - Start with 0. 141 | * - Enumerate 9^0 numbers (i.e., just 0). 142 | * - Add 1, multiply by 10, then enumerate 9^1 numbers (i.e., 143 | * 10, 11, ..., 18). 144 | * - Add 1, multiply by 10, then enumerate 9^2 numbers (i.e., 145 | * 190, 191, ..., 270). 146 | * - Repeat this pattern indefinitely, enumerating 147 | * 9^(d-1) d-digit numbers for each d >= 1. 148 | */ 149 | function lexSucc(n: number): number { 150 | const d = n === 0 ? 1 : Math.floor(Math.log10(n)) + 1; 151 | if (n === Math.pow(10, d) - Math.pow(9, d) - 1) { 152 | // n -> (n + 1) * 10 153 | return (n + 1) * 10; 154 | } else { 155 | // n -> n + 1 156 | return n + 1; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/implementations/string_fugue.ts: -------------------------------------------------------------------------------- 1 | import { LexUDTotalOrder } from "../lex_ud_total_order"; 2 | import { randomReplicaID } from "../utils"; 3 | 4 | /** 5 | * A simple [[LexUDTotalOrder]] implementing the Fugue algorithm. 6 | * 7 | * For a description of the algorithm, see 8 | * [https://mattweidner.com/2022/10/05/basic-list-crdt.html#intro-string-implementation](https://mattweidner.com/2022/10/05/basic-list-crdt.html#intro-string-implementation) 9 | */ 10 | export class StringFugue extends LexUDTotalOrder { 11 | /** 12 | * Local replica ID, set in constructor. 13 | * All replicaIDs have the same length. 14 | */ 15 | readonly replicaID: string; 16 | // Local counter. 17 | private counter = 0; 18 | 19 | /** 20 | * @param options.replicaID A unique replicaID. Must be unique among all 21 | * collaborating replicas, including past or concurrent replicas for the 22 | * same device or user. All collaborating replicas' replicaIDs must be the 23 | * same length. 24 | */ 25 | constructor(options: { replicaID?: string } = {}) { 26 | super(); 27 | this.replicaID = options.replicaID ?? randomReplicaID(); 28 | } 29 | 30 | createBetween(a: string | undefined, b: string | undefined): string { 31 | // A globally unique new string, in the form of a causal dot. 32 | const uniqueStr = `${this.replicaID}${this.counter++}`; 33 | 34 | if (a !== undefined && b !== undefined) { 35 | if (!b.startsWith(a)) { 36 | // a is not a prefix of b. 37 | return a + uniqueStr + "R"; 38 | } else { 39 | // a is a strict prefix of b. 40 | const bWithL = b.slice(0, -1) + "L"; 41 | return bWithL + uniqueStr + "R"; 42 | } 43 | } else { 44 | // Edge cases. 45 | if (b === undefined) { 46 | // Treat a (possibly undefined) as not a prefix of b. 47 | return (a ?? "") + uniqueStr + "R"; 48 | } else { 49 | // Treat a (undefined) as a strict prefix of b. 50 | const bWithL = b.slice(0, -1) + "L"; 51 | return bWithL + uniqueStr + "R"; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/implementations/tree_fugue.ts: -------------------------------------------------------------------------------- 1 | import { UniquelyDenseTotalOrder } from "../uniquely_dense_total_order"; 2 | import { randomReplicaID } from "../utils"; 3 | 4 | export interface TreePosition { 5 | sender: string; 6 | counter: number; 7 | } 8 | 9 | interface InternalPosition { 10 | sender: string; 11 | counter: number; 12 | /** Root is null */ 13 | parent: InternalPosition | null; 14 | /** true for left child, false for right child. */ 15 | leftChild: boolean; 16 | /** Root is 0, so this is >= 1. */ 17 | depth: number; 18 | } 19 | 20 | interface NewPositionMessage { 21 | sender: string; 22 | counter: number; 23 | parent: TreePosition | null; 24 | leftChild: boolean; 25 | } 26 | 27 | /** 28 | * A simple [[UniquelyDenseTotalOrder]] implementing the Fugue algorithm. 29 | * 30 | * For a description of the algorithm, see 31 | * [https://mattweidner.com/2022/10/05/basic-list-crdt.html#tree-implementation](https://mattweidner.com/2022/10/05/basic-list-crdt.html#tree-implementation) 32 | */ 33 | export class TreeFugue implements UniquelyDenseTotalOrder { 34 | /** 35 | * Local replica ID, set in constructor. 36 | */ 37 | readonly replicaID: string; 38 | 39 | /** 40 | * The tree of all known (created or [[receive]]d) positions, represented 41 | * as a collection of [[InternalPosition]]s. Specifically, this collection 42 | * maps sender, then counter, to the corresponding [[InternalPosition]]. 43 | * For each position, there is a unique InternalPosition object, so they are 44 | * safe to compare by-reference. 45 | */ 46 | private readonly state = new Map(); 47 | 48 | /** 49 | * @param send When called, sends message to all other replica's [[receive]] 50 | * functions, at-least-once in causal order. Also, if send is called 51 | * during a [[createBetween]] call, then other replicas must 52 | * [[receive]] the message before using the created position 53 | * (i.e., passing it to [[compare]] or [[createBetween]]). 54 | * @param options.replicaID A unique replicaID. Must be unique among all 55 | * collaborating replicas, including past or concurrent replicas for the 56 | * same device or user. 57 | */ 58 | constructor( 59 | private readonly send: (message: string) => void, 60 | options: { replicaID?: string } = {} 61 | ) { 62 | this.replicaID = options.replicaID ?? randomReplicaID(); 63 | this.state.set(this.replicaID, []); 64 | } 65 | 66 | /** 67 | * Returns the [[InternalPosition]] corresponding to pos. 68 | * 69 | * Throws an error if pos is not known. 70 | */ 71 | private getInternalPos(pos: TreePosition): InternalPosition { 72 | const ans = this.state.get(pos.sender)?.[pos.counter]; 73 | if (ans === undefined) { 74 | throw new Error("Unknown position: " + JSON.stringify(pos)); 75 | } 76 | return ans; 77 | } 78 | 79 | compare(a: TreePosition, b: TreePosition): number { 80 | let aAnc = this.getInternalPos(a); 81 | let bAnc = this.getInternalPos(b); 82 | // Compare a and b in the tree walk. 83 | // 0. Shortcut: equal case. 84 | if (a === b) return 0; 85 | // 1. Walk one up the tree until they are the same depth. 86 | let lastMove: ["a" | "b", boolean] | null = null; 87 | while (aAnc.depth > bAnc.depth) { 88 | lastMove = ["a", aAnc.leftChild]; 89 | aAnc = aAnc.parent!; 90 | } 91 | while (bAnc.depth > aAnc.depth) { 92 | lastMove = ["b", bAnc.leftChild]; 93 | bAnc = bAnc.parent!; 94 | } 95 | // 2. If one is an ancestor of the other, short by the descendant's 96 | // next side (L or R). 97 | if (aAnc === bAnc) { 98 | // lastMove is [which is the descendant, true if it's a left descendant]. 99 | return (lastMove![0] === "a" ? 1 : -1) * (lastMove![1] ? -1 : 1); 100 | } 101 | // 3. Walk both up the tree until they have a common parent. 102 | while (aAnc.parent !== bAnc.parent) { 103 | aAnc = aAnc.parent!; 104 | bAnc = bAnc.parent!; 105 | } 106 | // 4. Compare as siblings: order first by side, then sender, then counter. 107 | if (aAnc.leftChild && !bAnc.leftChild) return -1; 108 | else if (!aAnc.leftChild && bAnc.leftChild) return 1; 109 | else { 110 | if (aAnc.sender < bAnc.sender) return -1; 111 | else if (aAnc.sender > bAnc.sender) return 1; 112 | else return aAnc.counter - bAnc.counter; 113 | } 114 | } 115 | 116 | createBetween( 117 | a: TreePosition | undefined, 118 | b: TreePosition | undefined 119 | ): TreePosition { 120 | // Determine where to place newPos in the tree, following Fugue alg: 121 | // - If a is *not* an ancestor of b, newPos is a right child of a. 122 | // - Else, newPos is a left child of b. 123 | let isAnc = false; 124 | if (b !== undefined) { 125 | if (a === undefined) isAnc = true; 126 | else { 127 | const aInt = this.getInternalPos(a); 128 | const bInt = this.getInternalPos(b); 129 | if (bInt.depth > aInt.depth) { 130 | let bAnc = bInt; 131 | while (bAnc.depth > aInt.depth) { 132 | bAnc = bAnc.parent!; 133 | } 134 | if (bAnc === aInt) isAnc = true; 135 | } 136 | } 137 | } 138 | // A globally unique new position, in the form of a causal dot. 139 | const newPos = { 140 | sender: this.replicaID, 141 | counter: this.state.get(this.replicaID)!.length, 142 | }; 143 | let newIntPos: InternalPosition; 144 | if (isAnc) { 145 | // Left child of b. 146 | if (b === undefined) { 147 | newIntPos = { ...newPos, parent: null, leftChild: true, depth: 1 }; 148 | } else { 149 | const bInt = this.getInternalPos(b); 150 | newIntPos = { 151 | ...newPos, 152 | parent: bInt, 153 | leftChild: true, 154 | depth: bInt.depth + 1, 155 | }; 156 | } 157 | } else { 158 | // Right child of a. 159 | if (a === undefined) { 160 | newIntPos = { ...newPos, parent: null, leftChild: false, depth: 1 }; 161 | } else { 162 | const aInt = this.getInternalPos(a); 163 | newIntPos = { 164 | ...newPos, 165 | parent: aInt, 166 | leftChild: false, 167 | depth: aInt.depth + 1, 168 | }; 169 | } 170 | } 171 | 172 | // Store locally. 173 | this.state.get(this.replicaID)!.push(newIntPos); 174 | 175 | // Broadcast metadata about newPos. 176 | const message = { 177 | ...newPos, 178 | parent: 179 | newIntPos.parent === null 180 | ? null 181 | : { 182 | sender: newIntPos.parent.sender, 183 | counter: newIntPos.parent.counter, 184 | }, 185 | leftChild: newIntPos.leftChild, 186 | }; 187 | this.send(JSON.stringify(message)); 188 | 189 | return newPos; 190 | } 191 | 192 | /** 193 | * See the description of the constructor's `send` argument. 194 | */ 195 | receive(message: string): void { 196 | const decoded = JSON.parse(message); 197 | let bySender = this.state.get(decoded.sender); 198 | if (bySender === undefined) { 199 | bySender = []; 200 | this.state.set(decoded.sender, bySender); 201 | } 202 | // Ignore duplicates (at-least-once delivery). 203 | if (decoded.counter < bySender.length) return; 204 | // Require causal order. 205 | if (decoded.counter > bySender.length) { 206 | throw new Error( 207 | 'Message delivered out of causal order: "' + message + '"' 208 | ); 209 | } 210 | // Good case. 211 | const parentInt = 212 | decoded.parent === null ? null : this.getInternalPos(decoded.parent); 213 | bySender.push({ 214 | sender: decoded.sender, 215 | counter: decoded.counter, 216 | parent: parentInt, 217 | leftChild: decoded.leftChild, 218 | depth: (parentInt?.depth ?? 0) + 1, 219 | }); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./uniquely_dense_total_order"; 2 | export * from "./lex_ud_total_order"; 3 | export * from "./implementations"; 4 | -------------------------------------------------------------------------------- /src/lex_ud_total_order.ts: -------------------------------------------------------------------------------- 1 | import { UniquelyDenseTotalOrder } from "./uniquely_dense_total_order"; 2 | 3 | /** 4 | * A [[UniquelyDenseTotalOrder]] that sorts using the lexicographic 5 | * order on strings. 6 | * 7 | * This is useful in contexts where you can't specify a 8 | * custom [[compare]] function for sorts (e.g., a column 9 | * in a database table). However, it comes at the cost 10 | * of super-constant size positions (both on the network 11 | * and in memory). 12 | */ 13 | export abstract class LexUDTotalOrder 14 | implements UniquelyDenseTotalOrder 15 | { 16 | compare(a: string, b: string): number { 17 | return a < b ? -1 : a === b ? 0 : 1; 18 | } 19 | 20 | abstract createBetween(a: string | undefined, b: string | undefined): string; 21 | } 22 | -------------------------------------------------------------------------------- /src/uniquely_dense_total_order.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper interface for sorting and creating unique immutable positions, 3 | * suitable for use in a List CRDT. 4 | * 5 | * @typeParam P The type of positions. Treated as immutable. 6 | */ 7 | export interface UniquelyDenseTotalOrder

{ 8 | /** 9 | * Usual compare function for sorts: returns negative if a < b in 10 | * their sort order, positive if a > b. 11 | */ 12 | compare(a: P, b: P): number; 13 | 14 | /** 15 | * Returns a globally unique new position c such that a < c < b. 16 | * 17 | * "Globally unique" means that the created position must be distinct 18 | * from all other created positions, including ones created concurrently 19 | * by other users. 20 | * 21 | * When a is undefined, it is treated as the start of the list, i.e., 22 | * this returns c such that c < b. Likewise, undefined b is treated 23 | * as the end of the list. 24 | */ 25 | createBetween(a: P | undefined, b: P | undefined): P; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copied from @collabs/core (src/core/random_replica_id). 2 | 3 | import * as crypto from "crypto"; 4 | import seedrandom from "seedrandom"; 5 | 6 | const BASE64CHARS = 7 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 8 | 9 | /** 10 | * The default length of a replicaID, in characters. 11 | * 12 | * Rationale for value 10: 13 | * Each character of the replicaID gives us 6 bits of entropy, 14 | * for a total of 60 bits. This gives a < 1% 15 | * probability that two replicas in the same conversation 16 | * will ever choose the same replicaID's, even if we 17 | * consider the total probability across 100,000,000 18 | * conversations with 10,000 replicaIDs each 19 | * (= 10 users * 1,000 days * 1 replica/user/day). 20 | */ 21 | export const DEFAULT_REPLICA_ID_LENGTH = 10; 22 | 23 | /** 24 | * @return A random replicaID made of base64 characters. 25 | * Such replicaID's can be safely treated as either 26 | * byte arrays or UTF-8 strings, and they are printable. 27 | */ 28 | export function randomReplicaID( 29 | length: number = DEFAULT_REPLICA_ID_LENGTH 30 | ): string { 31 | const arr = new Array(length); 32 | let randomValues = new Uint8Array(length); 33 | if (typeof window === "undefined") { 34 | // Use Node crypto library. 35 | // We use eval("require") to prevent Webpack from attempting 36 | // to bundle the crypto module and complaining. 37 | // In theory we should also be able to do this by 38 | // adding "browser": {"crypto": false} to package.json, 39 | // but that is not working, and besides, every user 40 | // of this package would have to remember to do so. 41 | // See https://github.com/webpack/webpack/issues/8826 42 | const cryptoReal = ( 43 | (eval("require"))("crypto") 44 | ); 45 | const randomBuffer = cryptoReal.randomBytes(length); 46 | randomValues = new Uint8Array(randomBuffer); 47 | } else { 48 | // Use browser crypto library. 49 | window.crypto.getRandomValues(randomValues); 50 | } 51 | for (let i = 0; i < length; i++) { 52 | // Here we exploit the fact that 128 divides 256. 53 | // This would be biased otherwise. 54 | arr[i] = BASE64CHARS[randomValues[i] % 64]; 55 | } 56 | return arr.join(""); 57 | } 58 | 59 | /** 60 | * For debugging/testing/benchmarking purposes. 61 | * Like [[randomReplicaID]] but using pseudo-randomness 62 | * instead of cryptographic randomness. 63 | * 64 | * This can be passed to [[CRDTApp]]'s `debugReplicaID` option. 65 | * It is recommended to do so for tests and benchmarks. 66 | * 67 | * @param rng The psuedo-random number generator, from 68 | * npm package "seedrandom". 69 | * @return A psueod-random replicaID made of base64 characters. 70 | * Such replicaID's can be safely treated as either 71 | * byte arrays or UTF-8 strings, and they are printable. 72 | */ 73 | export function pseudoRandomReplicaID( 74 | rng: seedrandom.PRNG, 75 | length: number = DEFAULT_REPLICA_ID_LENGTH 76 | ) { 77 | const arr = new Array(length); 78 | for (let i = 0; i < arr.length; i++) { 79 | arr[i] = BASE64CHARS[Math.floor(rng() * 64)]; 80 | } 81 | return arr.join(""); 82 | } 83 | -------------------------------------------------------------------------------- /test/implementations.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GolfStringFugue, 3 | OptStringFugue, 4 | StringFugue, 5 | TreeFugue, 6 | } from "../src/implementations"; 7 | import { pseudoRandomReplicaID } from "../src/utils"; 8 | import { GroupFactory } from "./utils"; 9 | 10 | export const IMPLEMENTATIONS: { [name: string]: GroupFactory } = { 11 | GolfStringFugue: () => (rng) => 12 | new GolfStringFugue({ replicaID: pseudoRandomReplicaID(rng) }), 13 | OptStringFugue: () => (rng) => 14 | new OptStringFugue({ replicaID: pseudoRandomReplicaID(rng) }), 15 | StringFugue: () => (rng) => 16 | new StringFugue({ replicaID: pseudoRandomReplicaID(rng) }), 17 | TreeFugue: () => { 18 | const replicas: TreeFugue[] = []; 19 | // send function: immediate in-order broadcast. 20 | function send(message: string) { 21 | replicas.forEach((replica) => replica.receive(message)); 22 | } 23 | return function (rng) { 24 | const replica = new TreeFugue(send, { 25 | replicaID: pseudoRandomReplicaID(rng), 26 | }); 27 | replicas.push(replica); 28 | return replica; 29 | }; 30 | }, 31 | } as const; 32 | -------------------------------------------------------------------------------- /test/manual.test.ts: -------------------------------------------------------------------------------- 1 | import seedrandom from "seedrandom"; 2 | import { UniquelyDenseTotalOrder } from "../src/uniquely_dense_total_order"; 3 | import { IMPLEMENTATIONS } from "./implementations"; 4 | import { 5 | assertIsOrdered, 6 | assertIsOrderedAll, 7 | GroupFactory, 8 | safeCreateBetween, 9 | } from "./utils"; 10 | 11 | for (const [name, newGroup] of Object.entries(IMPLEMENTATIONS)) { 12 | describe(name, () => { 13 | doManualTests(newGroup); 14 | }); 15 | } 16 | 17 | function doManualTests

(newGroup: GroupFactory

) { 18 | describe("manual", () => { 19 | describe("single user", () => { 20 | let rng: seedrandom.PRNG; 21 | let alice: UniquelyDenseTotalOrder

; 22 | 23 | beforeEach(() => { 24 | rng = seedrandom("42"); 25 | alice = newGroup()(rng); 26 | }); 27 | 28 | it("LTR", () => { 29 | let last: P | undefined = undefined; 30 | const list: P[] = []; 31 | for (let i = 0; i < 20; i++) { 32 | last = safeCreateBetween(alice, last, undefined); 33 | list.push(last); 34 | } 35 | assertIsOrdered(alice, list); 36 | }); 37 | 38 | it("RTL", () => { 39 | let last: P | undefined = undefined; 40 | const list: P[] = []; 41 | for (let i = 0; i < 20; i++) { 42 | last = safeCreateBetween(alice, undefined, last); 43 | list.unshift(last); 44 | } 45 | assertIsOrdered(alice, list); 46 | }); 47 | 48 | it("restart", () => { 49 | const list: P[] = []; 50 | for (let j = 0; j < 5; j++) { 51 | let last: P | undefined = undefined; 52 | let after = j === 0 ? undefined : list[0]; 53 | for (let i = 0; i < 10; i++) { 54 | last = safeCreateBetween(alice, last, after); 55 | list.splice(i, 0, last); 56 | } 57 | } 58 | assertIsOrdered(alice, list); 59 | }); 60 | }); 61 | 62 | describe("two users", () => { 63 | let rng: seedrandom.PRNG; 64 | let alice: UniquelyDenseTotalOrder

; 65 | let bob: UniquelyDenseTotalOrder

; 66 | 67 | beforeEach(() => { 68 | rng = seedrandom("42"); 69 | const group = newGroup(); 70 | alice = group(rng); 71 | bob = group(rng); 72 | }); 73 | 74 | it("LTR sequential", () => { 75 | let last: P | undefined = undefined; 76 | const list: P[] = []; 77 | for (let i = 0; i < 40; i++) { 78 | const user = i >= 20 ? bob : alice; 79 | last = safeCreateBetween(user, last, undefined); 80 | list.push(last); 81 | } 82 | assertIsOrderedAll([alice, bob], list); 83 | }); 84 | 85 | it("LTR alternating", () => { 86 | let last: P | undefined = undefined; 87 | const list: P[] = []; 88 | for (let i = 0; i < 40; i++) { 89 | const user = i % 2 === 0 ? bob : alice; 90 | last = safeCreateBetween(user, last, undefined); 91 | list.push(last); 92 | } 93 | assertIsOrderedAll([alice, bob], list); 94 | }); 95 | 96 | it("RTL sequential", () => { 97 | let last: P | undefined = undefined; 98 | const list: P[] = []; 99 | for (let i = 0; i < 40; i++) { 100 | const user = i >= 20 ? bob : alice; 101 | last = safeCreateBetween(user, undefined, last); 102 | list.unshift(last); 103 | } 104 | assertIsOrderedAll([alice, bob], list); 105 | }); 106 | 107 | it("RTL alternating", () => { 108 | let last: P | undefined = undefined; 109 | const list: P[] = []; 110 | for (let i = 0; i < 40; i++) { 111 | const user = i % 2 === 0 ? bob : alice; 112 | last = safeCreateBetween(user, undefined, last); 113 | list.unshift(last); 114 | } 115 | assertIsOrderedAll([alice, bob], list); 116 | }); 117 | 118 | it("restart alternating", () => { 119 | const list: P[] = []; 120 | for (let j = 0; j < 5; j++) { 121 | let last: P | undefined = undefined; 122 | let after = j === 0 ? undefined : list[0]; 123 | for (let i = 0; i < 10; i++) { 124 | const user = i % 2 === 0 ? bob : alice; 125 | last = safeCreateBetween(user, last, after); 126 | list.splice(i, 0, last); 127 | } 128 | } 129 | assertIsOrderedAll([alice, bob], list); 130 | }); 131 | 132 | it("LTR concurrent", () => { 133 | let last: P | undefined = undefined; 134 | const list1: P[] = []; 135 | for (let i = 0; i < 20; i++) { 136 | last = safeCreateBetween(alice, last, undefined); 137 | list1.push(last); 138 | } 139 | last = undefined; 140 | const list2: P[] = []; 141 | for (let i = 0; i < 20; i++) { 142 | last = safeCreateBetween(bob, last, undefined); 143 | list2.push(last); 144 | } 145 | // list1 and list2 should be sorted one after the other, according 146 | // to their first element (non-interleaving). 147 | let list: P[]; 148 | if (alice.compare(list1[0], list2[0]) < 0) { 149 | // list1 < list2 150 | list = [...list1, ...list2]; 151 | } else list = [...list2, ...list1]; 152 | assertIsOrderedAll([alice, bob], list); 153 | }); 154 | 155 | it("RTL concurrent", () => { 156 | let last: P | undefined = undefined; 157 | const list1: P[] = []; 158 | for (let i = 0; i < 20; i++) { 159 | last = safeCreateBetween(alice, undefined, last); 160 | list1.unshift(last); 161 | } 162 | last = undefined; 163 | const list2: P[] = []; 164 | for (let i = 0; i < 20; i++) { 165 | last = safeCreateBetween(bob, undefined, last); 166 | list2.unshift(last); 167 | } 168 | // list1 and list2 should be sorted one after the other, according 169 | // to their first element (non-interleaving). 170 | let list: P[]; 171 | if (alice.compare(list1[0], list2[0]) < 0) { 172 | // list1 < list2 173 | list = [...list1, ...list2]; 174 | } else list = [...list2, ...list1]; 175 | assertIsOrderedAll([alice, bob], list); 176 | }); 177 | 178 | it("insert between concurrent", () => { 179 | // "Hard case" from the blog post - see 180 | // https://mattweidner.com/2022/10/05/basic-list-crdt.html#between-concurrent 181 | const a = safeCreateBetween(alice, undefined, undefined); 182 | const b = safeCreateBetween(alice, a, undefined); 183 | 184 | let c = safeCreateBetween(alice, a, b); 185 | let d = safeCreateBetween(bob, a, b); 186 | // Order so c < d. 187 | if (alice.compare(c, d) > 0) [c, d] = [d, c]; 188 | 189 | // Try making e on both alice and bob. 190 | let e1 = safeCreateBetween(alice, c, d); 191 | let e2 = safeCreateBetween(bob, c, d); 192 | 193 | assertIsOrderedAll([alice, bob], [a, c, e1, d, b]); 194 | assertIsOrderedAll([alice, bob], [a, c, e2, d, b]); 195 | }); 196 | }); 197 | }); 198 | } 199 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* I put this file here instead of in the root dir with 3 | * a special name, beacuse otherwise I cannot figure out 4 | * how to get atom-typescript to recognize any settings 5 | * for the test/ folder. 6 | * See https://github.com/TypeStrong/atom-typescript/issues/1250 7 | */ 8 | "extends": "../tsconfig.json", 9 | "compilerOptions": { 10 | "rootDir": "..", 11 | "module": "commonjs" 12 | }, 13 | "include": ["../src", "../test"] 14 | } 15 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import seedrandom from "seedrandom"; 3 | import { UniquelyDenseTotalOrder } from "../src/uniquely_dense_total_order"; 4 | 5 | export type ReplicaFactory

= ( 6 | rng: seedrandom.PRNG 7 | ) => UniquelyDenseTotalOrder

; 8 | export type GroupFactory

= () => ReplicaFactory

; 9 | 10 | export function safeCreateBetween

( 11 | order: UniquelyDenseTotalOrder

, 12 | a: P | undefined, 13 | b: P | undefined 14 | ): P { 15 | if (a !== undefined && b !== undefined) { 16 | assert.isBelow(order.compare(a, b), 0, `${a}, ${b}`); 17 | assert.isAbove(order.compare(b, a), 0, `${a}, ${b}`); 18 | } 19 | const c = order.createBetween(a, b); 20 | if (a !== undefined) { 21 | assert.isBelow(order.compare(a, c), 0, `${a}, ${c}, ${b}`); 22 | assert.isAbove(order.compare(c, a), 0, `${a}, ${c}, ${b}`); 23 | } 24 | if (b !== undefined) { 25 | assert.isBelow(order.compare(c, b), 0, `${a}, ${c}, ${b}`); 26 | assert.isAbove(order.compare(b, c), 0, `${a}, ${c}, ${b}`); 27 | } 28 | assert.strictEqual(order.compare(c, c), 0, `${a}, ${c}, ${b}`); 29 | return c; 30 | } 31 | 32 | export function assertIsOrdered

( 33 | order: UniquelyDenseTotalOrder

, 34 | list: P[] 35 | ) { 36 | for (let i = 0; i < list.length; i++) { 37 | for (let j = i + 1; j < list.length; j++) { 38 | assert.isBelow(order.compare(list[i], list[j]), 0); 39 | assert.isAbove(order.compare(list[j], list[i]), 0); 40 | } 41 | } 42 | } 43 | 44 | export function assertIsOrderedAll

( 45 | orders: UniquelyDenseTotalOrder

[], 46 | list: P[] 47 | ) { 48 | orders.forEach((order) => assertIsOrdered(order, list)); 49 | } 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "target": "es2019", 5 | "module": "es2015", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "sourceMap": true, 12 | "importHelpers": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /typedoc/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /typedoc/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #001080; 3 | --dark-hl-0: #9CDCFE; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #0070C1; 7 | --dark-hl-2: #4FC1FF; 8 | --light-code-background: #F5F5F5; 9 | --dark-code-background: #1E1E1E; 10 | } 11 | 12 | @media (prefers-color-scheme: light) { :root { 13 | --hl-0: var(--light-hl-0); 14 | --hl-1: var(--light-hl-1); 15 | --hl-2: var(--light-hl-2); 16 | --code-background: var(--light-code-background); 17 | } } 18 | 19 | @media (prefers-color-scheme: dark) { :root { 20 | --hl-0: var(--dark-hl-0); 21 | --hl-1: var(--dark-hl-1); 22 | --hl-2: var(--dark-hl-2); 23 | --code-background: var(--dark-code-background); 24 | } } 25 | 26 | body.light { 27 | --hl-0: var(--light-hl-0); 28 | --hl-1: var(--light-hl-1); 29 | --hl-2: var(--light-hl-2); 30 | --code-background: var(--light-code-background); 31 | } 32 | 33 | body.dark { 34 | --hl-0: var(--dark-hl-0); 35 | --hl-1: var(--dark-hl-1); 36 | --hl-2: var(--dark-hl-2); 37 | --code-background: var(--dark-code-background); 38 | } 39 | 40 | .hl-0 { color: var(--hl-0); } 41 | .hl-1 { color: var(--hl-1); } 42 | .hl-2 { color: var(--hl-2); } 43 | pre, code { background: var(--code-background); } 44 | -------------------------------------------------------------------------------- /typedoc/assets/icons.css: -------------------------------------------------------------------------------- 1 | .tsd-kind-icon { 2 | display: block; 3 | position: relative; 4 | padding-left: 20px; 5 | text-indent: -20px; 6 | } 7 | .tsd-kind-icon:before { 8 | content: ""; 9 | display: inline-block; 10 | vertical-align: middle; 11 | width: 17px; 12 | height: 17px; 13 | margin: 0 3px 2px 0; 14 | background-image: url(./icons.png); 15 | } 16 | @media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { 17 | .tsd-kind-icon:before { 18 | background-image: url(./icons@2x.png); 19 | background-size: 238px 204px; 20 | } 21 | } 22 | 23 | .tsd-signature.tsd-kind-icon:before { 24 | background-position: 0 -153px; 25 | } 26 | 27 | .tsd-kind-object-literal > .tsd-kind-icon:before { 28 | background-position: 0px -17px; 29 | } 30 | .tsd-kind-object-literal.tsd-is-protected > .tsd-kind-icon:before { 31 | background-position: -17px -17px; 32 | } 33 | .tsd-kind-object-literal.tsd-is-private > .tsd-kind-icon:before { 34 | background-position: -34px -17px; 35 | } 36 | 37 | .tsd-kind-class > .tsd-kind-icon:before { 38 | background-position: 0px -34px; 39 | } 40 | .tsd-kind-class.tsd-is-protected > .tsd-kind-icon:before { 41 | background-position: -17px -34px; 42 | } 43 | .tsd-kind-class.tsd-is-private > .tsd-kind-icon:before { 44 | background-position: -34px -34px; 45 | } 46 | 47 | .tsd-kind-class.tsd-has-type-parameter > .tsd-kind-icon:before { 48 | background-position: 0px -51px; 49 | } 50 | .tsd-kind-class.tsd-has-type-parameter.tsd-is-protected 51 | > .tsd-kind-icon:before { 52 | background-position: -17px -51px; 53 | } 54 | .tsd-kind-class.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { 55 | background-position: -34px -51px; 56 | } 57 | 58 | .tsd-kind-interface > .tsd-kind-icon:before { 59 | background-position: 0px -68px; 60 | } 61 | .tsd-kind-interface.tsd-is-protected > .tsd-kind-icon:before { 62 | background-position: -17px -68px; 63 | } 64 | .tsd-kind-interface.tsd-is-private > .tsd-kind-icon:before { 65 | background-position: -34px -68px; 66 | } 67 | 68 | .tsd-kind-interface.tsd-has-type-parameter > .tsd-kind-icon:before { 69 | background-position: 0px -85px; 70 | } 71 | .tsd-kind-interface.tsd-has-type-parameter.tsd-is-protected 72 | > .tsd-kind-icon:before { 73 | background-position: -17px -85px; 74 | } 75 | .tsd-kind-interface.tsd-has-type-parameter.tsd-is-private 76 | > .tsd-kind-icon:before { 77 | background-position: -34px -85px; 78 | } 79 | 80 | .tsd-kind-namespace > .tsd-kind-icon:before { 81 | background-position: 0px -102px; 82 | } 83 | .tsd-kind-namespace.tsd-is-protected > .tsd-kind-icon:before { 84 | background-position: -17px -102px; 85 | } 86 | .tsd-kind-namespace.tsd-is-private > .tsd-kind-icon:before { 87 | background-position: -34px -102px; 88 | } 89 | 90 | .tsd-kind-module > .tsd-kind-icon:before { 91 | background-position: 0px -102px; 92 | } 93 | .tsd-kind-module.tsd-is-protected > .tsd-kind-icon:before { 94 | background-position: -17px -102px; 95 | } 96 | .tsd-kind-module.tsd-is-private > .tsd-kind-icon:before { 97 | background-position: -34px -102px; 98 | } 99 | 100 | .tsd-kind-enum > .tsd-kind-icon:before { 101 | background-position: 0px -119px; 102 | } 103 | .tsd-kind-enum.tsd-is-protected > .tsd-kind-icon:before { 104 | background-position: -17px -119px; 105 | } 106 | .tsd-kind-enum.tsd-is-private > .tsd-kind-icon:before { 107 | background-position: -34px -119px; 108 | } 109 | 110 | .tsd-kind-enum-member > .tsd-kind-icon:before { 111 | background-position: 0px -136px; 112 | } 113 | .tsd-kind-enum-member.tsd-is-protected > .tsd-kind-icon:before { 114 | background-position: -17px -136px; 115 | } 116 | .tsd-kind-enum-member.tsd-is-private > .tsd-kind-icon:before { 117 | background-position: -34px -136px; 118 | } 119 | 120 | .tsd-kind-signature > .tsd-kind-icon:before { 121 | background-position: 0px -153px; 122 | } 123 | .tsd-kind-signature.tsd-is-protected > .tsd-kind-icon:before { 124 | background-position: -17px -153px; 125 | } 126 | .tsd-kind-signature.tsd-is-private > .tsd-kind-icon:before { 127 | background-position: -34px -153px; 128 | } 129 | 130 | .tsd-kind-type-alias > .tsd-kind-icon:before { 131 | background-position: 0px -170px; 132 | } 133 | .tsd-kind-type-alias.tsd-is-protected > .tsd-kind-icon:before { 134 | background-position: -17px -170px; 135 | } 136 | .tsd-kind-type-alias.tsd-is-private > .tsd-kind-icon:before { 137 | background-position: -34px -170px; 138 | } 139 | 140 | .tsd-kind-type-alias.tsd-has-type-parameter > .tsd-kind-icon:before { 141 | background-position: 0px -187px; 142 | } 143 | .tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-protected 144 | > .tsd-kind-icon:before { 145 | background-position: -17px -187px; 146 | } 147 | .tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-private 148 | > .tsd-kind-icon:before { 149 | background-position: -34px -187px; 150 | } 151 | 152 | .tsd-kind-variable > .tsd-kind-icon:before { 153 | background-position: -136px -0px; 154 | } 155 | .tsd-kind-variable.tsd-is-protected > .tsd-kind-icon:before { 156 | background-position: -153px -0px; 157 | } 158 | .tsd-kind-variable.tsd-is-private > .tsd-kind-icon:before { 159 | background-position: -119px -0px; 160 | } 161 | .tsd-kind-variable.tsd-parent-kind-class > .tsd-kind-icon:before { 162 | background-position: -51px -0px; 163 | } 164 | .tsd-kind-variable.tsd-parent-kind-class.tsd-is-inherited 165 | > .tsd-kind-icon:before { 166 | background-position: -68px -0px; 167 | } 168 | .tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected 169 | > .tsd-kind-icon:before { 170 | background-position: -85px -0px; 171 | } 172 | .tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 173 | > .tsd-kind-icon:before { 174 | background-position: -102px -0px; 175 | } 176 | .tsd-kind-variable.tsd-parent-kind-class.tsd-is-private 177 | > .tsd-kind-icon:before { 178 | background-position: -119px -0px; 179 | } 180 | .tsd-kind-variable.tsd-parent-kind-enum > .tsd-kind-icon:before { 181 | background-position: -170px -0px; 182 | } 183 | .tsd-kind-variable.tsd-parent-kind-enum.tsd-is-protected 184 | > .tsd-kind-icon:before { 185 | background-position: -187px -0px; 186 | } 187 | .tsd-kind-variable.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { 188 | background-position: -119px -0px; 189 | } 190 | .tsd-kind-variable.tsd-parent-kind-interface > .tsd-kind-icon:before { 191 | background-position: -204px -0px; 192 | } 193 | .tsd-kind-variable.tsd-parent-kind-interface.tsd-is-inherited 194 | > .tsd-kind-icon:before { 195 | background-position: -221px -0px; 196 | } 197 | 198 | .tsd-kind-property > .tsd-kind-icon:before { 199 | background-position: -136px -0px; 200 | } 201 | .tsd-kind-property.tsd-is-protected > .tsd-kind-icon:before { 202 | background-position: -153px -0px; 203 | } 204 | .tsd-kind-property.tsd-is-private > .tsd-kind-icon:before { 205 | background-position: -119px -0px; 206 | } 207 | .tsd-kind-property.tsd-parent-kind-class > .tsd-kind-icon:before { 208 | background-position: -51px -0px; 209 | } 210 | .tsd-kind-property.tsd-parent-kind-class.tsd-is-inherited 211 | > .tsd-kind-icon:before { 212 | background-position: -68px -0px; 213 | } 214 | .tsd-kind-property.tsd-parent-kind-class.tsd-is-protected 215 | > .tsd-kind-icon:before { 216 | background-position: -85px -0px; 217 | } 218 | .tsd-kind-property.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 219 | > .tsd-kind-icon:before { 220 | background-position: -102px -0px; 221 | } 222 | .tsd-kind-property.tsd-parent-kind-class.tsd-is-private 223 | > .tsd-kind-icon:before { 224 | background-position: -119px -0px; 225 | } 226 | .tsd-kind-property.tsd-parent-kind-enum > .tsd-kind-icon:before { 227 | background-position: -170px -0px; 228 | } 229 | .tsd-kind-property.tsd-parent-kind-enum.tsd-is-protected 230 | > .tsd-kind-icon:before { 231 | background-position: -187px -0px; 232 | } 233 | .tsd-kind-property.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { 234 | background-position: -119px -0px; 235 | } 236 | .tsd-kind-property.tsd-parent-kind-interface > .tsd-kind-icon:before { 237 | background-position: -204px -0px; 238 | } 239 | .tsd-kind-property.tsd-parent-kind-interface.tsd-is-inherited 240 | > .tsd-kind-icon:before { 241 | background-position: -221px -0px; 242 | } 243 | 244 | .tsd-kind-get-signature > .tsd-kind-icon:before { 245 | background-position: -136px -17px; 246 | } 247 | .tsd-kind-get-signature.tsd-is-protected > .tsd-kind-icon:before { 248 | background-position: -153px -17px; 249 | } 250 | .tsd-kind-get-signature.tsd-is-private > .tsd-kind-icon:before { 251 | background-position: -119px -17px; 252 | } 253 | .tsd-kind-get-signature.tsd-parent-kind-class > .tsd-kind-icon:before { 254 | background-position: -51px -17px; 255 | } 256 | .tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-inherited 257 | > .tsd-kind-icon:before { 258 | background-position: -68px -17px; 259 | } 260 | .tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected 261 | > .tsd-kind-icon:before { 262 | background-position: -85px -17px; 263 | } 264 | .tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 265 | > .tsd-kind-icon:before { 266 | background-position: -102px -17px; 267 | } 268 | .tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-private 269 | > .tsd-kind-icon:before { 270 | background-position: -119px -17px; 271 | } 272 | .tsd-kind-get-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { 273 | background-position: -170px -17px; 274 | } 275 | .tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-protected 276 | > .tsd-kind-icon:before { 277 | background-position: -187px -17px; 278 | } 279 | .tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-private 280 | > .tsd-kind-icon:before { 281 | background-position: -119px -17px; 282 | } 283 | .tsd-kind-get-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { 284 | background-position: -204px -17px; 285 | } 286 | .tsd-kind-get-signature.tsd-parent-kind-interface.tsd-is-inherited 287 | > .tsd-kind-icon:before { 288 | background-position: -221px -17px; 289 | } 290 | 291 | .tsd-kind-set-signature > .tsd-kind-icon:before { 292 | background-position: -136px -34px; 293 | } 294 | .tsd-kind-set-signature.tsd-is-protected > .tsd-kind-icon:before { 295 | background-position: -153px -34px; 296 | } 297 | .tsd-kind-set-signature.tsd-is-private > .tsd-kind-icon:before { 298 | background-position: -119px -34px; 299 | } 300 | .tsd-kind-set-signature.tsd-parent-kind-class > .tsd-kind-icon:before { 301 | background-position: -51px -34px; 302 | } 303 | .tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-inherited 304 | > .tsd-kind-icon:before { 305 | background-position: -68px -34px; 306 | } 307 | .tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected 308 | > .tsd-kind-icon:before { 309 | background-position: -85px -34px; 310 | } 311 | .tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 312 | > .tsd-kind-icon:before { 313 | background-position: -102px -34px; 314 | } 315 | .tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-private 316 | > .tsd-kind-icon:before { 317 | background-position: -119px -34px; 318 | } 319 | .tsd-kind-set-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { 320 | background-position: -170px -34px; 321 | } 322 | .tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-protected 323 | > .tsd-kind-icon:before { 324 | background-position: -187px -34px; 325 | } 326 | .tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-private 327 | > .tsd-kind-icon:before { 328 | background-position: -119px -34px; 329 | } 330 | .tsd-kind-set-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { 331 | background-position: -204px -34px; 332 | } 333 | .tsd-kind-set-signature.tsd-parent-kind-interface.tsd-is-inherited 334 | > .tsd-kind-icon:before { 335 | background-position: -221px -34px; 336 | } 337 | 338 | .tsd-kind-accessor > .tsd-kind-icon:before { 339 | background-position: -136px -51px; 340 | } 341 | .tsd-kind-accessor.tsd-is-protected > .tsd-kind-icon:before { 342 | background-position: -153px -51px; 343 | } 344 | .tsd-kind-accessor.tsd-is-private > .tsd-kind-icon:before { 345 | background-position: -119px -51px; 346 | } 347 | .tsd-kind-accessor.tsd-parent-kind-class > .tsd-kind-icon:before { 348 | background-position: -51px -51px; 349 | } 350 | .tsd-kind-accessor.tsd-parent-kind-class.tsd-is-inherited 351 | > .tsd-kind-icon:before { 352 | background-position: -68px -51px; 353 | } 354 | .tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected 355 | > .tsd-kind-icon:before { 356 | background-position: -85px -51px; 357 | } 358 | .tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 359 | > .tsd-kind-icon:before { 360 | background-position: -102px -51px; 361 | } 362 | .tsd-kind-accessor.tsd-parent-kind-class.tsd-is-private 363 | > .tsd-kind-icon:before { 364 | background-position: -119px -51px; 365 | } 366 | .tsd-kind-accessor.tsd-parent-kind-enum > .tsd-kind-icon:before { 367 | background-position: -170px -51px; 368 | } 369 | .tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-protected 370 | > .tsd-kind-icon:before { 371 | background-position: -187px -51px; 372 | } 373 | .tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { 374 | background-position: -119px -51px; 375 | } 376 | .tsd-kind-accessor.tsd-parent-kind-interface > .tsd-kind-icon:before { 377 | background-position: -204px -51px; 378 | } 379 | .tsd-kind-accessor.tsd-parent-kind-interface.tsd-is-inherited 380 | > .tsd-kind-icon:before { 381 | background-position: -221px -51px; 382 | } 383 | 384 | .tsd-kind-function > .tsd-kind-icon:before { 385 | background-position: -136px -68px; 386 | } 387 | .tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { 388 | background-position: -153px -68px; 389 | } 390 | .tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { 391 | background-position: -119px -68px; 392 | } 393 | .tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { 394 | background-position: -51px -68px; 395 | } 396 | .tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited 397 | > .tsd-kind-icon:before { 398 | background-position: -68px -68px; 399 | } 400 | .tsd-kind-function.tsd-parent-kind-class.tsd-is-protected 401 | > .tsd-kind-icon:before { 402 | background-position: -85px -68px; 403 | } 404 | .tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 405 | > .tsd-kind-icon:before { 406 | background-position: -102px -68px; 407 | } 408 | .tsd-kind-function.tsd-parent-kind-class.tsd-is-private 409 | > .tsd-kind-icon:before { 410 | background-position: -119px -68px; 411 | } 412 | .tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { 413 | background-position: -170px -68px; 414 | } 415 | .tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected 416 | > .tsd-kind-icon:before { 417 | background-position: -187px -68px; 418 | } 419 | .tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { 420 | background-position: -119px -68px; 421 | } 422 | .tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { 423 | background-position: -204px -68px; 424 | } 425 | .tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited 426 | > .tsd-kind-icon:before { 427 | background-position: -221px -68px; 428 | } 429 | 430 | .tsd-kind-method > .tsd-kind-icon:before { 431 | background-position: -136px -68px; 432 | } 433 | .tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { 434 | background-position: -153px -68px; 435 | } 436 | .tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { 437 | background-position: -119px -68px; 438 | } 439 | .tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { 440 | background-position: -51px -68px; 441 | } 442 | .tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited 443 | > .tsd-kind-icon:before { 444 | background-position: -68px -68px; 445 | } 446 | .tsd-kind-method.tsd-parent-kind-class.tsd-is-protected 447 | > .tsd-kind-icon:before { 448 | background-position: -85px -68px; 449 | } 450 | .tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 451 | > .tsd-kind-icon:before { 452 | background-position: -102px -68px; 453 | } 454 | .tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { 455 | background-position: -119px -68px; 456 | } 457 | .tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { 458 | background-position: -170px -68px; 459 | } 460 | .tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { 461 | background-position: -187px -68px; 462 | } 463 | .tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { 464 | background-position: -119px -68px; 465 | } 466 | .tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { 467 | background-position: -204px -68px; 468 | } 469 | .tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited 470 | > .tsd-kind-icon:before { 471 | background-position: -221px -68px; 472 | } 473 | 474 | .tsd-kind-call-signature > .tsd-kind-icon:before { 475 | background-position: -136px -68px; 476 | } 477 | .tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { 478 | background-position: -153px -68px; 479 | } 480 | .tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { 481 | background-position: -119px -68px; 482 | } 483 | .tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { 484 | background-position: -51px -68px; 485 | } 486 | .tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited 487 | > .tsd-kind-icon:before { 488 | background-position: -68px -68px; 489 | } 490 | .tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected 491 | > .tsd-kind-icon:before { 492 | background-position: -85px -68px; 493 | } 494 | .tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 495 | > .tsd-kind-icon:before { 496 | background-position: -102px -68px; 497 | } 498 | .tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private 499 | > .tsd-kind-icon:before { 500 | background-position: -119px -68px; 501 | } 502 | .tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { 503 | background-position: -170px -68px; 504 | } 505 | .tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected 506 | > .tsd-kind-icon:before { 507 | background-position: -187px -68px; 508 | } 509 | .tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private 510 | > .tsd-kind-icon:before { 511 | background-position: -119px -68px; 512 | } 513 | .tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { 514 | background-position: -204px -68px; 515 | } 516 | .tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited 517 | > .tsd-kind-icon:before { 518 | background-position: -221px -68px; 519 | } 520 | 521 | .tsd-kind-function.tsd-has-type-parameter > .tsd-kind-icon:before { 522 | background-position: -136px -85px; 523 | } 524 | .tsd-kind-function.tsd-has-type-parameter.tsd-is-protected 525 | > .tsd-kind-icon:before { 526 | background-position: -153px -85px; 527 | } 528 | .tsd-kind-function.tsd-has-type-parameter.tsd-is-private 529 | > .tsd-kind-icon:before { 530 | background-position: -119px -85px; 531 | } 532 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class 533 | > .tsd-kind-icon:before { 534 | background-position: -51px -85px; 535 | } 536 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited 537 | > .tsd-kind-icon:before { 538 | background-position: -68px -85px; 539 | } 540 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected 541 | > .tsd-kind-icon:before { 542 | background-position: -85px -85px; 543 | } 544 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 545 | > .tsd-kind-icon:before { 546 | background-position: -102px -85px; 547 | } 548 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private 549 | > .tsd-kind-icon:before { 550 | background-position: -119px -85px; 551 | } 552 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum 553 | > .tsd-kind-icon:before { 554 | background-position: -170px -85px; 555 | } 556 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected 557 | > .tsd-kind-icon:before { 558 | background-position: -187px -85px; 559 | } 560 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private 561 | > .tsd-kind-icon:before { 562 | background-position: -119px -85px; 563 | } 564 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface 565 | > .tsd-kind-icon:before { 566 | background-position: -204px -85px; 567 | } 568 | .tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited 569 | > .tsd-kind-icon:before { 570 | background-position: -221px -85px; 571 | } 572 | 573 | .tsd-kind-method.tsd-has-type-parameter > .tsd-kind-icon:before { 574 | background-position: -136px -85px; 575 | } 576 | .tsd-kind-method.tsd-has-type-parameter.tsd-is-protected 577 | > .tsd-kind-icon:before { 578 | background-position: -153px -85px; 579 | } 580 | .tsd-kind-method.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { 581 | background-position: -119px -85px; 582 | } 583 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class 584 | > .tsd-kind-icon:before { 585 | background-position: -51px -85px; 586 | } 587 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited 588 | > .tsd-kind-icon:before { 589 | background-position: -68px -85px; 590 | } 591 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected 592 | > .tsd-kind-icon:before { 593 | background-position: -85px -85px; 594 | } 595 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 596 | > .tsd-kind-icon:before { 597 | background-position: -102px -85px; 598 | } 599 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private 600 | > .tsd-kind-icon:before { 601 | background-position: -119px -85px; 602 | } 603 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum 604 | > .tsd-kind-icon:before { 605 | background-position: -170px -85px; 606 | } 607 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected 608 | > .tsd-kind-icon:before { 609 | background-position: -187px -85px; 610 | } 611 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private 612 | > .tsd-kind-icon:before { 613 | background-position: -119px -85px; 614 | } 615 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface 616 | > .tsd-kind-icon:before { 617 | background-position: -204px -85px; 618 | } 619 | .tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited 620 | > .tsd-kind-icon:before { 621 | background-position: -221px -85px; 622 | } 623 | 624 | .tsd-kind-constructor > .tsd-kind-icon:before { 625 | background-position: -136px -102px; 626 | } 627 | .tsd-kind-constructor.tsd-is-protected > .tsd-kind-icon:before { 628 | background-position: -153px -102px; 629 | } 630 | .tsd-kind-constructor.tsd-is-private > .tsd-kind-icon:before { 631 | background-position: -119px -102px; 632 | } 633 | .tsd-kind-constructor.tsd-parent-kind-class > .tsd-kind-icon:before { 634 | background-position: -51px -102px; 635 | } 636 | .tsd-kind-constructor.tsd-parent-kind-class.tsd-is-inherited 637 | > .tsd-kind-icon:before { 638 | background-position: -68px -102px; 639 | } 640 | .tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected 641 | > .tsd-kind-icon:before { 642 | background-position: -85px -102px; 643 | } 644 | .tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 645 | > .tsd-kind-icon:before { 646 | background-position: -102px -102px; 647 | } 648 | .tsd-kind-constructor.tsd-parent-kind-class.tsd-is-private 649 | > .tsd-kind-icon:before { 650 | background-position: -119px -102px; 651 | } 652 | .tsd-kind-constructor.tsd-parent-kind-enum > .tsd-kind-icon:before { 653 | background-position: -170px -102px; 654 | } 655 | .tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-protected 656 | > .tsd-kind-icon:before { 657 | background-position: -187px -102px; 658 | } 659 | .tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-private 660 | > .tsd-kind-icon:before { 661 | background-position: -119px -102px; 662 | } 663 | .tsd-kind-constructor.tsd-parent-kind-interface > .tsd-kind-icon:before { 664 | background-position: -204px -102px; 665 | } 666 | .tsd-kind-constructor.tsd-parent-kind-interface.tsd-is-inherited 667 | > .tsd-kind-icon:before { 668 | background-position: -221px -102px; 669 | } 670 | 671 | .tsd-kind-constructor-signature > .tsd-kind-icon:before { 672 | background-position: -136px -102px; 673 | } 674 | .tsd-kind-constructor-signature.tsd-is-protected > .tsd-kind-icon:before { 675 | background-position: -153px -102px; 676 | } 677 | .tsd-kind-constructor-signature.tsd-is-private > .tsd-kind-icon:before { 678 | background-position: -119px -102px; 679 | } 680 | .tsd-kind-constructor-signature.tsd-parent-kind-class > .tsd-kind-icon:before { 681 | background-position: -51px -102px; 682 | } 683 | .tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-inherited 684 | > .tsd-kind-icon:before { 685 | background-position: -68px -102px; 686 | } 687 | .tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected 688 | > .tsd-kind-icon:before { 689 | background-position: -85px -102px; 690 | } 691 | .tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 692 | > .tsd-kind-icon:before { 693 | background-position: -102px -102px; 694 | } 695 | .tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-private 696 | > .tsd-kind-icon:before { 697 | background-position: -119px -102px; 698 | } 699 | .tsd-kind-constructor-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { 700 | background-position: -170px -102px; 701 | } 702 | .tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-protected 703 | > .tsd-kind-icon:before { 704 | background-position: -187px -102px; 705 | } 706 | .tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-private 707 | > .tsd-kind-icon:before { 708 | background-position: -119px -102px; 709 | } 710 | .tsd-kind-constructor-signature.tsd-parent-kind-interface 711 | > .tsd-kind-icon:before { 712 | background-position: -204px -102px; 713 | } 714 | .tsd-kind-constructor-signature.tsd-parent-kind-interface.tsd-is-inherited 715 | > .tsd-kind-icon:before { 716 | background-position: -221px -102px; 717 | } 718 | 719 | .tsd-kind-index-signature > .tsd-kind-icon:before { 720 | background-position: -136px -119px; 721 | } 722 | .tsd-kind-index-signature.tsd-is-protected > .tsd-kind-icon:before { 723 | background-position: -153px -119px; 724 | } 725 | .tsd-kind-index-signature.tsd-is-private > .tsd-kind-icon:before { 726 | background-position: -119px -119px; 727 | } 728 | .tsd-kind-index-signature.tsd-parent-kind-class > .tsd-kind-icon:before { 729 | background-position: -51px -119px; 730 | } 731 | .tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-inherited 732 | > .tsd-kind-icon:before { 733 | background-position: -68px -119px; 734 | } 735 | .tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected 736 | > .tsd-kind-icon:before { 737 | background-position: -85px -119px; 738 | } 739 | .tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 740 | > .tsd-kind-icon:before { 741 | background-position: -102px -119px; 742 | } 743 | .tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-private 744 | > .tsd-kind-icon:before { 745 | background-position: -119px -119px; 746 | } 747 | .tsd-kind-index-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { 748 | background-position: -170px -119px; 749 | } 750 | .tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-protected 751 | > .tsd-kind-icon:before { 752 | background-position: -187px -119px; 753 | } 754 | .tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-private 755 | > .tsd-kind-icon:before { 756 | background-position: -119px -119px; 757 | } 758 | .tsd-kind-index-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { 759 | background-position: -204px -119px; 760 | } 761 | .tsd-kind-index-signature.tsd-parent-kind-interface.tsd-is-inherited 762 | > .tsd-kind-icon:before { 763 | background-position: -221px -119px; 764 | } 765 | 766 | .tsd-kind-event > .tsd-kind-icon:before { 767 | background-position: -136px -136px; 768 | } 769 | .tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { 770 | background-position: -153px -136px; 771 | } 772 | .tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { 773 | background-position: -119px -136px; 774 | } 775 | .tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { 776 | background-position: -51px -136px; 777 | } 778 | .tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { 779 | background-position: -68px -136px; 780 | } 781 | .tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { 782 | background-position: -85px -136px; 783 | } 784 | .tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 785 | > .tsd-kind-icon:before { 786 | background-position: -102px -136px; 787 | } 788 | .tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { 789 | background-position: -119px -136px; 790 | } 791 | .tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { 792 | background-position: -170px -136px; 793 | } 794 | .tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { 795 | background-position: -187px -136px; 796 | } 797 | .tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { 798 | background-position: -119px -136px; 799 | } 800 | .tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { 801 | background-position: -204px -136px; 802 | } 803 | .tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited 804 | > .tsd-kind-icon:before { 805 | background-position: -221px -136px; 806 | } 807 | 808 | .tsd-is-static > .tsd-kind-icon:before { 809 | background-position: -136px -153px; 810 | } 811 | .tsd-is-static.tsd-is-protected > .tsd-kind-icon:before { 812 | background-position: -153px -153px; 813 | } 814 | .tsd-is-static.tsd-is-private > .tsd-kind-icon:before { 815 | background-position: -119px -153px; 816 | } 817 | .tsd-is-static.tsd-parent-kind-class > .tsd-kind-icon:before { 818 | background-position: -51px -153px; 819 | } 820 | .tsd-is-static.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { 821 | background-position: -68px -153px; 822 | } 823 | .tsd-is-static.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { 824 | background-position: -85px -153px; 825 | } 826 | .tsd-is-static.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 827 | > .tsd-kind-icon:before { 828 | background-position: -102px -153px; 829 | } 830 | .tsd-is-static.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { 831 | background-position: -119px -153px; 832 | } 833 | .tsd-is-static.tsd-parent-kind-enum > .tsd-kind-icon:before { 834 | background-position: -170px -153px; 835 | } 836 | .tsd-is-static.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { 837 | background-position: -187px -153px; 838 | } 839 | .tsd-is-static.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { 840 | background-position: -119px -153px; 841 | } 842 | .tsd-is-static.tsd-parent-kind-interface > .tsd-kind-icon:before { 843 | background-position: -204px -153px; 844 | } 845 | .tsd-is-static.tsd-parent-kind-interface.tsd-is-inherited 846 | > .tsd-kind-icon:before { 847 | background-position: -221px -153px; 848 | } 849 | 850 | .tsd-is-static.tsd-kind-function > .tsd-kind-icon:before { 851 | background-position: -136px -170px; 852 | } 853 | .tsd-is-static.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { 854 | background-position: -153px -170px; 855 | } 856 | .tsd-is-static.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { 857 | background-position: -119px -170px; 858 | } 859 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { 860 | background-position: -51px -170px; 861 | } 862 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited 863 | > .tsd-kind-icon:before { 864 | background-position: -68px -170px; 865 | } 866 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected 867 | > .tsd-kind-icon:before { 868 | background-position: -85px -170px; 869 | } 870 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 871 | > .tsd-kind-icon:before { 872 | background-position: -102px -170px; 873 | } 874 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-private 875 | > .tsd-kind-icon:before { 876 | background-position: -119px -170px; 877 | } 878 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { 879 | background-position: -170px -170px; 880 | } 881 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected 882 | > .tsd-kind-icon:before { 883 | background-position: -187px -170px; 884 | } 885 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private 886 | > .tsd-kind-icon:before { 887 | background-position: -119px -170px; 888 | } 889 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-interface 890 | > .tsd-kind-icon:before { 891 | background-position: -204px -170px; 892 | } 893 | .tsd-is-static.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited 894 | > .tsd-kind-icon:before { 895 | background-position: -221px -170px; 896 | } 897 | 898 | .tsd-is-static.tsd-kind-method > .tsd-kind-icon:before { 899 | background-position: -136px -170px; 900 | } 901 | .tsd-is-static.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { 902 | background-position: -153px -170px; 903 | } 904 | .tsd-is-static.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { 905 | background-position: -119px -170px; 906 | } 907 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { 908 | background-position: -51px -170px; 909 | } 910 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited 911 | > .tsd-kind-icon:before { 912 | background-position: -68px -170px; 913 | } 914 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected 915 | > .tsd-kind-icon:before { 916 | background-position: -85px -170px; 917 | } 918 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 919 | > .tsd-kind-icon:before { 920 | background-position: -102px -170px; 921 | } 922 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-private 923 | > .tsd-kind-icon:before { 924 | background-position: -119px -170px; 925 | } 926 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { 927 | background-position: -170px -170px; 928 | } 929 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected 930 | > .tsd-kind-icon:before { 931 | background-position: -187px -170px; 932 | } 933 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private 934 | > .tsd-kind-icon:before { 935 | background-position: -119px -170px; 936 | } 937 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-interface 938 | > .tsd-kind-icon:before { 939 | background-position: -204px -170px; 940 | } 941 | .tsd-is-static.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited 942 | > .tsd-kind-icon:before { 943 | background-position: -221px -170px; 944 | } 945 | 946 | .tsd-is-static.tsd-kind-call-signature > .tsd-kind-icon:before { 947 | background-position: -136px -170px; 948 | } 949 | .tsd-is-static.tsd-kind-call-signature.tsd-is-protected 950 | > .tsd-kind-icon:before { 951 | background-position: -153px -170px; 952 | } 953 | .tsd-is-static.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { 954 | background-position: -119px -170px; 955 | } 956 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class 957 | > .tsd-kind-icon:before { 958 | background-position: -51px -170px; 959 | } 960 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited 961 | > .tsd-kind-icon:before { 962 | background-position: -68px -170px; 963 | } 964 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected 965 | > .tsd-kind-icon:before { 966 | background-position: -85px -170px; 967 | } 968 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 969 | > .tsd-kind-icon:before { 970 | background-position: -102px -170px; 971 | } 972 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private 973 | > .tsd-kind-icon:before { 974 | background-position: -119px -170px; 975 | } 976 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum 977 | > .tsd-kind-icon:before { 978 | background-position: -170px -170px; 979 | } 980 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected 981 | > .tsd-kind-icon:before { 982 | background-position: -187px -170px; 983 | } 984 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private 985 | > .tsd-kind-icon:before { 986 | background-position: -119px -170px; 987 | } 988 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface 989 | > .tsd-kind-icon:before { 990 | background-position: -204px -170px; 991 | } 992 | .tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited 993 | > .tsd-kind-icon:before { 994 | background-position: -221px -170px; 995 | } 996 | 997 | .tsd-is-static.tsd-kind-event > .tsd-kind-icon:before { 998 | background-position: -136px -187px; 999 | } 1000 | .tsd-is-static.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { 1001 | background-position: -153px -187px; 1002 | } 1003 | .tsd-is-static.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { 1004 | background-position: -119px -187px; 1005 | } 1006 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { 1007 | background-position: -51px -187px; 1008 | } 1009 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited 1010 | > .tsd-kind-icon:before { 1011 | background-position: -68px -187px; 1012 | } 1013 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected 1014 | > .tsd-kind-icon:before { 1015 | background-position: -85px -187px; 1016 | } 1017 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited 1018 | > .tsd-kind-icon:before { 1019 | background-position: -102px -187px; 1020 | } 1021 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-private 1022 | > .tsd-kind-icon:before { 1023 | background-position: -119px -187px; 1024 | } 1025 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { 1026 | background-position: -170px -187px; 1027 | } 1028 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected 1029 | > .tsd-kind-icon:before { 1030 | background-position: -187px -187px; 1031 | } 1032 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private 1033 | > .tsd-kind-icon:before { 1034 | background-position: -119px -187px; 1035 | } 1036 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-interface 1037 | > .tsd-kind-icon:before { 1038 | background-position: -204px -187px; 1039 | } 1040 | .tsd-is-static.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited 1041 | > .tsd-kind-icon:before { 1042 | background-position: -221px -187px; 1043 | } 1044 | -------------------------------------------------------------------------------- /typedoc/assets/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mweidner037/uniquely-dense-total-order/274dfa22e9068c2769f6c3b1015d2c745f5e2775/typedoc/assets/icons.png -------------------------------------------------------------------------------- /typedoc/assets/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mweidner037/uniquely-dense-total-order/274dfa22e9068c2769f6c3b1015d2c745f5e2775/typedoc/assets/icons@2x.png -------------------------------------------------------------------------------- /typedoc/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = JSON.parse("{\"kinds\":{\"128\":\"Class\",\"256\":\"Interface\",\"512\":\"Constructor\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":256,\"name\":\"UniquelyDenseTotalOrder\",\"url\":\"interfaces/UniquelyDenseTotalOrder.html\",\"classes\":\"tsd-kind-interface tsd-has-type-parameter\"},{\"id\":1,\"kind\":2048,\"name\":\"compare\",\"url\":\"interfaces/UniquelyDenseTotalOrder.html#compare\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"UniquelyDenseTotalOrder\"},{\"id\":2,\"kind\":2048,\"name\":\"createBetween\",\"url\":\"interfaces/UniquelyDenseTotalOrder.html#createBetween\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"UniquelyDenseTotalOrder\"},{\"id\":3,\"kind\":128,\"name\":\"LexUDTotalOrder\",\"url\":\"classes/LexUDTotalOrder.html\",\"classes\":\"tsd-kind-class\"},{\"id\":4,\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/LexUDTotalOrder.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"LexUDTotalOrder\"},{\"id\":5,\"kind\":2048,\"name\":\"compare\",\"url\":\"classes/LexUDTotalOrder.html#compare\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"LexUDTotalOrder\"},{\"id\":6,\"kind\":2048,\"name\":\"createBetween\",\"url\":\"classes/LexUDTotalOrder.html#createBetween\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"LexUDTotalOrder\"},{\"id\":7,\"kind\":128,\"name\":\"GolfStringFugue\",\"url\":\"classes/GolfStringFugue.html\",\"classes\":\"tsd-kind-class\"},{\"id\":8,\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/GolfStringFugue.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class tsd-is-overwrite\",\"parent\":\"GolfStringFugue\"},{\"id\":9,\"kind\":1024,\"name\":\"id\",\"url\":\"classes/GolfStringFugue.html#id\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"GolfStringFugue\"},{\"id\":10,\"kind\":1024,\"name\":\"c\",\"url\":\"classes/GolfStringFugue.html#c\",\"classes\":\"tsd-kind-property tsd-parent-kind-class tsd-is-private\",\"parent\":\"GolfStringFugue\"},{\"id\":11,\"kind\":2048,\"name\":\"createBetween\",\"url\":\"classes/GolfStringFugue.html#createBetween\",\"classes\":\"tsd-kind-method tsd-parent-kind-class tsd-is-overwrite\",\"parent\":\"GolfStringFugue\"},{\"id\":12,\"kind\":2048,\"name\":\"compare\",\"url\":\"classes/GolfStringFugue.html#compare\",\"classes\":\"tsd-kind-method tsd-parent-kind-class tsd-is-inherited\",\"parent\":\"GolfStringFugue\"},{\"id\":13,\"kind\":128,\"name\":\"OptStringFugue\",\"url\":\"classes/OptStringFugue.html\",\"classes\":\"tsd-kind-class\"},{\"id\":14,\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/OptStringFugue.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class tsd-is-overwrite\",\"parent\":\"OptStringFugue\"},{\"id\":15,\"kind\":1024,\"name\":\"replicaID\",\"url\":\"classes/OptStringFugue.html#replicaID\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"OptStringFugue\"},{\"id\":16,\"kind\":1024,\"name\":\"lastValueIndices\",\"url\":\"classes/OptStringFugue.html#lastValueIndices\",\"classes\":\"tsd-kind-property tsd-parent-kind-class tsd-is-private\",\"parent\":\"OptStringFugue\"},{\"id\":17,\"kind\":2048,\"name\":\"createBetween\",\"url\":\"classes/OptStringFugue.html#createBetween\",\"classes\":\"tsd-kind-method tsd-parent-kind-class tsd-is-overwrite\",\"parent\":\"OptStringFugue\"},{\"id\":18,\"kind\":2048,\"name\":\"newWaypointNode\",\"url\":\"classes/OptStringFugue.html#newWaypointNode\",\"classes\":\"tsd-kind-method tsd-parent-kind-class tsd-is-private\",\"parent\":\"OptStringFugue\"},{\"id\":19,\"kind\":2048,\"name\":\"compare\",\"url\":\"classes/OptStringFugue.html#compare\",\"classes\":\"tsd-kind-method tsd-parent-kind-class tsd-is-inherited\",\"parent\":\"OptStringFugue\"},{\"id\":20,\"kind\":128,\"name\":\"StringFugue\",\"url\":\"classes/StringFugue.html\",\"classes\":\"tsd-kind-class\"},{\"id\":21,\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/StringFugue.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class tsd-is-overwrite\",\"parent\":\"StringFugue\"},{\"id\":22,\"kind\":1024,\"name\":\"replicaID\",\"url\":\"classes/StringFugue.html#replicaID\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"StringFugue\"},{\"id\":23,\"kind\":1024,\"name\":\"counter\",\"url\":\"classes/StringFugue.html#counter\",\"classes\":\"tsd-kind-property tsd-parent-kind-class tsd-is-private\",\"parent\":\"StringFugue\"},{\"id\":24,\"kind\":2048,\"name\":\"createBetween\",\"url\":\"classes/StringFugue.html#createBetween\",\"classes\":\"tsd-kind-method tsd-parent-kind-class tsd-is-overwrite\",\"parent\":\"StringFugue\"},{\"id\":25,\"kind\":2048,\"name\":\"compare\",\"url\":\"classes/StringFugue.html#compare\",\"classes\":\"tsd-kind-method tsd-parent-kind-class tsd-is-inherited\",\"parent\":\"StringFugue\"},{\"id\":26,\"kind\":256,\"name\":\"TreePosition\",\"url\":\"interfaces/TreePosition.html\",\"classes\":\"tsd-kind-interface\"},{\"id\":27,\"kind\":1024,\"name\":\"sender\",\"url\":\"interfaces/TreePosition.html#sender\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"TreePosition\"},{\"id\":28,\"kind\":1024,\"name\":\"counter\",\"url\":\"interfaces/TreePosition.html#counter\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"TreePosition\"},{\"id\":29,\"kind\":128,\"name\":\"TreeFugue\",\"url\":\"classes/TreeFugue.html\",\"classes\":\"tsd-kind-class\"},{\"id\":30,\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/TreeFugue.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"TreeFugue\"},{\"id\":31,\"kind\":1024,\"name\":\"replicaID\",\"url\":\"classes/TreeFugue.html#replicaID\",\"classes\":\"tsd-kind-property tsd-parent-kind-class\",\"parent\":\"TreeFugue\"},{\"id\":32,\"kind\":1024,\"name\":\"state\",\"url\":\"classes/TreeFugue.html#state\",\"classes\":\"tsd-kind-property tsd-parent-kind-class tsd-is-private\",\"parent\":\"TreeFugue\"},{\"id\":33,\"kind\":2048,\"name\":\"getInternalPos\",\"url\":\"classes/TreeFugue.html#getInternalPos\",\"classes\":\"tsd-kind-method tsd-parent-kind-class tsd-is-private\",\"parent\":\"TreeFugue\"},{\"id\":34,\"kind\":2048,\"name\":\"compare\",\"url\":\"classes/TreeFugue.html#compare\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"TreeFugue\"},{\"id\":35,\"kind\":2048,\"name\":\"createBetween\",\"url\":\"classes/TreeFugue.html#createBetween\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"TreeFugue\"},{\"id\":36,\"kind\":2048,\"name\":\"receive\",\"url\":\"classes/TreeFugue.html#receive\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"TreeFugue\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,23.848]],[\"parent/0\",[]],[\"name/1\",[1,17.658]],[\"parent/1\",[0,2.177]],[\"name/2\",[2,17.658]],[\"parent/2\",[0,2.177]],[\"name/3\",[3,21.335]],[\"parent/3\",[]],[\"name/4\",[4,19.328]],[\"parent/4\",[3,1.948]],[\"name/5\",[1,17.658]],[\"parent/5\",[3,1.948]],[\"name/6\",[2,17.658]],[\"parent/6\",[3,1.948]],[\"name/7\",[5,17.658]],[\"parent/7\",[]],[\"name/8\",[4,19.328]],[\"parent/8\",[5,1.612]],[\"name/9\",[6,32.321]],[\"parent/9\",[5,1.612]],[\"name/10\",[7,32.321]],[\"parent/10\",[5,1.612]],[\"name/11\",[2,17.658]],[\"parent/11\",[5,1.612]],[\"name/12\",[1,17.658]],[\"parent/12\",[5,1.612]],[\"name/13\",[8,16.227]],[\"parent/13\",[]],[\"name/14\",[4,19.328]],[\"parent/14\",[8,1.481]],[\"name/15\",[9,23.848]],[\"parent/15\",[8,1.481]],[\"name/16\",[10,32.321]],[\"parent/16\",[8,1.481]],[\"name/17\",[2,17.658]],[\"parent/17\",[8,1.481]],[\"name/18\",[11,32.321]],[\"parent/18\",[8,1.481]],[\"name/19\",[1,17.658]],[\"parent/19\",[8,1.481]],[\"name/20\",[12,17.658]],[\"parent/20\",[]],[\"name/21\",[4,19.328]],[\"parent/21\",[12,1.612]],[\"name/22\",[9,23.848]],[\"parent/22\",[12,1.612]],[\"name/23\",[13,27.213]],[\"parent/23\",[12,1.612]],[\"name/24\",[2,17.658]],[\"parent/24\",[12,1.612]],[\"name/25\",[1,17.658]],[\"parent/25\",[12,1.612]],[\"name/26\",[14,23.848]],[\"parent/26\",[]],[\"name/27\",[15,32.321]],[\"parent/27\",[14,2.177]],[\"name/28\",[13,27.213]],[\"parent/28\",[14,2.177]],[\"name/29\",[16,14.975]],[\"parent/29\",[]],[\"name/30\",[4,19.328]],[\"parent/30\",[16,1.367]],[\"name/31\",[9,23.848]],[\"parent/31\",[16,1.367]],[\"name/32\",[17,32.321]],[\"parent/32\",[16,1.367]],[\"name/33\",[18,32.321]],[\"parent/33\",[16,1.367]],[\"name/34\",[1,17.658]],[\"parent/34\",[16,1.367]],[\"name/35\",[2,17.658]],[\"parent/35\",[16,1.367]],[\"name/36\",[19,32.321]],[\"parent/36\",[16,1.367]]],\"invertedIndex\":[[\"c\",{\"_index\":7,\"name\":{\"10\":{}},\"parent\":{}}],[\"compare\",{\"_index\":1,\"name\":{\"1\":{},\"5\":{},\"12\":{},\"19\":{},\"25\":{},\"34\":{}},\"parent\":{}}],[\"constructor\",{\"_index\":4,\"name\":{\"4\":{},\"8\":{},\"14\":{},\"21\":{},\"30\":{}},\"parent\":{}}],[\"counter\",{\"_index\":13,\"name\":{\"23\":{},\"28\":{}},\"parent\":{}}],[\"createbetween\",{\"_index\":2,\"name\":{\"2\":{},\"6\":{},\"11\":{},\"17\":{},\"24\":{},\"35\":{}},\"parent\":{}}],[\"getinternalpos\",{\"_index\":18,\"name\":{\"33\":{}},\"parent\":{}}],[\"golfstringfugue\",{\"_index\":5,\"name\":{\"7\":{}},\"parent\":{\"8\":{},\"9\":{},\"10\":{},\"11\":{},\"12\":{}}}],[\"id\",{\"_index\":6,\"name\":{\"9\":{}},\"parent\":{}}],[\"lastvalueindices\",{\"_index\":10,\"name\":{\"16\":{}},\"parent\":{}}],[\"lexudtotalorder\",{\"_index\":3,\"name\":{\"3\":{}},\"parent\":{\"4\":{},\"5\":{},\"6\":{}}}],[\"newwaypointnode\",{\"_index\":11,\"name\":{\"18\":{}},\"parent\":{}}],[\"optstringfugue\",{\"_index\":8,\"name\":{\"13\":{}},\"parent\":{\"14\":{},\"15\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{}}}],[\"receive\",{\"_index\":19,\"name\":{\"36\":{}},\"parent\":{}}],[\"replicaid\",{\"_index\":9,\"name\":{\"15\":{},\"22\":{},\"31\":{}},\"parent\":{}}],[\"sender\",{\"_index\":15,\"name\":{\"27\":{}},\"parent\":{}}],[\"state\",{\"_index\":17,\"name\":{\"32\":{}},\"parent\":{}}],[\"stringfugue\",{\"_index\":12,\"name\":{\"20\":{}},\"parent\":{\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{}}}],[\"treefugue\",{\"_index\":16,\"name\":{\"29\":{}},\"parent\":{\"30\":{},\"31\":{},\"32\":{},\"33\":{},\"34\":{},\"35\":{},\"36\":{}}}],[\"treeposition\",{\"_index\":14,\"name\":{\"26\":{}},\"parent\":{\"27\":{},\"28\":{}}}],[\"uniquelydensetotalorder\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{},\"2\":{}}}]],\"pipeline\":[]}}"); -------------------------------------------------------------------------------- /typedoc/assets/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mweidner037/uniquely-dense-total-order/274dfa22e9068c2769f6c3b1015d2c745f5e2775/typedoc/assets/widgets.png -------------------------------------------------------------------------------- /typedoc/assets/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mweidner037/uniquely-dense-total-order/274dfa22e9068c2769f6c3b1015d2c745f5e2775/typedoc/assets/widgets@2x.png -------------------------------------------------------------------------------- /typedoc/classes/GolfStringFugue.html: -------------------------------------------------------------------------------- 1 | GolfStringFugue | uniquely-dense-total-order

Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

A code golf version of StringFugue.

3 |

Its positions are the same as those of StringFugue 4 | except that they all start with an extra "R". 5 | This implementation is just for code golf; StringFugue 6 | has clearer code and 1-character-shorter positions.

7 |

For a description of the algorithm, see 8 | https://mattweidner.com/2022/10/05/basic-list-crdt.html#intro-string-implementation

9 |

Hierarchy

Index

Constructors

Properties

Methods

Constructors

  • Parameters

    • options: { replicaID?: string } = {}
      • Optional replicaID?: string
        10 |

        A unique replicaID. Must be unique among all 11 | collaborating replicas, including past or concurrent replicas for the 12 | same device or user. All collaborating replicas' replicaIDs must be the 13 | same length.

        14 |

    Returns GolfStringFugue

Properties

c: number = 0
id: string

Methods

  • compare(a: string, b: string): number
  • 15 |

    Usual compare function for sorts: returns negative if a < b in 16 | their sort order, positive if a > b.

    17 |

    Parameters

    • a: string
    • b: string

    Returns number

  • createBetween(a?: string, b?: string): string
  • 18 |

    Returns a globally unique new position c such that a < c < b.

    19 |

    "Globally unique" means that the created position must be distinct 20 | from all other created positions, including ones created concurrently 21 | by other users.

    22 |

    When a is undefined, it is treated as the start of the list, i.e., 23 | this returns c such that c < b. Likewise, undefined b is treated 24 | as the end of the list.

    25 |

    Parameters

    • a: string = "R"
    • b: string = "Z"

    Returns string

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Private property
  • Private method
  • Inherited method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /typedoc/classes/LexUDTotalOrder.html: -------------------------------------------------------------------------------- 1 | LexUDTotalOrder | uniquely-dense-total-order
Options
All
  • Public
  • Public/Protected
  • All
Menu

Class LexUDTotalOrder Abstract

2 |

A UniquelyDenseTotalOrder that sorts using the lexicographic 3 | order on strings.

4 |

This is useful in contexts where you can't specify a 5 | custom compare function for sorts (e.g., a column 6 | in a database table). However, it comes at the cost 7 | of super-constant size positions (both on the network 8 | and in memory).

9 |

Hierarchy

Implements

Index

Constructors

Methods

  • compare(a: string, b: string): number
  • createBetween(a: undefined | string, b: undefined | string): string
  • 13 |

    Returns a globally unique new position c such that a < c < b.

    14 |

    "Globally unique" means that the created position must be distinct 15 | from all other created positions, including ones created concurrently 16 | by other users.

    17 |

    When a is undefined, it is treated as the start of the list, i.e., 18 | this returns c such that c < b. Likewise, undefined b is treated 19 | as the end of the list.

    20 |

    Parameters

    • a: undefined | string
    • b: undefined | string

    Returns string

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Private property
  • Private method
  • Inherited method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /typedoc/classes/OptStringFugue.html: -------------------------------------------------------------------------------- 1 | OptStringFugue | uniquely-dense-total-order
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

An optimized version of StringFugue.

3 |

For a description of the algorithm, see 4 | https://mattweidner.com/2022/10/05/basic-list-crdt.html#intro-string-implementation

5 |

The difference from StringFugue (the linked algorithm) 6 | is a common-case optimization: left-to-right insertions 7 | by the same replica reuse the same (replicaID, counter) 8 | pair (we call this a waypoint), just using 9 | an extra valueIndex to distinguish positions 10 | within the sequence, instead of creating a long 11 | rightward path in the tree. In this way, 12 | a sequence of m left-to-right insertions see their 13 | positions grow by O(log(m)) length (the size of 14 | valueIndex) instead of O(m) length (the size of 15 | a path containing one node per insertion).

16 |

In more detail, the underlying tree consists of alternating 17 | layers:

18 |
    19 |
  • Nodes in even layers (starting with the root's children) 20 | are waypoints, each labeled by a pair (replicaID, counter). A waypoint can be either a left or right 21 | child of its parent, except that the root only has right 22 | children. Waypoint same-siblings siblings are sorted the 23 | same as nodes in [[LexSimpleTotalOrder]].
  • 24 |
  • Nodes in odd layers are value indices, each labelled 25 | by a nonnegative integer. A value index is always a right 26 | child of its parent. Value indices are sorted 27 | lexicographically; we use a subset of numbers for which 28 | this coincides with the usual order by magnitude.
  • 29 |
30 |

Each position corresponds to a value index node in the tree 31 | whose parent waypoint's replicaID equals the position's 32 | creator. A position is a string description of the path 33 | from the root to its node (excluding the root). 34 | Each pair of nodes (waypoint = (replicaID, counter), valueIndex) 35 | is represented by the substring (here written like a template literal):

36 |
${replicaID},${counter},${valueIndex}${L or R}
37 | 
38 |

where the final value is L if the next node is a left 39 | child, else R (including if this pair is the final node pair 40 | to ensure that a terminal node is sorted in between its 41 | left and right children).

42 |

Hierarchy

Index

Constructors

  • Parameters

    • options: { replicaID?: string } = {}
      • Optional replicaID?: string
        43 |

        A unique replicaID. Must be unique among all 44 | collaborating replicas, including past or concurrent replicas for the 45 | same device or user. All collaborating replicas' replicaIDs must be the 46 | same length.

        47 |

    Returns OptStringFugue

Properties

lastValueIndices: number[] = []
48 |

Maps counter to the most recently used 49 | valueIndex for the waypoint (this.replicaID, counter).

50 |
replicaID: string
51 |

Local replica ID, set in constructor. 52 | All replicaIDs have the same length.

53 |

Methods

  • compare(a: string, b: string): number
  • 54 |

    Usual compare function for sorts: returns negative if a < b in 55 | their sort order, positive if a > b.

    56 |

    Parameters

    • a: string
    • b: string

    Returns number

  • createBetween(a: undefined | string, b: undefined | string): string
  • 57 |

    Returns a globally unique new position c such that a < c < b.

    58 |

    "Globally unique" means that the created position must be distinct 59 | from all other created positions, including ones created concurrently 60 | by other users.

    61 |

    When a is undefined, it is treated as the start of the list, i.e., 62 | this returns c such that c < b. Likewise, undefined b is treated 63 | as the end of the list.

    64 |

    Parameters

    • a: undefined | string
    • b: undefined | string

    Returns string

  • newWaypointNode(): string
  • 65 |

    Returns a node corresponding to a new waypoint, also 66 | updating this.lastValueIndices accordingly.

    67 |

    Returns string

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Private property
  • Private method
  • Inherited method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /typedoc/classes/StringFugue.html: -------------------------------------------------------------------------------- 1 | StringFugue | uniquely-dense-total-order
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

A simple LexUDTotalOrder implementing the Fugue algorithm.

3 |

Hierarchy

Index

Constructors

  • new StringFugue(options?: { replicaID?: string }): StringFugue
  • Parameters

    • options: { replicaID?: string } = {}
      • Optional replicaID?: string
        6 |

        A unique replicaID. Must be unique among all 7 | collaborating replicas, including past or concurrent replicas for the 8 | same device or user. All collaborating replicas' replicaIDs must be the 9 | same length.

        10 |

    Returns StringFugue

Properties

counter: number = 0
replicaID: string
11 |

Local replica ID, set in constructor. 12 | All replicaIDs have the same length.

13 |

Methods

  • compare(a: string, b: string): number
  • 14 |

    Usual compare function for sorts: returns negative if a < b in 15 | their sort order, positive if a > b.

    16 |

    Parameters

    • a: string
    • b: string

    Returns number

  • createBetween(a: undefined | string, b: undefined | string): string
  • 17 |

    Returns a globally unique new position c such that a < c < b.

    18 |

    "Globally unique" means that the created position must be distinct 19 | from all other created positions, including ones created concurrently 20 | by other users.

    21 |

    When a is undefined, it is treated as the start of the list, i.e., 22 | this returns c such that c < b. Likewise, undefined b is treated 23 | as the end of the list.

    24 |

    Parameters

    • a: undefined | string
    • b: undefined | string

    Returns string

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Private property
  • Private method
  • Inherited method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /typedoc/classes/TreeFugue.html: -------------------------------------------------------------------------------- 1 | TreeFugue | uniquely-dense-total-order
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

A simple UniquelyDenseTotalOrder implementing the Fugue algorithm.

3 |

Hierarchy

  • TreeFugue

Implements

Index

Constructors

  • new TreeFugue(send: (message: string) => void, options?: { replicaID?: string }): TreeFugue
  • Parameters

    • send: (message: string) => void
      6 |

      When called, sends message to all other replica's receive 7 | functions, at-least-once in causal order. Also, if send is called 8 | during a createBetween call, then other replicas must 9 | receive the message before using the created position 10 | (i.e., passing it to compare or createBetween).

      11 |
        • (message: string): void
        • Parameters

          • message: string

          Returns void

    • options: { replicaID?: string } = {}
      • Optional replicaID?: string
        12 |

        A unique replicaID. Must be unique among all 13 | collaborating replicas, including past or concurrent replicas for the 14 | same device or user.

        15 |

    Returns TreeFugue

Properties

replicaID: string
16 |

Local replica ID, set in constructor.

17 |
state: Map<string, InternalPosition[]> = ...
18 |

The tree of all known (created or received) positions, represented 19 | as a collection of [[InternalPosition]]s. Specifically, this collection 20 | maps sender, then counter, to the corresponding [[InternalPosition]]. 21 | For each position, there is a unique InternalPosition object, so they are 22 | safe to compare by-reference.

23 |

Methods

  • 27 |

    Returns a globally unique new position c such that a < c < b.

    28 |

    "Globally unique" means that the created position must be distinct 29 | from all other created positions, including ones created concurrently 30 | by other users.

    31 |

    When a is undefined, it is treated as the start of the list, i.e., 32 | this returns c such that c < b. Likewise, undefined b is treated 33 | as the end of the list.

    34 |

    Parameters

    Returns TreePosition

  • 35 |

    Returns the [[InternalPosition]] corresponding to pos.

    36 |

    Throws an error if pos is not known.

    37 |

    Parameters

    Returns InternalPosition

  • receive(message: string): void
  • 38 |

    See the description of the constructor's send argument.

    39 |

    Parameters

    • message: string

    Returns void

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Private property
  • Private method
  • Inherited method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /typedoc/index.html: -------------------------------------------------------------------------------- 1 | uniquely-dense-total-order
Options
All
  • Public
  • Public/Protected
  • All
Menu

uniquely-dense-total-order

2 | 3 |

Uniquely Dense Total Orders

4 |
5 |

Interface and implementations for a uniquely dense total order abstract data type. This is a concept similar to fractional indexing, but resilient to concurrent insertions. A uniquely dense total order can be used as the core of a list/text CRDT.

6 |

This repo is a companion to the blog post Fugue: A Basic List CRDT, which gives more info about the UniquelyDenseTotalOrder interface and the Fugue implementations.

7 |

Caution: I have only minimally tested the implementations.

8 | 9 | 10 |

Docs

11 |
12 |

Open typedoc/index.html.

13 |

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Private property
  • Private method
  • Inherited method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /typedoc/interfaces/TreePosition.html: -------------------------------------------------------------------------------- 1 | TreePosition | uniquely-dense-total-order
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • TreePosition

Index

Properties

Properties

counter: number
sender: string

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Private property
  • Private method
  • Inherited method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /typedoc/interfaces/UniquelyDenseTotalOrder.html: -------------------------------------------------------------------------------- 1 | UniquelyDenseTotalOrder | uniquely-dense-total-order
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface UniquelyDenseTotalOrder<P>

2 |

Helper interface for sorting and creating unique immutable positions, 3 | suitable for use in a List CRDT.

4 |

Type parameters

  • P

    5 |

    The type of positions. Treated as immutable.

    6 |

Hierarchy

  • UniquelyDenseTotalOrder

Implemented by

Index

Methods

  • compare(a: P, b: P): number
  • 7 |

    Usual compare function for sorts: returns negative if a < b in 8 | their sort order, positive if a > b.

    9 |

    Parameters

    • a: P
    • b: P

    Returns number

  • createBetween(a: undefined | P, b: undefined | P): P
  • 10 |

    Returns a globally unique new position c such that a < c < b.

    11 |

    "Globally unique" means that the created position must be distinct 12 | from all other created positions, including ones created concurrently 13 | by other users.

    14 |

    When a is undefined, it is treated as the start of the list, i.e., 15 | this returns c such that c < b. Likewise, undefined b is treated 16 | as the end of the list.

    17 |

    Parameters

    • a: undefined | P
    • b: undefined | P

    Returns P

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Private property
  • Private method
  • Inherited method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /typedoc/modules.html: -------------------------------------------------------------------------------- 1 | uniquely-dense-total-order
Options
All
  • Public
  • Public/Protected
  • All
Menu

uniquely-dense-total-order

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Private property
  • Private method
  • Inherited method

Settings

Theme

Generated using TypeDoc

--------------------------------------------------------------------------------