├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── publishPackage.yaml │ ├── runBuildTests.yaml │ ├── runLint.yaml │ ├── runPrettier.yaml │ └── runTests.yaml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── example-servers ├── README.md ├── commonjs │ ├── .gitignore │ ├── EXAMPLE.env │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── server.js │ │ └── websocket │ │ └── utils.js ├── esm │ ├── .gitignore │ ├── EXAMPLE.env │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── server.ts │ │ └── websocket │ │ │ ├── interfaces.ts │ │ │ └── utils.ts │ └── tsconfig.json └── rollup │ ├── .gitignore │ ├── EXAMPLE.env │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ ├── server.mts │ └── websocket │ │ ├── interfaces.mts │ │ └── utils.mts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── mongo-adapter.js ├── utils.js └── y-mongodb.js ├── tests ├── generateLargeText.js └── y-mongodb.test.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | example-servers/ -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | jest: true, 5 | }, 6 | extends: ['airbnb-base', 'plugin:prettier/recommended'], 7 | plugins: ['prettier'], 8 | overrides: [], 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | sourceType: 'module', 12 | }, 13 | rules: { 14 | 'prettier/prettier': 'error', 15 | // 'linebreak-style': ['warn', 'windows'], 16 | 'no-tabs': 'off', 17 | indent: ['warn', 'tab'], 18 | 'no-underscore-dangle': 'off', // easier to see if a function is intended as private function 19 | 'import/prefer-default-export': 'off', // I like it more this way 20 | 'no-else-return': 'off', // sometimes, its just easier to read 21 | 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 22 | 'implicit-arrow-linebreak': 'off', 23 | 'import/extensions': ['error', 'ignorePackages'], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/publishPackage.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: '20.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: npm ci 16 | - run: npm run test 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/runBuildTests.yaml: -------------------------------------------------------------------------------- 1 | name: Test Builds 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | # check the esm build 9 | esm: 10 | runs-on: ubuntu-latest 11 | 12 | timeout-minutes: 10 13 | 14 | strategy: 15 | # I want the test to run on all node versions to see if a problem is caused by a specific node version 16 | fail-fast: false 17 | matrix: 18 | node-version: ['16.x', '18.x', '20.x'] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Use Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Build y-mongodb-provider 29 | run: | 30 | npm ci 31 | npm run build 32 | 33 | # I install the dependencies here and overwrite y-mongodb-provider with the just built version to use the latest version 34 | - name: Create ESM server 35 | run: | 36 | cd example-servers/esm 37 | npm ci 38 | cp -r $GITHUB_WORKSPACE/src $GITHUB_WORKSPACE/example-servers/esm/node_modules/y-mongodb-provider/ 39 | cp -r $GITHUB_WORKSPACE/dist $GITHUB_WORKSPACE/example-servers/esm/node_modules/y-mongodb-provider/ 40 | cp $GITHUB_WORKSPACE/package.json $GITHUB_WORKSPACE/example-servers/esm/node_modules/y-mongodb-provider/ 41 | npm run build 42 | 43 | # Afterwards I run it and kill it after 5 seconds to get the console output 44 | - name: Create and run ESM server 45 | id: scripts 46 | env: 47 | PORT: 3000 48 | MONGO_URL: 'mongodb://127.0.0.1:27017/todos' # doesnt really matter for this test since we dont need to connect 49 | run: | 50 | cd example-servers/esm 51 | npm run start &>> output.txt & SERVER_PID=$! 52 | sleep 5 53 | kill $SERVER_PID 54 | cat output.txt 55 | 56 | # We currently only check if the console output contains the "Yjs was already imported" error message 57 | - name: Check console output 58 | run: | 59 | cd example-servers/esm 60 | CONSOLE_OUTPUT=$(cat output.txt) 61 | echo "$CONSOLE_OUTPUT" 62 | if echo "$CONSOLE_OUTPUT" | grep -q "Yjs was already imported"; then 63 | echo "Error message found in console output" 64 | exit 1 65 | else 66 | echo "No error message found in console output" 67 | fi 68 | shell: bash 69 | 70 | # check the cjs build 71 | commonjs: 72 | runs-on: ubuntu-latest 73 | 74 | timeout-minutes: 10 75 | 76 | strategy: 77 | # I want the test to run on all node versions to see if a problem is caused by a specific node version 78 | fail-fast: false 79 | matrix: 80 | node-version: ['16.x', '18.x', '20.x'] 81 | 82 | steps: 83 | - uses: actions/checkout@v4 84 | 85 | - name: Use Node.js 86 | uses: actions/setup-node@v4 87 | with: 88 | node-version: ${{ matrix.node-version }} 89 | 90 | - name: Build y-mongodb-provider 91 | run: | 92 | npm ci 93 | npm run build 94 | 95 | # I install the dependencies here and overwrite y-mongodb-provider with the just built version to use the latest version 96 | - name: Create CJM server 97 | run: | 98 | cd example-servers/commonjs 99 | npm ci 100 | cp -r $GITHUB_WORKSPACE/src $GITHUB_WORKSPACE/example-servers/commonjs/node_modules/y-mongodb-provider/ 101 | cp -r $GITHUB_WORKSPACE/dist $GITHUB_WORKSPACE/example-servers/commonjs/node_modules/y-mongodb-provider/ 102 | cp $GITHUB_WORKSPACE/package.json $GITHUB_WORKSPACE/example-servers/commonjs/node_modules/y-mongodb-provider/ 103 | 104 | # Afterwards I run it and kill it after 5 seconds to get the console output 105 | - name: Create and run CJM server 106 | id: scripts 107 | env: 108 | PORT: 3000 109 | MONGO_URL: 'mongodb://127.0.0.1:27017/todos' # doesnt really matter for this test since we dont need to connect 110 | run: | 111 | cd example-servers/commonjs 112 | npm run start &>> output.txt & SERVER_PID=$! 113 | sleep 5 114 | kill $SERVER_PID 115 | cat output.txt 116 | 117 | # We currently only check if the console output contains the "Yjs was already imported" error message 118 | - name: Check console output 119 | run: | 120 | cd example-servers/commonjs 121 | CONSOLE_OUTPUT=$(cat output.txt) 122 | echo "$CONSOLE_OUTPUT" 123 | if echo "$CONSOLE_OUTPUT" | grep -q "Yjs was already imported"; then 124 | echo "Error message found in console output" 125 | exit 1 126 | else 127 | echo "No error message found in console output" 128 | fi 129 | shell: bash 130 | 131 | # check the rollup build 132 | rollup: 133 | runs-on: ubuntu-latest 134 | 135 | timeout-minutes: 10 136 | 137 | strategy: 138 | # I want the test to run on all node versions to see if a problem is caused by a specific node version 139 | fail-fast: false 140 | matrix: 141 | node-version: ['16.x', '18.x', '20.x'] 142 | 143 | steps: 144 | - uses: actions/checkout@v4 145 | 146 | - name: Use Node.js 147 | uses: actions/setup-node@v4 148 | with: 149 | node-version: ${{ matrix.node-version }} 150 | 151 | - name: Build y-mongodb-provider 152 | run: | 153 | npm ci 154 | npm run build 155 | 156 | # I install the dependencies here and overwrite y-mongodb-provider with the just built version to use the latest version 157 | - name: Create rollup server 158 | run: | 159 | cd example-servers/rollup 160 | npm ci 161 | cp -r $GITHUB_WORKSPACE/src $GITHUB_WORKSPACE/example-servers/rollup/node_modules/y-mongodb-provider/ 162 | cp -r $GITHUB_WORKSPACE/dist $GITHUB_WORKSPACE/example-servers/rollup/node_modules/y-mongodb-provider/ 163 | cp $GITHUB_WORKSPACE/package.json $GITHUB_WORKSPACE/example-servers/rollup/node_modules/y-mongodb-provider/ 164 | npm run build 165 | 166 | # Afterwards I run it and kill it after 5 seconds to get the console output 167 | - name: Create and run rollup server 168 | id: scripts 169 | env: 170 | PORT: 3000 171 | MONGO_URL: 'mongodb://127.0.0.1:27017/todos' # doesnt really matter for this test since we dont need to connect 172 | run: | 173 | cd example-servers/rollup 174 | npm run start &>> output.txt & SERVER_PID=$! 175 | sleep 5 176 | kill $SERVER_PID 177 | cat output.txt 178 | 179 | # We currently only check if the console output contains the "Yjs was already imported" error message 180 | - name: Check console output 181 | run: | 182 | cd example-servers/rollup 183 | CONSOLE_OUTPUT=$(cat output.txt) 184 | echo "$CONSOLE_OUTPUT" 185 | if echo "$CONSOLE_OUTPUT" | grep -q "Yjs was already imported"; then 186 | echo "Error message found in console output" 187 | exit 1 188 | else 189 | echo "No error message found in console output" 190 | fi 191 | shell: bash 192 | -------------------------------------------------------------------------------- /.github/workflows/runLint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Check 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | 11 | timeout-minutes: 5 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '16' 20 | 21 | - name: Install Dependencies 22 | run: npm ci 23 | 24 | - name: Run Linter 25 | run: npm run lint 26 | -------------------------------------------------------------------------------- /.github/workflows/runPrettier.yaml: -------------------------------------------------------------------------------- 1 | name: Prettier Check 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | prettier: 9 | runs-on: ubuntu-latest 10 | 11 | timeout-minutes: 5 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '16' 20 | 21 | - name: Install Dependencies 22 | run: npm ci 23 | 24 | - name: Run Prettier 25 | run: npx prettier --write . 26 | -------------------------------------------------------------------------------- /.github/workflows/runTests.yaml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | # to run this workflow manually 5 | workflow_dispatch: 6 | 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | timeout-minutes: 5 15 | 16 | strategy: 17 | # I want the test to run on all node versions to see if a problem is caused by a specific node version 18 | fail-fast: false 19 | matrix: 20 | node-version: ['16.x', '18.x', '20.x'] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Use Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Install Dependencies 31 | run: npm ci 32 | 33 | - name: Run Tests 34 | run: npm run test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": true, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "trailingComma": "all", 9 | "bracketSpacing": true, 10 | "arrowParens": "always" 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Max Nötzold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mongodb database adapter for [Yjs](https://github.com/yjs/yjs) 2 | 3 | Persistent MongoDB storage for [y-websocket](https://github.com/yjs/y-websocket) server. You can use this adapter to easily store and retrieve Yjs documents in/from MongoDB. 4 | 5 | ### Notes: 6 | 7 | - This was once a fork of the official [y-leveldb](https://github.com/yjs/y-leveldb) but for MongoDB 8 | - This package is not officially supported by the Yjs team. 9 | 10 | ## Use it (Installation) 11 | 12 | You need Node version 16 or newer. 13 | 14 | It is available at [npm](https://www.npmjs.com/package/y-mongodb-provider). 15 | 16 | ```sh 17 | npm i y-mongodb-provider 18 | ``` 19 | 20 | #### Simple Server Example 21 | 22 | There are full working server examples in the `example-servers` directory. 23 | 24 | ```js 25 | import http from 'http'; 26 | import { WebSocketServer } from 'ws'; 27 | import * as Y from 'yjs'; 28 | import { MongodbPersistence } from 'y-mongodb-provider'; 29 | import yUtils from 'y-websocket/bin/utils'; 30 | 31 | const server = http.createServer((request, response) => { 32 | response.writeHead(200, { 'Content-Type': 'text/plain' }); 33 | response.end('okay'); 34 | }); 35 | 36 | // y-websocket 37 | const wss = new WebSocketServer({ server }); 38 | wss.on('connection', yUtils.setupWSConnection); 39 | 40 | /* 41 | * y-mongodb-provider 42 | * with all possible options (see API section below) 43 | */ 44 | const mdb = new MongodbPersistence(createConnectionString('yjstest'), { 45 | collectionName: 'transactions', 46 | flushSize: 100, 47 | multipleCollections: true, 48 | }); 49 | 50 | /* 51 | Persistence must have the following signature: 52 | { bindState: function(string,WSSharedDoc):void, writeState:function(string,WSSharedDoc):Promise } 53 | */ 54 | yUtils.setPersistence({ 55 | bindState: async (docName, ydoc) => { 56 | // Here you listen to granular document updates and store them in the database 57 | // You don't have to do this, but it ensures that you don't lose content when the server crashes 58 | // See https://github.com/yjs/yjs#Document-Updates for documentation on how to encode 59 | // document updates 60 | 61 | // official default code from: https://github.com/yjs/y-websocket/blob/37887badc1f00326855a29fc6b9197745866c3aa/bin/utils.js#L36 62 | const persistedYdoc = await mdb.getYDoc(docName); 63 | const newUpdates = Y.encodeStateAsUpdate(ydoc); 64 | mdb.storeUpdate(docName, newUpdates); 65 | Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc)); 66 | ydoc.on('update', async (update) => { 67 | mdb.storeUpdate(docName, update); 68 | }); 69 | }, 70 | writeState: async (docName, ydoc) => { 71 | // This is called when all connections to the document are closed. 72 | // In the future, this method might also be called in intervals or after a certain number of updates. 73 | return new Promise((resolve) => { 74 | // When the returned Promise resolves, the document will be destroyed. 75 | // So make sure that the document really has been written to the database. 76 | resolve(); 77 | }); 78 | }, 79 | }); 80 | 81 | server.listen(port, () => { 82 | console.log('listening on port:' + port); 83 | }); 84 | ``` 85 | 86 | ## API 87 | 88 | ### `persistence = MongodbPersistence(connectionObj: string|{ client: MongoClient, db: Db }, options: object)` 89 | 90 | Create a y-mongodb-provider persistence instance. 91 | 92 | ```js 93 | import { MongodbPersistence } from 'y-mongodb-provider'; 94 | 95 | const persistence = new MongodbPersistence(connectionObj, { 96 | collectionName, 97 | flushSize, 98 | multipleCollections, 99 | }); 100 | ``` 101 | 102 | `connectionObj` can be a connection string or an object with a client and db property. If you pass a connection string, the client and db will be created for you. [It is recommended to use the object form if you use the same client on other parts of your application as well](https://www.mongodb.com/docs/manual/administration/connection-pool-overview/#create-and-use-a-connection-pool). 103 | 104 | Options: 105 | 106 | - `collectionName` 107 | - Name of the collection where all documents are stored 108 | - Default: `"yjs-writings"` 109 | - `flushSize` 110 | - The number of transactions needed until they are merged automatically into one document 111 | - Default: `400` 112 | - `multipleCollections` 113 | - When set to true, each document gets an own collection (instead of all documents stored in the same one) 114 | - When set to true, the option collectionName gets ignored. 115 | - Default: `false` 116 | - **Note**: When you dont set this setting to true, you should [create an index for your MongoDB collection](https://github.com/MaxNoetzold/y-mongodb-provider?tab=readme-ov-file#indexes). 117 | 118 | #### `persistence.getYDoc(docName: string): Promise` 119 | 120 | Create a Y.Doc instance with the data persistet in MongoDB. Use this to 121 | temporarily create a Yjs document to sync changes or extract data. 122 | 123 | #### `persistence.storeUpdate(docName: string, update: Uint8Array): Promise` 124 | 125 | Store a single document update to the database. 126 | 127 | #### `persistence.getStateVector(docName: string): Promise` 128 | 129 | The state vector (describing the state of the persisted document - see 130 | [Yjs docs](https://github.com/yjs/yjs#Document-Updates)) is maintained in a separate 131 | field and constantly updated. 132 | 133 | This allows you to sync changes without actually creating a Yjs document. 134 | 135 | #### `persistence.getDiff(docName: string, stateVector: Uint8Array): Promise` 136 | 137 | Get the differences directly from the database. The same as 138 | `Y.encodeStateAsUpdate(ydoc, stateVector)`. 139 | 140 | #### `persistence.clearDocument(docName: string): Promise` 141 | 142 | Delete a document, and all associated data from the database. 143 | 144 | #### `persistence.setMeta(docName: string, metaKey: string, value: any): Promise` 145 | 146 | Persist some meta information in the database and associate it with a document. 147 | It is up to you what you store here. You could, for example, store credentials 148 | here. 149 | 150 | #### `persistence.getMeta(docName: string, metaKey: string): Promise` 151 | 152 | Retrieve a store meta value from the database. Returns undefined if the 153 | `metaKey` doesn't exist. 154 | 155 | #### `persistence.delMeta(docName: string, metaKey: string): Promise` 156 | 157 | Delete a store meta value. 158 | 159 | #### `persistence.getAllDocNames(docName: string): Promise>` 160 | 161 | Retrieve the names of all stored documents. 162 | 163 | #### `persistence.getAllDocStateVectors(docName: string): Promise { 199 | const persistedYdoc = await mdb.getYDoc(docName); 200 | // get the state vector so we can just store the diffs between client and server 201 | const persistedStateVector = Y.encodeStateVector(persistedYdoc); 202 | 203 | /* we could also retrieve that sv with a mdb function 204 | * however this takes longer; 205 | * it would also flush the document (which merges all updates into one) 206 | * thats prob a good thing, which is why we always do this on document close (see writeState) 207 | */ 208 | //const persistedStateVector = await mdb.getStateVector(docName); 209 | 210 | // in the default code the following value gets saved in the db 211 | // this however leads to the case that multiple complete Y.Docs are saved in the db (https://github.com/fadiquader/y-mongodb/issues/7) 212 | //const newUpdates = Y.encodeStateAsUpdate(ydoc); 213 | 214 | // better just get the differences and save those: 215 | const diff = Y.encodeStateAsUpdate(ydoc, persistedStateVector); 216 | 217 | // store the new data in db (if there is any: empty update is an array of 0s) 218 | if (diff.reduce((previousValue, currentValue) => previousValue + currentValue, 0) > 0) 219 | mdb.storeUpdate(docName, diff); 220 | 221 | // send the persisted data to clients 222 | Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc)); 223 | 224 | // store updates of the document in db 225 | ydoc.on('update', async (update) => { 226 | mdb.storeUpdate(docName, update); 227 | }); 228 | 229 | // cleanup some memory 230 | persistedYdoc.destroy(); 231 | }, 232 | writeState: async (docName, ydoc) => { 233 | // This is called when all connections to the document are closed. 234 | 235 | // flush document on close to have the smallest possible database 236 | await mdb.flushDocument(docName); 237 | }, 238 | }); 239 | ``` 240 | 241 | ## Contributing 242 | 243 | We welcome contributions! Please follow these steps to contribute: 244 | 245 | 1. Fork the repository. 246 | 2. Set up your development environment: `npm install`. 247 | 3. Make your changes and ensure tests pass: `npm test`. 248 | 4. Submit a pull request with your changes. 249 | 250 | ### Note about TypeScript 251 | 252 | We use type checking and type file generation with JSDoc comments. We do not use TypeScript in this project. We want to keep close to the original Yjs project, which is written in JavaScript. To read more about the decision to use JSDoc comments instead of the more conventional TypeScript, see [this issue](https://discuss.yjs.dev/t/why-are-yjs-types-writen-with-jsdocs-and-not-typescript/2668/3). 253 | 254 | If you are adding new functionality, please ensure that you add JSDoc comments to your code. 255 | 256 | ### Testing 257 | 258 | To run the test suite, use the following command: 259 | 260 | ```sh 261 | npm test 262 | ``` 263 | 264 | ## License 265 | 266 | y-mongodb-provider is licensed under the [MIT License](./LICENSE). 267 | 268 | 269 | -------------------------------------------------------------------------------- /example-servers/README.md: -------------------------------------------------------------------------------- 1 | # Y-Websocket & Y-Mongodb-Provider 2 | 3 | This is a simple Node server example that runs [y-websocket](https://github.com/yjs/y-websocket/) with [persistence](https://github.com/MaxNoetzold/y-mongodb-provider) for [Mongodb](https://www.mongodb.com/de-de). 4 | 5 | This server is a simplified version of the [official example for a y-websocket server](https://github.com/yjs/y-websocket/tree/master/bin). 6 | 7 | ## Directories 8 | 9 | This backend was implemented with three systems in mind: CommonJS, ESM, and Bundling (with Rollup). The goal is to ensure that the y-mongodb-provider works in all three environments. 10 | 11 | ## Testing 12 | 13 | They are also used for automatic tests to check if the library works in all three environments. 14 | -------------------------------------------------------------------------------- /example-servers/commonjs/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /example-servers/commonjs/EXAMPLE.env: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | MONGO_URL="mongodb://127.0.0.1:27017/todos" 3 | GC="true" -------------------------------------------------------------------------------- /example-servers/commonjs/README.md: -------------------------------------------------------------------------------- 1 | # Y-Websocket & Y-Mongodb-Provider - Commonjs 2 | 3 | This is a simple Node server that runs [y-websocket](https://github.com/yjs/y-websocket/) with [persistence](https://github.com/MaxNoetzold/y-mongodb-provider) for [Mongodb](https://www.mongodb.com/de-de). It is written in TypeScript and requires Node v20. 4 | 5 | This server is a simplified version of the [official example for a y-websocket server](https://github.com/yjs/y-websocket/tree/master/bin). 6 | 7 | ## How to run? 8 | 9 | First, install the dependencies with `npm install`. 10 | 11 | Next, copy the `EXAMPLE.env` file, rename it to `.env`, and edit the entries as needed. 12 | 13 | To compile and run the server code, use `npm run start` or `npm run dev`. 14 | -------------------------------------------------------------------------------- /example-servers/commonjs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-todo-backend-commonjs", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "y-todo-backend-commonjs", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "dotenv": "^16.5.0", 13 | "ws": "^8.18.2", 14 | "y-mongodb-provider": "^0.2.0", 15 | "y-websocket": "^3.0.0", 16 | "yjs": "^13.6.27" 17 | }, 18 | "devDependencies": { 19 | "nodemon": "^3.1.10" 20 | } 21 | }, 22 | "node_modules/@mongodb-js/saslprep": { 23 | "version": "1.2.2", 24 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", 25 | "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", 26 | "license": "MIT", 27 | "dependencies": { 28 | "sparse-bitfield": "^3.0.3" 29 | } 30 | }, 31 | "node_modules/@types/webidl-conversions": { 32 | "version": "7.0.3", 33 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", 34 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", 35 | "license": "MIT" 36 | }, 37 | "node_modules/@types/whatwg-url": { 38 | "version": "11.0.5", 39 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", 40 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", 41 | "license": "MIT", 42 | "dependencies": { 43 | "@types/webidl-conversions": "*" 44 | } 45 | }, 46 | "node_modules/abbrev": { 47 | "version": "1.1.1", 48 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 49 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", 50 | "dev": true 51 | }, 52 | "node_modules/anymatch": { 53 | "version": "3.1.3", 54 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 55 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 56 | "dev": true, 57 | "dependencies": { 58 | "normalize-path": "^3.0.0", 59 | "picomatch": "^2.0.4" 60 | }, 61 | "engines": { 62 | "node": ">= 8" 63 | } 64 | }, 65 | "node_modules/balanced-match": { 66 | "version": "1.0.2", 67 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 68 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 69 | "dev": true 70 | }, 71 | "node_modules/binary-extensions": { 72 | "version": "2.2.0", 73 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 74 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 75 | "dev": true, 76 | "engines": { 77 | "node": ">=8" 78 | } 79 | }, 80 | "node_modules/brace-expansion": { 81 | "version": "1.1.11", 82 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 83 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 84 | "dev": true, 85 | "dependencies": { 86 | "balanced-match": "^1.0.0", 87 | "concat-map": "0.0.1" 88 | } 89 | }, 90 | "node_modules/braces": { 91 | "version": "3.0.2", 92 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 93 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 94 | "dev": true, 95 | "dependencies": { 96 | "fill-range": "^7.0.1" 97 | }, 98 | "engines": { 99 | "node": ">=8" 100 | } 101 | }, 102 | "node_modules/bson": { 103 | "version": "6.10.3", 104 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", 105 | "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", 106 | "license": "Apache-2.0", 107 | "engines": { 108 | "node": ">=16.20.1" 109 | } 110 | }, 111 | "node_modules/chokidar": { 112 | "version": "3.5.3", 113 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 114 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 115 | "dev": true, 116 | "funding": [ 117 | { 118 | "type": "individual", 119 | "url": "https://paulmillr.com/funding/" 120 | } 121 | ], 122 | "dependencies": { 123 | "anymatch": "~3.1.2", 124 | "braces": "~3.0.2", 125 | "glob-parent": "~5.1.2", 126 | "is-binary-path": "~2.1.0", 127 | "is-glob": "~4.0.1", 128 | "normalize-path": "~3.0.0", 129 | "readdirp": "~3.6.0" 130 | }, 131 | "engines": { 132 | "node": ">= 8.10.0" 133 | }, 134 | "optionalDependencies": { 135 | "fsevents": "~2.3.2" 136 | } 137 | }, 138 | "node_modules/concat-map": { 139 | "version": "0.0.1", 140 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 141 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 142 | "dev": true 143 | }, 144 | "node_modules/debug": { 145 | "version": "4.3.4", 146 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 147 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 148 | "dev": true, 149 | "dependencies": { 150 | "ms": "2.1.2" 151 | }, 152 | "engines": { 153 | "node": ">=6.0" 154 | }, 155 | "peerDependenciesMeta": { 156 | "supports-color": { 157 | "optional": true 158 | } 159 | } 160 | }, 161 | "node_modules/dotenv": { 162 | "version": "16.5.0", 163 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", 164 | "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", 165 | "license": "BSD-2-Clause", 166 | "engines": { 167 | "node": ">=12" 168 | }, 169 | "funding": { 170 | "url": "https://dotenvx.com" 171 | } 172 | }, 173 | "node_modules/fill-range": { 174 | "version": "7.0.1", 175 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 176 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 177 | "dev": true, 178 | "dependencies": { 179 | "to-regex-range": "^5.0.1" 180 | }, 181 | "engines": { 182 | "node": ">=8" 183 | } 184 | }, 185 | "node_modules/fsevents": { 186 | "version": "2.3.3", 187 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 188 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 189 | "dev": true, 190 | "hasInstallScript": true, 191 | "optional": true, 192 | "os": [ 193 | "darwin" 194 | ], 195 | "engines": { 196 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 197 | } 198 | }, 199 | "node_modules/glob-parent": { 200 | "version": "5.1.2", 201 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 202 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 203 | "dev": true, 204 | "dependencies": { 205 | "is-glob": "^4.0.1" 206 | }, 207 | "engines": { 208 | "node": ">= 6" 209 | } 210 | }, 211 | "node_modules/has-flag": { 212 | "version": "3.0.0", 213 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 214 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 215 | "dev": true, 216 | "engines": { 217 | "node": ">=4" 218 | } 219 | }, 220 | "node_modules/ignore-by-default": { 221 | "version": "1.0.1", 222 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 223 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 224 | "dev": true 225 | }, 226 | "node_modules/is-binary-path": { 227 | "version": "2.1.0", 228 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 229 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 230 | "dev": true, 231 | "dependencies": { 232 | "binary-extensions": "^2.0.0" 233 | }, 234 | "engines": { 235 | "node": ">=8" 236 | } 237 | }, 238 | "node_modules/is-extglob": { 239 | "version": "2.1.1", 240 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 241 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 242 | "dev": true, 243 | "engines": { 244 | "node": ">=0.10.0" 245 | } 246 | }, 247 | "node_modules/is-glob": { 248 | "version": "4.0.3", 249 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 250 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 251 | "dev": true, 252 | "dependencies": { 253 | "is-extglob": "^2.1.1" 254 | }, 255 | "engines": { 256 | "node": ">=0.10.0" 257 | } 258 | }, 259 | "node_modules/is-number": { 260 | "version": "7.0.0", 261 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 262 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 263 | "dev": true, 264 | "engines": { 265 | "node": ">=0.12.0" 266 | } 267 | }, 268 | "node_modules/isomorphic.js": { 269 | "version": "0.2.5", 270 | "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", 271 | "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", 272 | "funding": { 273 | "type": "GitHub Sponsors ❤", 274 | "url": "https://github.com/sponsors/dmonad" 275 | } 276 | }, 277 | "node_modules/lib0": { 278 | "version": "0.2.108", 279 | "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.108.tgz", 280 | "integrity": "sha512-+3eK/B0SqYoZiQu9fNk4VEc6EX8cb0Li96tPGKgugzoGj/OdRdREtuTLvUW+mtinoB2mFiJjSqOJBIaMkAGhxQ==", 281 | "license": "MIT", 282 | "dependencies": { 283 | "isomorphic.js": "^0.2.4" 284 | }, 285 | "bin": { 286 | "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", 287 | "0gentesthtml": "bin/gentesthtml.js", 288 | "0serve": "bin/0serve.js" 289 | }, 290 | "engines": { 291 | "node": ">=16" 292 | }, 293 | "funding": { 294 | "type": "GitHub Sponsors ❤", 295 | "url": "https://github.com/sponsors/dmonad" 296 | } 297 | }, 298 | "node_modules/lru-cache": { 299 | "version": "6.0.0", 300 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 301 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 302 | "dev": true, 303 | "dependencies": { 304 | "yallist": "^4.0.0" 305 | }, 306 | "engines": { 307 | "node": ">=10" 308 | } 309 | }, 310 | "node_modules/memory-pager": { 311 | "version": "1.5.0", 312 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 313 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 314 | "license": "MIT" 315 | }, 316 | "node_modules/minimatch": { 317 | "version": "3.1.2", 318 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 319 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 320 | "dev": true, 321 | "dependencies": { 322 | "brace-expansion": "^1.1.7" 323 | }, 324 | "engines": { 325 | "node": "*" 326 | } 327 | }, 328 | "node_modules/mongodb": { 329 | "version": "6.16.0", 330 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", 331 | "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", 332 | "license": "Apache-2.0", 333 | "dependencies": { 334 | "@mongodb-js/saslprep": "^1.1.9", 335 | "bson": "^6.10.3", 336 | "mongodb-connection-string-url": "^3.0.0" 337 | }, 338 | "engines": { 339 | "node": ">=16.20.1" 340 | }, 341 | "peerDependencies": { 342 | "@aws-sdk/credential-providers": "^3.188.0", 343 | "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", 344 | "gcp-metadata": "^5.2.0", 345 | "kerberos": "^2.0.1", 346 | "mongodb-client-encryption": ">=6.0.0 <7", 347 | "snappy": "^7.2.2", 348 | "socks": "^2.7.1" 349 | }, 350 | "peerDependenciesMeta": { 351 | "@aws-sdk/credential-providers": { 352 | "optional": true 353 | }, 354 | "@mongodb-js/zstd": { 355 | "optional": true 356 | }, 357 | "gcp-metadata": { 358 | "optional": true 359 | }, 360 | "kerberos": { 361 | "optional": true 362 | }, 363 | "mongodb-client-encryption": { 364 | "optional": true 365 | }, 366 | "snappy": { 367 | "optional": true 368 | }, 369 | "socks": { 370 | "optional": true 371 | } 372 | } 373 | }, 374 | "node_modules/mongodb-connection-string-url": { 375 | "version": "3.0.2", 376 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", 377 | "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", 378 | "license": "Apache-2.0", 379 | "dependencies": { 380 | "@types/whatwg-url": "^11.0.2", 381 | "whatwg-url": "^14.1.0 || ^13.0.0" 382 | } 383 | }, 384 | "node_modules/ms": { 385 | "version": "2.1.2", 386 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 387 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 388 | "dev": true 389 | }, 390 | "node_modules/nodemon": { 391 | "version": "3.1.10", 392 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", 393 | "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", 394 | "dev": true, 395 | "license": "MIT", 396 | "dependencies": { 397 | "chokidar": "^3.5.2", 398 | "debug": "^4", 399 | "ignore-by-default": "^1.0.1", 400 | "minimatch": "^3.1.2", 401 | "pstree.remy": "^1.1.8", 402 | "semver": "^7.5.3", 403 | "simple-update-notifier": "^2.0.0", 404 | "supports-color": "^5.5.0", 405 | "touch": "^3.1.0", 406 | "undefsafe": "^2.0.5" 407 | }, 408 | "bin": { 409 | "nodemon": "bin/nodemon.js" 410 | }, 411 | "engines": { 412 | "node": ">=10" 413 | }, 414 | "funding": { 415 | "type": "opencollective", 416 | "url": "https://opencollective.com/nodemon" 417 | } 418 | }, 419 | "node_modules/nopt": { 420 | "version": "1.0.10", 421 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 422 | "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", 423 | "dev": true, 424 | "dependencies": { 425 | "abbrev": "1" 426 | }, 427 | "bin": { 428 | "nopt": "bin/nopt.js" 429 | }, 430 | "engines": { 431 | "node": "*" 432 | } 433 | }, 434 | "node_modules/normalize-path": { 435 | "version": "3.0.0", 436 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 437 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 438 | "dev": true, 439 | "engines": { 440 | "node": ">=0.10.0" 441 | } 442 | }, 443 | "node_modules/picomatch": { 444 | "version": "2.3.1", 445 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 446 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 447 | "dev": true, 448 | "engines": { 449 | "node": ">=8.6" 450 | }, 451 | "funding": { 452 | "url": "https://github.com/sponsors/jonschlinkert" 453 | } 454 | }, 455 | "node_modules/pstree.remy": { 456 | "version": "1.1.8", 457 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 458 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 459 | "dev": true 460 | }, 461 | "node_modules/punycode": { 462 | "version": "2.3.1", 463 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 464 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 465 | "license": "MIT", 466 | "engines": { 467 | "node": ">=6" 468 | } 469 | }, 470 | "node_modules/readdirp": { 471 | "version": "3.6.0", 472 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 473 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 474 | "dev": true, 475 | "dependencies": { 476 | "picomatch": "^2.2.1" 477 | }, 478 | "engines": { 479 | "node": ">=8.10.0" 480 | } 481 | }, 482 | "node_modules/semver": { 483 | "version": "7.5.4", 484 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 485 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 486 | "dev": true, 487 | "dependencies": { 488 | "lru-cache": "^6.0.0" 489 | }, 490 | "bin": { 491 | "semver": "bin/semver.js" 492 | }, 493 | "engines": { 494 | "node": ">=10" 495 | } 496 | }, 497 | "node_modules/simple-update-notifier": { 498 | "version": "2.0.0", 499 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 500 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 501 | "dev": true, 502 | "dependencies": { 503 | "semver": "^7.5.3" 504 | }, 505 | "engines": { 506 | "node": ">=10" 507 | } 508 | }, 509 | "node_modules/sparse-bitfield": { 510 | "version": "3.0.3", 511 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 512 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", 513 | "license": "MIT", 514 | "dependencies": { 515 | "memory-pager": "^1.0.2" 516 | } 517 | }, 518 | "node_modules/supports-color": { 519 | "version": "5.5.0", 520 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 521 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 522 | "dev": true, 523 | "dependencies": { 524 | "has-flag": "^3.0.0" 525 | }, 526 | "engines": { 527 | "node": ">=4" 528 | } 529 | }, 530 | "node_modules/to-regex-range": { 531 | "version": "5.0.1", 532 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 533 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 534 | "dev": true, 535 | "dependencies": { 536 | "is-number": "^7.0.0" 537 | }, 538 | "engines": { 539 | "node": ">=8.0" 540 | } 541 | }, 542 | "node_modules/touch": { 543 | "version": "3.1.0", 544 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", 545 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", 546 | "dev": true, 547 | "dependencies": { 548 | "nopt": "~1.0.10" 549 | }, 550 | "bin": { 551 | "nodetouch": "bin/nodetouch.js" 552 | } 553 | }, 554 | "node_modules/tr46": { 555 | "version": "5.1.1", 556 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", 557 | "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", 558 | "license": "MIT", 559 | "dependencies": { 560 | "punycode": "^2.3.1" 561 | }, 562 | "engines": { 563 | "node": ">=18" 564 | } 565 | }, 566 | "node_modules/undefsafe": { 567 | "version": "2.0.5", 568 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 569 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 570 | "dev": true 571 | }, 572 | "node_modules/webidl-conversions": { 573 | "version": "7.0.0", 574 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 575 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 576 | "license": "BSD-2-Clause", 577 | "engines": { 578 | "node": ">=12" 579 | } 580 | }, 581 | "node_modules/whatwg-url": { 582 | "version": "14.2.0", 583 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", 584 | "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", 585 | "license": "MIT", 586 | "dependencies": { 587 | "tr46": "^5.1.0", 588 | "webidl-conversions": "^7.0.0" 589 | }, 590 | "engines": { 591 | "node": ">=18" 592 | } 593 | }, 594 | "node_modules/ws": { 595 | "version": "8.18.2", 596 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", 597 | "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", 598 | "license": "MIT", 599 | "engines": { 600 | "node": ">=10.0.0" 601 | }, 602 | "peerDependencies": { 603 | "bufferutil": "^4.0.1", 604 | "utf-8-validate": ">=5.0.2" 605 | }, 606 | "peerDependenciesMeta": { 607 | "bufferutil": { 608 | "optional": true 609 | }, 610 | "utf-8-validate": { 611 | "optional": true 612 | } 613 | } 614 | }, 615 | "node_modules/y-mongodb-provider": { 616 | "version": "0.2.0", 617 | "resolved": "https://registry.npmjs.org/y-mongodb-provider/-/y-mongodb-provider-0.2.0.tgz", 618 | "integrity": "sha512-l2Qus1lix7TkxemLGzMJn8HYKiUD+vLJpZxYjtPvdBNbM7THhgVuyHvZYzknUDonvnBBjnpbDaZkKPWOMAsSAw==", 619 | "license": "MIT", 620 | "dependencies": { 621 | "lib0": "^0.2.94", 622 | "mongodb": "^6.7.0" 623 | }, 624 | "peerDependencies": { 625 | "yjs": "^13.6.15" 626 | } 627 | }, 628 | "node_modules/y-protocols": { 629 | "version": "1.0.6", 630 | "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", 631 | "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", 632 | "dependencies": { 633 | "lib0": "^0.2.85" 634 | }, 635 | "engines": { 636 | "node": ">=16.0.0", 637 | "npm": ">=8.0.0" 638 | }, 639 | "funding": { 640 | "type": "GitHub Sponsors ❤", 641 | "url": "https://github.com/sponsors/dmonad" 642 | }, 643 | "peerDependencies": { 644 | "yjs": "^13.0.0" 645 | } 646 | }, 647 | "node_modules/y-websocket": { 648 | "version": "3.0.0", 649 | "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-3.0.0.tgz", 650 | "integrity": "sha512-mUHy7AzkOZ834T/7piqtlA8Yk6AchqKqcrCXjKW8J1w2lPtRDjz8W5/CvXz9higKAHgKRKqpI3T33YkRFLkPtg==", 651 | "license": "MIT", 652 | "dependencies": { 653 | "lib0": "^0.2.102", 654 | "y-protocols": "^1.0.5" 655 | }, 656 | "engines": { 657 | "node": ">=16.0.0", 658 | "npm": ">=8.0.0" 659 | }, 660 | "funding": { 661 | "type": "GitHub Sponsors ❤", 662 | "url": "https://github.com/sponsors/dmonad" 663 | }, 664 | "peerDependencies": { 665 | "yjs": "^13.5.6" 666 | } 667 | }, 668 | "node_modules/yallist": { 669 | "version": "4.0.0", 670 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 671 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 672 | "dev": true 673 | }, 674 | "node_modules/yjs": { 675 | "version": "13.6.27", 676 | "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", 677 | "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", 678 | "license": "MIT", 679 | "dependencies": { 680 | "lib0": "^0.2.99" 681 | }, 682 | "engines": { 683 | "node": ">=16.0.0", 684 | "npm": ">=8.0.0" 685 | }, 686 | "funding": { 687 | "type": "GitHub Sponsors ❤", 688 | "url": "https://github.com/sponsors/dmonad" 689 | } 690 | } 691 | } 692 | } 693 | -------------------------------------------------------------------------------- /example-servers/commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-todo-backend-commonjs", 3 | "version": "1.0.0", 4 | "type": "commonjs", 5 | "author": "max.noetzold@gmail.com", 6 | "license": "MIT", 7 | "main": "./src/server.ts", 8 | "scripts": { 9 | "start": "node src/server.js", 10 | "dev": "nodemon --watch ./src --exec 'npm run start'" 11 | }, 12 | "devDependencies": { 13 | "nodemon": "^3.1.10" 14 | }, 15 | "dependencies": { 16 | "dotenv": "^16.5.0", 17 | "ws": "^8.18.2", 18 | "y-mongodb-provider": "^0.2.0", 19 | "y-websocket": "^3.0.0", 20 | "yjs": "^13.6.27" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example-servers/commonjs/src/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const http = require('http'); 3 | const WebSocketServer = require('ws').Server; 4 | const Y = require('yjs'); 5 | const { MongodbPersistence } = require('y-mongodb-provider'); 6 | const { setPersistence, setupWSConnection } = require('./websocket/utils.js'); 7 | 8 | const server = http.createServer((request, response) => { 9 | response.writeHead(200, { 'Content-Type': 'text/plain' }); 10 | response.end('okay'); 11 | }); 12 | 13 | // y-websocket 14 | const wss = new WebSocketServer({ server }); 15 | wss.on('connection', setupWSConnection); 16 | 17 | /* 18 | * y-mongodb-provider 19 | */ 20 | if (!process.env.MONGO_URL) { 21 | throw new Error('Please define the MONGO_URL environment variable'); 22 | } 23 | const mdb = new MongodbPersistence(process.env.MONGO_URL, { 24 | flushSize: 100, 25 | multipleCollections: true, 26 | }); 27 | 28 | setPersistence({ 29 | bindState: async (docName, ydoc) => { 30 | const persistedYdoc = await mdb.getYDoc(docName); 31 | const newUpdates = Y.encodeStateAsUpdate(ydoc); 32 | mdb.storeUpdate(docName, newUpdates); 33 | Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc)); 34 | ydoc.on('update', async (update) => { 35 | mdb.storeUpdate(docName, update); 36 | }); 37 | }, 38 | writeState: () => { 39 | return new Promise((resolve) => { 40 | resolve(true); 41 | }); 42 | }, 43 | }); 44 | 45 | server.listen(process.env.PORT, () => { 46 | // eslint-disable-next-line no-console 47 | console.log(`listening on port: ${process.env.PORT}`); 48 | }); 49 | -------------------------------------------------------------------------------- /example-servers/commonjs/src/websocket/utils.js: -------------------------------------------------------------------------------- 1 | const Y = require('yjs'); 2 | const syncProtocol = require('y-protocols/dist/sync.cjs'); 3 | const awarenessProtocol = require('y-protocols/dist/awareness.cjs'); 4 | 5 | const encoding = require('lib0/dist/encoding.cjs'); 6 | const decoding = require('lib0/dist/decoding.cjs'); 7 | const map = require('lib0/dist/map.cjs'); 8 | 9 | const wsReadyStateConnecting = 0; 10 | const wsReadyStateOpen = 1; 11 | const wsReadyStateClosing = 2; // eslint-disable-line 12 | const wsReadyStateClosed = 3; // eslint-disable-line 13 | 14 | // disable gc when using snapshots! 15 | const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'; 16 | 17 | let persistence = null; 18 | 19 | const setPersistence = (persistence_) => { 20 | persistence = persistence_; 21 | }; 22 | 23 | const getPersistence = () => persistence; 24 | 25 | // exporting docs so that others can use it 26 | const docs = new Map(); 27 | 28 | const messageSync = 0; 29 | const messageAwareness = 1; 30 | // const messageAuth = 2 31 | 32 | const updateHandler = (update, origin, doc) => { 33 | const encoder = encoding.createEncoder(); 34 | encoding.writeVarUint(encoder, messageSync); 35 | syncProtocol.writeUpdate(encoder, update); 36 | const message = encoding.toUint8Array(encoder); 37 | doc.conns.forEach((_, conn) => send(doc, conn, message)); 38 | }; 39 | 40 | class WSSharedDoc extends Y.Doc { 41 | constructor(name) { 42 | super({ gc: gcEnabled }); 43 | this.name = name; 44 | this.conns = new Map(); 45 | this.awareness = new awarenessProtocol.Awareness(this); 46 | this.awareness.setLocalState(null); 47 | 48 | const awarenessChangeHandler = ({ added, updated, removed }, conn) => { 49 | const changedClients = added.concat(updated, removed); 50 | if (conn !== null) { 51 | const connControlledIDs = this.conns.get(conn); 52 | if (connControlledIDs !== undefined) { 53 | added.forEach((clientID) => { 54 | connControlledIDs.add(clientID); 55 | }); 56 | removed.forEach((clientID) => { 57 | connControlledIDs.delete(clientID); 58 | }); 59 | } 60 | } 61 | // broadcast awareness update 62 | const encoder = encoding.createEncoder(); 63 | encoding.writeVarUint(encoder, messageAwareness); 64 | encoding.writeVarUint8Array( 65 | encoder, 66 | awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients), 67 | ); 68 | const buff = encoding.toUint8Array(encoder); 69 | this.conns.forEach((_, c) => { 70 | send(this, c, buff); 71 | }); 72 | }; 73 | this.awareness.on('update', awarenessChangeHandler); 74 | this.on('update', updateHandler); 75 | } 76 | } 77 | 78 | const getYDoc = (docname, gc = true) => 79 | map.setIfUndefined(docs, docname, () => { 80 | const doc = new WSSharedDoc(docname); 81 | doc.gc = gc; 82 | if (persistence !== null) { 83 | persistence.bindState(docname, doc); 84 | } 85 | docs.set(docname, doc); 86 | return doc; 87 | }); 88 | 89 | const messageListener = (conn, doc, message) => { 90 | try { 91 | const encoder = encoding.createEncoder(); 92 | const decoder = decoding.createDecoder(message); 93 | const messageType = decoding.readVarUint(decoder); 94 | switch (messageType) { 95 | case messageSync: 96 | encoding.writeVarUint(encoder, messageSync); 97 | syncProtocol.readSyncMessage(decoder, encoder, doc, conn); 98 | 99 | if (encoding.length(encoder) > 1) { 100 | send(doc, conn, encoding.toUint8Array(encoder)); 101 | } 102 | break; 103 | case messageAwareness: { 104 | awarenessProtocol.applyAwarenessUpdate( 105 | doc.awareness, 106 | decoding.readVarUint8Array(decoder), 107 | conn, 108 | ); 109 | break; 110 | } 111 | } 112 | } catch (err) { 113 | console.error(err); 114 | doc.emit('error', [err]); 115 | } 116 | }; 117 | 118 | const closeConn = (doc, conn) => { 119 | if (doc.conns.has(conn)) { 120 | const controlledIds = doc.conns.get(conn); 121 | doc.conns.delete(conn); 122 | if (controlledIds) { 123 | awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null); 124 | } 125 | if (doc.conns.size === 0 && persistence !== null) { 126 | persistence.writeState(doc.name, doc).then(() => { 127 | doc.destroy(); 128 | }); 129 | docs.delete(doc.name); 130 | } 131 | } 132 | conn.close(); 133 | }; 134 | 135 | const send = (doc, conn, m) => { 136 | if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) { 137 | closeConn(doc, conn); 138 | } 139 | try { 140 | conn.send(m, (err) => { 141 | err != null && closeConn(doc, conn); 142 | }); 143 | } catch (e) { 144 | closeConn(doc, conn); 145 | } 146 | }; 147 | 148 | const pingTimeout = 30000; 149 | 150 | const setupWSConnection = ( 151 | conn, 152 | req, 153 | { docName = req.url.slice(1).split('?')[0], gc = true } = {}, 154 | ) => { 155 | conn.binaryType = 'arraybuffer'; 156 | const doc = getYDoc(docName, gc); 157 | doc.conns.set(conn, new Set()); 158 | conn.on('message', (message) => messageListener(conn, doc, new Uint8Array(message))); 159 | 160 | let pongReceived = true; 161 | const pingInterval = setInterval(() => { 162 | if (!pongReceived) { 163 | if (doc.conns.has(conn)) { 164 | closeConn(doc, conn); 165 | } 166 | clearInterval(pingInterval); 167 | } else if (doc.conns.has(conn)) { 168 | pongReceived = false; 169 | try { 170 | conn.ping(); 171 | } catch (e) { 172 | closeConn(doc, conn); 173 | clearInterval(pingInterval); 174 | } 175 | } 176 | }, pingTimeout); 177 | conn.on('close', () => { 178 | closeConn(doc, conn); 179 | clearInterval(pingInterval); 180 | }); 181 | conn.on('pong', () => { 182 | pongReceived = true; 183 | }); 184 | 185 | { 186 | const encoder = encoding.createEncoder(); 187 | encoding.writeVarUint(encoder, messageSync); 188 | syncProtocol.writeSyncStep1(encoder, doc); 189 | send(doc, conn, encoding.toUint8Array(encoder)); 190 | const awarenessStates = doc.awareness.getStates(); 191 | if (awarenessStates.size > 0) { 192 | const encoder = encoding.createEncoder(); 193 | encoding.writeVarUint(encoder, messageAwareness); 194 | encoding.writeVarUint8Array( 195 | encoder, 196 | awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())), 197 | ); 198 | send(doc, conn, encoding.toUint8Array(encoder)); 199 | } 200 | } 201 | }; 202 | 203 | module.exports = { 204 | setPersistence, 205 | getPersistence, 206 | docs, 207 | getYDoc, 208 | setupWSConnection, 209 | }; 210 | -------------------------------------------------------------------------------- /example-servers/esm/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /example-servers/esm/EXAMPLE.env: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | MONGO_URL="mongodb://127.0.0.1:27017/todos" 3 | GC="true" -------------------------------------------------------------------------------- /example-servers/esm/README.md: -------------------------------------------------------------------------------- 1 | # Y-Websocket & Y-Mongodb-Provider - ESM 2 | 3 | This is a simple Node server that runs [y-websocket](https://github.com/yjs/y-websocket/) with [persistence](https://github.com/MaxNoetzold/y-mongodb-provider) for [Mongodb](https://www.mongodb.com/de-de). It is written in TypeScript and requires Node v20. 4 | 5 | This server is a simplified version of the [official example for a y-websocket server](https://github.com/yjs/y-websocket/tree/master/bin). 6 | 7 | ## How to run? 8 | 9 | First, install the dependencies with `npm install`. 10 | 11 | Next, copy the `EXAMPLE.env` file, rename it to `.env`, and edit the entries as needed. 12 | 13 | To compile and run the server code, use `npm run build` and `npm run start`, or simply use `npm run dev`. 14 | 15 | ## Random Notes 16 | 17 | Regarding my nodemon script: I initially wanted to use `ts-node`, but it [does not work with Node v20 anymore](https://github.com/TypeStrong/ts-node/issues/1997). As a result, I reverted to a simpler variant using `tsc` for building. 18 | -------------------------------------------------------------------------------- /example-servers/esm/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-todo-backend-esm", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "y-todo-backend-esm", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "dotenv": "^16.5.0", 13 | "ws": "^8.18.2", 14 | "y-mongodb-provider": "^0.2.0", 15 | "y-websocket": "^3.0.0", 16 | "yjs": "^13.6.27" 17 | }, 18 | "devDependencies": { 19 | "@types/lodash": "^4.17.17", 20 | "@types/node": "^22.15.24", 21 | "@types/ws": "^8.18.1", 22 | "nodemon": "^3.1.10", 23 | "typescript": "^5.8.3" 24 | } 25 | }, 26 | "node_modules/@mongodb-js/saslprep": { 27 | "version": "1.2.2", 28 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", 29 | "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", 30 | "license": "MIT", 31 | "dependencies": { 32 | "sparse-bitfield": "^3.0.3" 33 | } 34 | }, 35 | "node_modules/@types/lodash": { 36 | "version": "4.17.17", 37 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", 38 | "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", 39 | "dev": true, 40 | "license": "MIT" 41 | }, 42 | "node_modules/@types/node": { 43 | "version": "22.15.24", 44 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz", 45 | "integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==", 46 | "dev": true, 47 | "license": "MIT", 48 | "dependencies": { 49 | "undici-types": "~6.21.0" 50 | } 51 | }, 52 | "node_modules/@types/webidl-conversions": { 53 | "version": "7.0.3", 54 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", 55 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", 56 | "license": "MIT" 57 | }, 58 | "node_modules/@types/whatwg-url": { 59 | "version": "11.0.5", 60 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", 61 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", 62 | "license": "MIT", 63 | "dependencies": { 64 | "@types/webidl-conversions": "*" 65 | } 66 | }, 67 | "node_modules/@types/ws": { 68 | "version": "8.18.1", 69 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", 70 | "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", 71 | "dev": true, 72 | "license": "MIT", 73 | "dependencies": { 74 | "@types/node": "*" 75 | } 76 | }, 77 | "node_modules/abbrev": { 78 | "version": "1.1.1", 79 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 80 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", 81 | "dev": true 82 | }, 83 | "node_modules/anymatch": { 84 | "version": "3.1.3", 85 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 86 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 87 | "dev": true, 88 | "dependencies": { 89 | "normalize-path": "^3.0.0", 90 | "picomatch": "^2.0.4" 91 | }, 92 | "engines": { 93 | "node": ">= 8" 94 | } 95 | }, 96 | "node_modules/balanced-match": { 97 | "version": "1.0.2", 98 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 99 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 100 | "dev": true 101 | }, 102 | "node_modules/binary-extensions": { 103 | "version": "2.2.0", 104 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 105 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 106 | "dev": true, 107 | "engines": { 108 | "node": ">=8" 109 | } 110 | }, 111 | "node_modules/brace-expansion": { 112 | "version": "1.1.11", 113 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 114 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 115 | "dev": true, 116 | "dependencies": { 117 | "balanced-match": "^1.0.0", 118 | "concat-map": "0.0.1" 119 | } 120 | }, 121 | "node_modules/braces": { 122 | "version": "3.0.2", 123 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 124 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 125 | "dev": true, 126 | "dependencies": { 127 | "fill-range": "^7.0.1" 128 | }, 129 | "engines": { 130 | "node": ">=8" 131 | } 132 | }, 133 | "node_modules/bson": { 134 | "version": "6.10.3", 135 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", 136 | "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", 137 | "license": "Apache-2.0", 138 | "engines": { 139 | "node": ">=16.20.1" 140 | } 141 | }, 142 | "node_modules/chokidar": { 143 | "version": "3.5.3", 144 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 145 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 146 | "dev": true, 147 | "funding": [ 148 | { 149 | "type": "individual", 150 | "url": "https://paulmillr.com/funding/" 151 | } 152 | ], 153 | "dependencies": { 154 | "anymatch": "~3.1.2", 155 | "braces": "~3.0.2", 156 | "glob-parent": "~5.1.2", 157 | "is-binary-path": "~2.1.0", 158 | "is-glob": "~4.0.1", 159 | "normalize-path": "~3.0.0", 160 | "readdirp": "~3.6.0" 161 | }, 162 | "engines": { 163 | "node": ">= 8.10.0" 164 | }, 165 | "optionalDependencies": { 166 | "fsevents": "~2.3.2" 167 | } 168 | }, 169 | "node_modules/concat-map": { 170 | "version": "0.0.1", 171 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 172 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 173 | "dev": true 174 | }, 175 | "node_modules/debug": { 176 | "version": "4.3.4", 177 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 178 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 179 | "dev": true, 180 | "dependencies": { 181 | "ms": "2.1.2" 182 | }, 183 | "engines": { 184 | "node": ">=6.0" 185 | }, 186 | "peerDependenciesMeta": { 187 | "supports-color": { 188 | "optional": true 189 | } 190 | } 191 | }, 192 | "node_modules/dotenv": { 193 | "version": "16.5.0", 194 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", 195 | "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", 196 | "license": "BSD-2-Clause", 197 | "engines": { 198 | "node": ">=12" 199 | }, 200 | "funding": { 201 | "url": "https://dotenvx.com" 202 | } 203 | }, 204 | "node_modules/fill-range": { 205 | "version": "7.0.1", 206 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 207 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 208 | "dev": true, 209 | "dependencies": { 210 | "to-regex-range": "^5.0.1" 211 | }, 212 | "engines": { 213 | "node": ">=8" 214 | } 215 | }, 216 | "node_modules/fsevents": { 217 | "version": "2.3.3", 218 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 219 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 220 | "dev": true, 221 | "hasInstallScript": true, 222 | "optional": true, 223 | "os": [ 224 | "darwin" 225 | ], 226 | "engines": { 227 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 228 | } 229 | }, 230 | "node_modules/glob-parent": { 231 | "version": "5.1.2", 232 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 233 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 234 | "dev": true, 235 | "dependencies": { 236 | "is-glob": "^4.0.1" 237 | }, 238 | "engines": { 239 | "node": ">= 6" 240 | } 241 | }, 242 | "node_modules/has-flag": { 243 | "version": "3.0.0", 244 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 245 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 246 | "dev": true, 247 | "engines": { 248 | "node": ">=4" 249 | } 250 | }, 251 | "node_modules/ignore-by-default": { 252 | "version": "1.0.1", 253 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 254 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 255 | "dev": true 256 | }, 257 | "node_modules/is-binary-path": { 258 | "version": "2.1.0", 259 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 260 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 261 | "dev": true, 262 | "dependencies": { 263 | "binary-extensions": "^2.0.0" 264 | }, 265 | "engines": { 266 | "node": ">=8" 267 | } 268 | }, 269 | "node_modules/is-extglob": { 270 | "version": "2.1.1", 271 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 272 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 273 | "dev": true, 274 | "engines": { 275 | "node": ">=0.10.0" 276 | } 277 | }, 278 | "node_modules/is-glob": { 279 | "version": "4.0.3", 280 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 281 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 282 | "dev": true, 283 | "dependencies": { 284 | "is-extglob": "^2.1.1" 285 | }, 286 | "engines": { 287 | "node": ">=0.10.0" 288 | } 289 | }, 290 | "node_modules/is-number": { 291 | "version": "7.0.0", 292 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 293 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 294 | "dev": true, 295 | "engines": { 296 | "node": ">=0.12.0" 297 | } 298 | }, 299 | "node_modules/isomorphic.js": { 300 | "version": "0.2.5", 301 | "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", 302 | "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", 303 | "funding": { 304 | "type": "GitHub Sponsors ❤", 305 | "url": "https://github.com/sponsors/dmonad" 306 | } 307 | }, 308 | "node_modules/lib0": { 309 | "version": "0.2.108", 310 | "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.108.tgz", 311 | "integrity": "sha512-+3eK/B0SqYoZiQu9fNk4VEc6EX8cb0Li96tPGKgugzoGj/OdRdREtuTLvUW+mtinoB2mFiJjSqOJBIaMkAGhxQ==", 312 | "license": "MIT", 313 | "dependencies": { 314 | "isomorphic.js": "^0.2.4" 315 | }, 316 | "bin": { 317 | "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", 318 | "0gentesthtml": "bin/gentesthtml.js", 319 | "0serve": "bin/0serve.js" 320 | }, 321 | "engines": { 322 | "node": ">=16" 323 | }, 324 | "funding": { 325 | "type": "GitHub Sponsors ❤", 326 | "url": "https://github.com/sponsors/dmonad" 327 | } 328 | }, 329 | "node_modules/lru-cache": { 330 | "version": "6.0.0", 331 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 332 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 333 | "dev": true, 334 | "dependencies": { 335 | "yallist": "^4.0.0" 336 | }, 337 | "engines": { 338 | "node": ">=10" 339 | } 340 | }, 341 | "node_modules/memory-pager": { 342 | "version": "1.5.0", 343 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 344 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 345 | "license": "MIT" 346 | }, 347 | "node_modules/minimatch": { 348 | "version": "3.1.2", 349 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 350 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 351 | "dev": true, 352 | "dependencies": { 353 | "brace-expansion": "^1.1.7" 354 | }, 355 | "engines": { 356 | "node": "*" 357 | } 358 | }, 359 | "node_modules/mongodb": { 360 | "version": "6.16.0", 361 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", 362 | "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", 363 | "license": "Apache-2.0", 364 | "dependencies": { 365 | "@mongodb-js/saslprep": "^1.1.9", 366 | "bson": "^6.10.3", 367 | "mongodb-connection-string-url": "^3.0.0" 368 | }, 369 | "engines": { 370 | "node": ">=16.20.1" 371 | }, 372 | "peerDependencies": { 373 | "@aws-sdk/credential-providers": "^3.188.0", 374 | "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", 375 | "gcp-metadata": "^5.2.0", 376 | "kerberos": "^2.0.1", 377 | "mongodb-client-encryption": ">=6.0.0 <7", 378 | "snappy": "^7.2.2", 379 | "socks": "^2.7.1" 380 | }, 381 | "peerDependenciesMeta": { 382 | "@aws-sdk/credential-providers": { 383 | "optional": true 384 | }, 385 | "@mongodb-js/zstd": { 386 | "optional": true 387 | }, 388 | "gcp-metadata": { 389 | "optional": true 390 | }, 391 | "kerberos": { 392 | "optional": true 393 | }, 394 | "mongodb-client-encryption": { 395 | "optional": true 396 | }, 397 | "snappy": { 398 | "optional": true 399 | }, 400 | "socks": { 401 | "optional": true 402 | } 403 | } 404 | }, 405 | "node_modules/mongodb-connection-string-url": { 406 | "version": "3.0.2", 407 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", 408 | "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", 409 | "license": "Apache-2.0", 410 | "dependencies": { 411 | "@types/whatwg-url": "^11.0.2", 412 | "whatwg-url": "^14.1.0 || ^13.0.0" 413 | } 414 | }, 415 | "node_modules/ms": { 416 | "version": "2.1.2", 417 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 418 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 419 | "dev": true 420 | }, 421 | "node_modules/nodemon": { 422 | "version": "3.1.10", 423 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", 424 | "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", 425 | "dev": true, 426 | "license": "MIT", 427 | "dependencies": { 428 | "chokidar": "^3.5.2", 429 | "debug": "^4", 430 | "ignore-by-default": "^1.0.1", 431 | "minimatch": "^3.1.2", 432 | "pstree.remy": "^1.1.8", 433 | "semver": "^7.5.3", 434 | "simple-update-notifier": "^2.0.0", 435 | "supports-color": "^5.5.0", 436 | "touch": "^3.1.0", 437 | "undefsafe": "^2.0.5" 438 | }, 439 | "bin": { 440 | "nodemon": "bin/nodemon.js" 441 | }, 442 | "engines": { 443 | "node": ">=10" 444 | }, 445 | "funding": { 446 | "type": "opencollective", 447 | "url": "https://opencollective.com/nodemon" 448 | } 449 | }, 450 | "node_modules/nopt": { 451 | "version": "1.0.10", 452 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 453 | "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", 454 | "dev": true, 455 | "dependencies": { 456 | "abbrev": "1" 457 | }, 458 | "bin": { 459 | "nopt": "bin/nopt.js" 460 | }, 461 | "engines": { 462 | "node": "*" 463 | } 464 | }, 465 | "node_modules/normalize-path": { 466 | "version": "3.0.0", 467 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 468 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 469 | "dev": true, 470 | "engines": { 471 | "node": ">=0.10.0" 472 | } 473 | }, 474 | "node_modules/picomatch": { 475 | "version": "2.3.1", 476 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 477 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 478 | "dev": true, 479 | "engines": { 480 | "node": ">=8.6" 481 | }, 482 | "funding": { 483 | "url": "https://github.com/sponsors/jonschlinkert" 484 | } 485 | }, 486 | "node_modules/pstree.remy": { 487 | "version": "1.1.8", 488 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 489 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 490 | "dev": true 491 | }, 492 | "node_modules/punycode": { 493 | "version": "2.3.1", 494 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 495 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 496 | "license": "MIT", 497 | "engines": { 498 | "node": ">=6" 499 | } 500 | }, 501 | "node_modules/readdirp": { 502 | "version": "3.6.0", 503 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 504 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 505 | "dev": true, 506 | "dependencies": { 507 | "picomatch": "^2.2.1" 508 | }, 509 | "engines": { 510 | "node": ">=8.10.0" 511 | } 512 | }, 513 | "node_modules/semver": { 514 | "version": "7.5.4", 515 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 516 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 517 | "dev": true, 518 | "dependencies": { 519 | "lru-cache": "^6.0.0" 520 | }, 521 | "bin": { 522 | "semver": "bin/semver.js" 523 | }, 524 | "engines": { 525 | "node": ">=10" 526 | } 527 | }, 528 | "node_modules/simple-update-notifier": { 529 | "version": "2.0.0", 530 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 531 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 532 | "dev": true, 533 | "dependencies": { 534 | "semver": "^7.5.3" 535 | }, 536 | "engines": { 537 | "node": ">=10" 538 | } 539 | }, 540 | "node_modules/sparse-bitfield": { 541 | "version": "3.0.3", 542 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 543 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", 544 | "license": "MIT", 545 | "dependencies": { 546 | "memory-pager": "^1.0.2" 547 | } 548 | }, 549 | "node_modules/supports-color": { 550 | "version": "5.5.0", 551 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 552 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 553 | "dev": true, 554 | "dependencies": { 555 | "has-flag": "^3.0.0" 556 | }, 557 | "engines": { 558 | "node": ">=4" 559 | } 560 | }, 561 | "node_modules/to-regex-range": { 562 | "version": "5.0.1", 563 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 564 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 565 | "dev": true, 566 | "dependencies": { 567 | "is-number": "^7.0.0" 568 | }, 569 | "engines": { 570 | "node": ">=8.0" 571 | } 572 | }, 573 | "node_modules/touch": { 574 | "version": "3.1.0", 575 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", 576 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", 577 | "dev": true, 578 | "dependencies": { 579 | "nopt": "~1.0.10" 580 | }, 581 | "bin": { 582 | "nodetouch": "bin/nodetouch.js" 583 | } 584 | }, 585 | "node_modules/tr46": { 586 | "version": "5.1.1", 587 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", 588 | "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", 589 | "license": "MIT", 590 | "dependencies": { 591 | "punycode": "^2.3.1" 592 | }, 593 | "engines": { 594 | "node": ">=18" 595 | } 596 | }, 597 | "node_modules/typescript": { 598 | "version": "5.8.3", 599 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 600 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 601 | "dev": true, 602 | "license": "Apache-2.0", 603 | "bin": { 604 | "tsc": "bin/tsc", 605 | "tsserver": "bin/tsserver" 606 | }, 607 | "engines": { 608 | "node": ">=14.17" 609 | } 610 | }, 611 | "node_modules/undefsafe": { 612 | "version": "2.0.5", 613 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 614 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 615 | "dev": true 616 | }, 617 | "node_modules/undici-types": { 618 | "version": "6.21.0", 619 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 620 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 621 | "dev": true, 622 | "license": "MIT" 623 | }, 624 | "node_modules/webidl-conversions": { 625 | "version": "7.0.0", 626 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 627 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 628 | "license": "BSD-2-Clause", 629 | "engines": { 630 | "node": ">=12" 631 | } 632 | }, 633 | "node_modules/whatwg-url": { 634 | "version": "14.2.0", 635 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", 636 | "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", 637 | "license": "MIT", 638 | "dependencies": { 639 | "tr46": "^5.1.0", 640 | "webidl-conversions": "^7.0.0" 641 | }, 642 | "engines": { 643 | "node": ">=18" 644 | } 645 | }, 646 | "node_modules/ws": { 647 | "version": "8.18.2", 648 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", 649 | "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", 650 | "license": "MIT", 651 | "engines": { 652 | "node": ">=10.0.0" 653 | }, 654 | "peerDependencies": { 655 | "bufferutil": "^4.0.1", 656 | "utf-8-validate": ">=5.0.2" 657 | }, 658 | "peerDependenciesMeta": { 659 | "bufferutil": { 660 | "optional": true 661 | }, 662 | "utf-8-validate": { 663 | "optional": true 664 | } 665 | } 666 | }, 667 | "node_modules/y-mongodb-provider": { 668 | "version": "0.2.0", 669 | "resolved": "https://registry.npmjs.org/y-mongodb-provider/-/y-mongodb-provider-0.2.0.tgz", 670 | "integrity": "sha512-l2Qus1lix7TkxemLGzMJn8HYKiUD+vLJpZxYjtPvdBNbM7THhgVuyHvZYzknUDonvnBBjnpbDaZkKPWOMAsSAw==", 671 | "license": "MIT", 672 | "dependencies": { 673 | "lib0": "^0.2.94", 674 | "mongodb": "^6.7.0" 675 | }, 676 | "peerDependencies": { 677 | "yjs": "^13.6.15" 678 | } 679 | }, 680 | "node_modules/y-protocols": { 681 | "version": "1.0.6", 682 | "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", 683 | "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", 684 | "dependencies": { 685 | "lib0": "^0.2.85" 686 | }, 687 | "engines": { 688 | "node": ">=16.0.0", 689 | "npm": ">=8.0.0" 690 | }, 691 | "funding": { 692 | "type": "GitHub Sponsors ❤", 693 | "url": "https://github.com/sponsors/dmonad" 694 | }, 695 | "peerDependencies": { 696 | "yjs": "^13.0.0" 697 | } 698 | }, 699 | "node_modules/y-websocket": { 700 | "version": "3.0.0", 701 | "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-3.0.0.tgz", 702 | "integrity": "sha512-mUHy7AzkOZ834T/7piqtlA8Yk6AchqKqcrCXjKW8J1w2lPtRDjz8W5/CvXz9higKAHgKRKqpI3T33YkRFLkPtg==", 703 | "license": "MIT", 704 | "dependencies": { 705 | "lib0": "^0.2.102", 706 | "y-protocols": "^1.0.5" 707 | }, 708 | "engines": { 709 | "node": ">=16.0.0", 710 | "npm": ">=8.0.0" 711 | }, 712 | "funding": { 713 | "type": "GitHub Sponsors ❤", 714 | "url": "https://github.com/sponsors/dmonad" 715 | }, 716 | "peerDependencies": { 717 | "yjs": "^13.5.6" 718 | } 719 | }, 720 | "node_modules/yallist": { 721 | "version": "4.0.0", 722 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 723 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 724 | "dev": true 725 | }, 726 | "node_modules/yjs": { 727 | "version": "13.6.27", 728 | "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", 729 | "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", 730 | "license": "MIT", 731 | "dependencies": { 732 | "lib0": "^0.2.99" 733 | }, 734 | "engines": { 735 | "node": ">=16.0.0", 736 | "npm": ">=8.0.0" 737 | }, 738 | "funding": { 739 | "type": "GitHub Sponsors ❤", 740 | "url": "https://github.com/sponsors/dmonad" 741 | } 742 | } 743 | } 744 | } 745 | -------------------------------------------------------------------------------- /example-servers/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-todo-backend-esm", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "author": "max.noetzold@gmail.com", 6 | "license": "MIT", 7 | "main": "./src/server.ts", 8 | "scripts": { 9 | "build": "rm -rf dist && npx tsc", 10 | "start": "node dist/server.js", 11 | "dev": "nodemon --watch ./src -e ts --exec 'npm run build && npm run start'" 12 | }, 13 | "devDependencies": { 14 | "@types/lodash": "^4.17.17", 15 | "@types/node": "^22.15.24", 16 | "@types/ws": "^8.18.1", 17 | "nodemon": "^3.1.10", 18 | "typescript": "^5.8.3" 19 | }, 20 | "dependencies": { 21 | "dotenv": "^16.5.0", 22 | "ws": "^8.18.2", 23 | "y-mongodb-provider": "^0.2.0", 24 | "y-websocket": "^3.0.0", 25 | "yjs": "^13.6.27" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example-servers/esm/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import http from 'http'; 3 | import { WebSocketServer } from 'ws'; 4 | import * as Y from 'yjs'; 5 | import { MongodbPersistence } from 'y-mongodb-provider'; 6 | import { setPersistence, setupWSConnection } from './websocket/utils.js'; 7 | 8 | import { IWSSharedDoc } from './websocket/interfaces.js'; 9 | 10 | const server = http.createServer((request, response) => { 11 | response.writeHead(200, { 'Content-Type': 'text/plain' }); 12 | response.end('okay'); 13 | }); 14 | 15 | // y-websocket 16 | const wss = new WebSocketServer({ server }); 17 | wss.on('connection', setupWSConnection); 18 | 19 | /* 20 | * y-mongodb-provider 21 | */ 22 | if (!process.env.MONGO_URL) { 23 | throw new Error('Please define the MONGO_URL environment variable'); 24 | } 25 | const mdb = new MongodbPersistence(process.env.MONGO_URL, { 26 | flushSize: 100, 27 | multipleCollections: true, 28 | }); 29 | 30 | setPersistence({ 31 | bindState: async (docName: string, ydoc: IWSSharedDoc) => { 32 | const persistedYdoc = await mdb.getYDoc(docName); 33 | const newUpdates = Y.encodeStateAsUpdate(ydoc); 34 | mdb.storeUpdate(docName, newUpdates); 35 | Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc)); 36 | ydoc.on('update', async (update: Uint8Array) => { 37 | mdb.storeUpdate(docName, update); 38 | }); 39 | }, 40 | writeState: (docName: string, ydoc: IWSSharedDoc) => { 41 | return new Promise((resolve) => { 42 | resolve(true); 43 | }); 44 | }, 45 | }); 46 | 47 | server.listen(process.env.PORT, () => { 48 | console.log('listening on port:' + process.env.PORT); 49 | }); 50 | -------------------------------------------------------------------------------- /example-servers/esm/src/websocket/interfaces.ts: -------------------------------------------------------------------------------- 1 | import * as awarenessProtocol from 'y-protocols/awareness.js'; 2 | import * as Y from 'yjs'; 3 | 4 | export interface IWSSharedDoc extends Y.Doc { 5 | name: string; 6 | conns: Map>; 7 | awareness: awarenessProtocol.Awareness; 8 | } 9 | 10 | export interface IPersistence { 11 | bindState: (arg1: string, arg2: IWSSharedDoc) => void; 12 | writeState: (arg1: string, arg2: IWSSharedDoc) => Promise; 13 | provider?: any; 14 | } 15 | -------------------------------------------------------------------------------- /example-servers/esm/src/websocket/utils.ts: -------------------------------------------------------------------------------- 1 | import * as Y from 'yjs'; 2 | import * as syncProtocol from 'y-protocols/sync.js'; 3 | import * as awarenessProtocol from 'y-protocols/awareness.js'; 4 | 5 | import * as encoding from 'lib0/encoding'; 6 | import * as decoding from 'lib0/decoding'; 7 | import * as map from 'lib0/map'; 8 | 9 | import { IPersistence, IWSSharedDoc } from './interfaces.js'; 10 | 11 | const wsReadyStateConnecting = 0; 12 | const wsReadyStateOpen = 1; 13 | const wsReadyStateClosing = 2; // eslint-disable-line 14 | const wsReadyStateClosed = 3; // eslint-disable-line 15 | 16 | // disable gc when using snapshots! 17 | const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'; 18 | 19 | let persistence: IPersistence | null = null; 20 | 21 | export const setPersistence = (persistence_: IPersistence) => { 22 | persistence = persistence_; 23 | }; 24 | 25 | export const getPersistence = () => persistence; 26 | 27 | // exporting docs so that others can use it 28 | export const docs = new Map(); 29 | 30 | const messageSync = 0; 31 | const messageAwareness = 1; 32 | // const messageAuth = 2 33 | 34 | const updateHandler = (update: Uint8Array, origin: any, doc: Y.Doc) => { 35 | const sharedDoc = doc as IWSSharedDoc; 36 | 37 | const encoder = encoding.createEncoder(); 38 | encoding.writeVarUint(encoder, messageSync); 39 | syncProtocol.writeUpdate(encoder, update); 40 | const message = encoding.toUint8Array(encoder); 41 | sharedDoc.conns.forEach((_, conn) => send(sharedDoc, conn, message)); 42 | }; 43 | 44 | class WSSharedDoc extends Y.Doc implements IWSSharedDoc { 45 | name: string; 46 | conns: Map>; 47 | awareness: awarenessProtocol.Awareness; 48 | 49 | constructor(name: string) { 50 | super({ gc: gcEnabled }); 51 | this.name = name; 52 | this.conns = new Map(); 53 | this.awareness = new awarenessProtocol.Awareness(this); 54 | this.awareness.setLocalState(null); 55 | 56 | const awarenessChangeHandler = ( 57 | { 58 | added, 59 | updated, 60 | removed, 61 | }: { 62 | added: Array; 63 | updated: Array; 64 | removed: Array; 65 | }, 66 | conn: Object | null, 67 | ) => { 68 | const changedClients = added.concat(updated, removed); 69 | if (conn !== null) { 70 | const connControlledIDs = /** @type {Set} */ this.conns.get(conn); 71 | if (connControlledIDs !== undefined) { 72 | added.forEach((clientID) => { 73 | connControlledIDs.add(clientID); 74 | }); 75 | removed.forEach((clientID) => { 76 | connControlledIDs.delete(clientID); 77 | }); 78 | } 79 | } 80 | // broadcast awareness update 81 | const encoder = encoding.createEncoder(); 82 | encoding.writeVarUint(encoder, messageAwareness); 83 | encoding.writeVarUint8Array( 84 | encoder, 85 | awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients), 86 | ); 87 | const buff = encoding.toUint8Array(encoder); 88 | this.conns.forEach((_, c) => { 89 | send(this, c, buff); 90 | }); 91 | }; 92 | this.awareness.on('update', awarenessChangeHandler); 93 | this.on('update', updateHandler); 94 | } 95 | } 96 | 97 | /** 98 | * Gets a Y.Doc by name, whether in memory or on disk 99 | * 100 | * @param {string} docname - the name of the Y.Doc to find or create 101 | * @param {boolean} gc - whether to allow gc on the doc (applies only when created) 102 | * @return {WSSharedDoc} 103 | */ 104 | export const getYDoc = (docname: string, gc = true) => 105 | map.setIfUndefined(docs, docname, () => { 106 | const doc = new WSSharedDoc(docname); 107 | doc.gc = gc; 108 | if (persistence !== null) { 109 | persistence.bindState(docname, doc); 110 | } 111 | docs.set(docname, doc); 112 | return doc; 113 | }); 114 | 115 | const messageListener = (conn: any, doc: IWSSharedDoc, message: Uint8Array) => { 116 | try { 117 | const encoder = encoding.createEncoder(); 118 | const decoder = decoding.createDecoder(message); 119 | const messageType = decoding.readVarUint(decoder); 120 | switch (messageType) { 121 | case messageSync: 122 | encoding.writeVarUint(encoder, messageSync); 123 | syncProtocol.readSyncMessage(decoder, encoder, doc, conn); 124 | 125 | // If the `encoder` only contains the type of reply message and no 126 | // message, there is no need to send the message. When `encoder` only 127 | // contains the type of reply, its length is 1. 128 | if (encoding.length(encoder) > 1) { 129 | send(doc, conn, encoding.toUint8Array(encoder)); 130 | } 131 | break; 132 | case messageAwareness: { 133 | awarenessProtocol.applyAwarenessUpdate( 134 | doc.awareness, 135 | decoding.readVarUint8Array(decoder), 136 | conn, 137 | ); 138 | break; 139 | } 140 | } 141 | } catch (err) { 142 | console.error(err); 143 | } 144 | }; 145 | 146 | const closeConn = (doc: IWSSharedDoc, conn: any) => { 147 | if (doc.conns.has(conn)) { 148 | const controlledIds = doc.conns.get(conn); 149 | doc.conns.delete(conn); 150 | if (controlledIds) { 151 | awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null); 152 | } 153 | if (doc.conns.size === 0 && persistence !== null) { 154 | // if persisted, we store state and destroy ydocument 155 | persistence.writeState(doc.name, doc).then(() => { 156 | doc.destroy(); 157 | }); 158 | docs.delete(doc.name); 159 | } 160 | } 161 | conn.close(); 162 | }; 163 | 164 | const send = (doc: IWSSharedDoc, conn: any, m: Uint8Array) => { 165 | if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) { 166 | closeConn(doc, conn); 167 | } 168 | try { 169 | conn.send(m, (err: any) => { 170 | err != null && closeConn(doc, conn); 171 | }); 172 | } catch (e) { 173 | closeConn(doc, conn); 174 | } 175 | }; 176 | 177 | const pingTimeout = 30000; 178 | 179 | export const setupWSConnection = ( 180 | conn: any, 181 | req: any, 182 | { docName = req.url.slice(1).split('?')[0], gc = true } = {}, 183 | ) => { 184 | conn.binaryType = 'arraybuffer'; 185 | // get doc, initialize if it does not exist yet 186 | const doc = getYDoc(docName, gc); 187 | doc.conns.set(conn, new Set()); 188 | // listen and reply to events 189 | conn.on('message', (message: ArrayBuffer) => messageListener(conn, doc, new Uint8Array(message))); 190 | 191 | // Check if connection is still alive 192 | let pongReceived = true; 193 | const pingInterval = setInterval(() => { 194 | if (!pongReceived) { 195 | if (doc.conns.has(conn)) { 196 | closeConn(doc, conn); 197 | } 198 | clearInterval(pingInterval); 199 | } else if (doc.conns.has(conn)) { 200 | pongReceived = false; 201 | try { 202 | conn.ping(); 203 | } catch (e) { 204 | closeConn(doc, conn); 205 | clearInterval(pingInterval); 206 | } 207 | } 208 | }, pingTimeout); 209 | conn.on('close', () => { 210 | closeConn(doc, conn); 211 | clearInterval(pingInterval); 212 | }); 213 | conn.on('pong', () => { 214 | pongReceived = true; 215 | }); 216 | // put the following in a variables in a block so the interval handlers don't keep in in 217 | // scope 218 | { 219 | // send sync step 1 220 | const encoder = encoding.createEncoder(); 221 | encoding.writeVarUint(encoder, messageSync); 222 | syncProtocol.writeSyncStep1(encoder, doc); 223 | send(doc, conn, encoding.toUint8Array(encoder)); 224 | const awarenessStates = doc.awareness.getStates(); 225 | if (awarenessStates.size > 0) { 226 | const encoder = encoding.createEncoder(); 227 | encoding.writeVarUint(encoder, messageAwareness); 228 | encoding.writeVarUint8Array( 229 | encoder, 230 | awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())), 231 | ); 232 | send(doc, conn, encoding.toUint8Array(encoder)); 233 | } 234 | } 235 | }; 236 | -------------------------------------------------------------------------------- /example-servers/esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // "target": "es2016", 4 | // "module": "ES2020", 5 | "rootDir": "./src", 6 | // "moduleResolution": "Node", 7 | // "resolveJsonModule": true, 8 | "outDir": "./dist", 9 | // "esModuleInterop": true, 10 | // "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | // "skipLibCheck": true 13 | 14 | /* Copied from: https://github.com/jupyterlab/jupyterlab/blob/main/tsconfigbase.json */ 15 | "allowSyntheticDefaultImports": true, 16 | "esModuleInterop": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "noEmitOnError": true, 20 | "noImplicitAny": true, 21 | "preserveWatchOutput": true, 22 | "resolveJsonModule": true, 23 | "sourceMap": true, 24 | "strictNullChecks": true, 25 | "target": "ES2018", 26 | "types": [] 27 | }, 28 | "include": ["./src"] 29 | } 30 | -------------------------------------------------------------------------------- /example-servers/rollup/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /example-servers/rollup/EXAMPLE.env: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | MONGO_URL="mongodb://127.0.0.1:27017/todos" 3 | GC="true" -------------------------------------------------------------------------------- /example-servers/rollup/README.md: -------------------------------------------------------------------------------- 1 | # Y-Websocket & Y-Mongodb-Provider - Rollup 2 | 3 | This is a simple Node server that runs [y-websocket](https://github.com/yjs/y-websocket/) with [persistence](https://github.com/MaxNoetzold/y-mongodb-provider) for [Mongodb](https://www.mongodb.com/de-de). It is written in TypeScript and requires Node v20. 4 | 5 | This server is a simplified version of the [official example for a y-websocket server](https://github.com/yjs/y-websocket/tree/master/bin). 6 | 7 | ## How to run? 8 | 9 | First, install the dependencies with `npm install`. 10 | 11 | Next, copy the `EXAMPLE.env` file, rename it to `.env`, and edit the entries as needed. 12 | 13 | To compile and run the server code, use `npm run build` and `npm run start`, or simply use `npm run dev`. 14 | -------------------------------------------------------------------------------- /example-servers/rollup/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-todo-backend-rollup", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "y-todo-backend-rollup", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "dotenv": "^16.5.0", 13 | "ws": "^8.18.2", 14 | "y-mongodb-provider": "^0.2.0", 15 | "y-websocket": "^3.0.0", 16 | "yjs": "^13.6.27" 17 | }, 18 | "devDependencies": { 19 | "@rollup/plugin-typescript": "^12.1.2", 20 | "@types/lodash": "^4.17.17", 21 | "@types/node": "^22.15.24", 22 | "@types/ws": "^8.18.1", 23 | "nodemon": "^3.1.10", 24 | "rollup": "^4.41.1", 25 | "tslib": "^2.8.1", 26 | "typescript": "^5.8.3" 27 | } 28 | }, 29 | "node_modules/@mongodb-js/saslprep": { 30 | "version": "1.2.2", 31 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", 32 | "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", 33 | "license": "MIT", 34 | "dependencies": { 35 | "sparse-bitfield": "^3.0.3" 36 | } 37 | }, 38 | "node_modules/@rollup/plugin-typescript": { 39 | "version": "12.1.2", 40 | "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz", 41 | "integrity": "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==", 42 | "dev": true, 43 | "license": "MIT", 44 | "dependencies": { 45 | "@rollup/pluginutils": "^5.1.0", 46 | "resolve": "^1.22.1" 47 | }, 48 | "engines": { 49 | "node": ">=14.0.0" 50 | }, 51 | "peerDependencies": { 52 | "rollup": "^2.14.0||^3.0.0||^4.0.0", 53 | "tslib": "*", 54 | "typescript": ">=3.7.0" 55 | }, 56 | "peerDependenciesMeta": { 57 | "rollup": { 58 | "optional": true 59 | }, 60 | "tslib": { 61 | "optional": true 62 | } 63 | } 64 | }, 65 | "node_modules/@rollup/pluginutils": { 66 | "version": "5.1.0", 67 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", 68 | "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", 69 | "dev": true, 70 | "dependencies": { 71 | "@types/estree": "^1.0.0", 72 | "estree-walker": "^2.0.2", 73 | "picomatch": "^2.3.1" 74 | }, 75 | "engines": { 76 | "node": ">=14.0.0" 77 | }, 78 | "peerDependencies": { 79 | "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" 80 | }, 81 | "peerDependenciesMeta": { 82 | "rollup": { 83 | "optional": true 84 | } 85 | } 86 | }, 87 | "node_modules/@rollup/rollup-android-arm-eabi": { 88 | "version": "4.41.1", 89 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", 90 | "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", 91 | "cpu": [ 92 | "arm" 93 | ], 94 | "dev": true, 95 | "license": "MIT", 96 | "optional": true, 97 | "os": [ 98 | "android" 99 | ] 100 | }, 101 | "node_modules/@rollup/rollup-android-arm64": { 102 | "version": "4.41.1", 103 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", 104 | "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", 105 | "cpu": [ 106 | "arm64" 107 | ], 108 | "dev": true, 109 | "license": "MIT", 110 | "optional": true, 111 | "os": [ 112 | "android" 113 | ] 114 | }, 115 | "node_modules/@rollup/rollup-darwin-arm64": { 116 | "version": "4.41.1", 117 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", 118 | "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", 119 | "cpu": [ 120 | "arm64" 121 | ], 122 | "dev": true, 123 | "license": "MIT", 124 | "optional": true, 125 | "os": [ 126 | "darwin" 127 | ] 128 | }, 129 | "node_modules/@rollup/rollup-darwin-x64": { 130 | "version": "4.41.1", 131 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", 132 | "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", 133 | "cpu": [ 134 | "x64" 135 | ], 136 | "dev": true, 137 | "license": "MIT", 138 | "optional": true, 139 | "os": [ 140 | "darwin" 141 | ] 142 | }, 143 | "node_modules/@rollup/rollup-freebsd-arm64": { 144 | "version": "4.41.1", 145 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", 146 | "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", 147 | "cpu": [ 148 | "arm64" 149 | ], 150 | "dev": true, 151 | "license": "MIT", 152 | "optional": true, 153 | "os": [ 154 | "freebsd" 155 | ] 156 | }, 157 | "node_modules/@rollup/rollup-freebsd-x64": { 158 | "version": "4.41.1", 159 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", 160 | "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", 161 | "cpu": [ 162 | "x64" 163 | ], 164 | "dev": true, 165 | "license": "MIT", 166 | "optional": true, 167 | "os": [ 168 | "freebsd" 169 | ] 170 | }, 171 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 172 | "version": "4.41.1", 173 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", 174 | "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", 175 | "cpu": [ 176 | "arm" 177 | ], 178 | "dev": true, 179 | "license": "MIT", 180 | "optional": true, 181 | "os": [ 182 | "linux" 183 | ] 184 | }, 185 | "node_modules/@rollup/rollup-linux-arm-musleabihf": { 186 | "version": "4.41.1", 187 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", 188 | "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", 189 | "cpu": [ 190 | "arm" 191 | ], 192 | "dev": true, 193 | "license": "MIT", 194 | "optional": true, 195 | "os": [ 196 | "linux" 197 | ] 198 | }, 199 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 200 | "version": "4.41.1", 201 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", 202 | "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", 203 | "cpu": [ 204 | "arm64" 205 | ], 206 | "dev": true, 207 | "license": "MIT", 208 | "optional": true, 209 | "os": [ 210 | "linux" 211 | ] 212 | }, 213 | "node_modules/@rollup/rollup-linux-arm64-musl": { 214 | "version": "4.41.1", 215 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", 216 | "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", 217 | "cpu": [ 218 | "arm64" 219 | ], 220 | "dev": true, 221 | "license": "MIT", 222 | "optional": true, 223 | "os": [ 224 | "linux" 225 | ] 226 | }, 227 | "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 228 | "version": "4.41.1", 229 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", 230 | "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", 231 | "cpu": [ 232 | "loong64" 233 | ], 234 | "dev": true, 235 | "license": "MIT", 236 | "optional": true, 237 | "os": [ 238 | "linux" 239 | ] 240 | }, 241 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 242 | "version": "4.41.1", 243 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", 244 | "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", 245 | "cpu": [ 246 | "ppc64" 247 | ], 248 | "dev": true, 249 | "license": "MIT", 250 | "optional": true, 251 | "os": [ 252 | "linux" 253 | ] 254 | }, 255 | "node_modules/@rollup/rollup-linux-riscv64-gnu": { 256 | "version": "4.41.1", 257 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", 258 | "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", 259 | "cpu": [ 260 | "riscv64" 261 | ], 262 | "dev": true, 263 | "license": "MIT", 264 | "optional": true, 265 | "os": [ 266 | "linux" 267 | ] 268 | }, 269 | "node_modules/@rollup/rollup-linux-riscv64-musl": { 270 | "version": "4.41.1", 271 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", 272 | "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", 273 | "cpu": [ 274 | "riscv64" 275 | ], 276 | "dev": true, 277 | "license": "MIT", 278 | "optional": true, 279 | "os": [ 280 | "linux" 281 | ] 282 | }, 283 | "node_modules/@rollup/rollup-linux-s390x-gnu": { 284 | "version": "4.41.1", 285 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", 286 | "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", 287 | "cpu": [ 288 | "s390x" 289 | ], 290 | "dev": true, 291 | "license": "MIT", 292 | "optional": true, 293 | "os": [ 294 | "linux" 295 | ] 296 | }, 297 | "node_modules/@rollup/rollup-linux-x64-gnu": { 298 | "version": "4.41.1", 299 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", 300 | "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", 301 | "cpu": [ 302 | "x64" 303 | ], 304 | "dev": true, 305 | "license": "MIT", 306 | "optional": true, 307 | "os": [ 308 | "linux" 309 | ] 310 | }, 311 | "node_modules/@rollup/rollup-linux-x64-musl": { 312 | "version": "4.41.1", 313 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", 314 | "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", 315 | "cpu": [ 316 | "x64" 317 | ], 318 | "dev": true, 319 | "license": "MIT", 320 | "optional": true, 321 | "os": [ 322 | "linux" 323 | ] 324 | }, 325 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 326 | "version": "4.41.1", 327 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", 328 | "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", 329 | "cpu": [ 330 | "arm64" 331 | ], 332 | "dev": true, 333 | "license": "MIT", 334 | "optional": true, 335 | "os": [ 336 | "win32" 337 | ] 338 | }, 339 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 340 | "version": "4.41.1", 341 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", 342 | "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", 343 | "cpu": [ 344 | "ia32" 345 | ], 346 | "dev": true, 347 | "license": "MIT", 348 | "optional": true, 349 | "os": [ 350 | "win32" 351 | ] 352 | }, 353 | "node_modules/@rollup/rollup-win32-x64-msvc": { 354 | "version": "4.41.1", 355 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", 356 | "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", 357 | "cpu": [ 358 | "x64" 359 | ], 360 | "dev": true, 361 | "license": "MIT", 362 | "optional": true, 363 | "os": [ 364 | "win32" 365 | ] 366 | }, 367 | "node_modules/@types/estree": { 368 | "version": "1.0.7", 369 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 370 | "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 371 | "dev": true, 372 | "license": "MIT" 373 | }, 374 | "node_modules/@types/lodash": { 375 | "version": "4.17.17", 376 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", 377 | "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", 378 | "dev": true, 379 | "license": "MIT" 380 | }, 381 | "node_modules/@types/node": { 382 | "version": "22.15.24", 383 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz", 384 | "integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==", 385 | "dev": true, 386 | "license": "MIT", 387 | "dependencies": { 388 | "undici-types": "~6.21.0" 389 | } 390 | }, 391 | "node_modules/@types/webidl-conversions": { 392 | "version": "7.0.3", 393 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", 394 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", 395 | "license": "MIT" 396 | }, 397 | "node_modules/@types/whatwg-url": { 398 | "version": "11.0.5", 399 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", 400 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", 401 | "license": "MIT", 402 | "dependencies": { 403 | "@types/webidl-conversions": "*" 404 | } 405 | }, 406 | "node_modules/@types/ws": { 407 | "version": "8.18.1", 408 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", 409 | "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", 410 | "dev": true, 411 | "license": "MIT", 412 | "dependencies": { 413 | "@types/node": "*" 414 | } 415 | }, 416 | "node_modules/abbrev": { 417 | "version": "1.1.1", 418 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 419 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", 420 | "dev": true 421 | }, 422 | "node_modules/anymatch": { 423 | "version": "3.1.3", 424 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 425 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 426 | "dev": true, 427 | "dependencies": { 428 | "normalize-path": "^3.0.0", 429 | "picomatch": "^2.0.4" 430 | }, 431 | "engines": { 432 | "node": ">= 8" 433 | } 434 | }, 435 | "node_modules/balanced-match": { 436 | "version": "1.0.2", 437 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 438 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 439 | "dev": true 440 | }, 441 | "node_modules/binary-extensions": { 442 | "version": "2.2.0", 443 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 444 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 445 | "dev": true, 446 | "engines": { 447 | "node": ">=8" 448 | } 449 | }, 450 | "node_modules/brace-expansion": { 451 | "version": "1.1.11", 452 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 453 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 454 | "dev": true, 455 | "dependencies": { 456 | "balanced-match": "^1.0.0", 457 | "concat-map": "0.0.1" 458 | } 459 | }, 460 | "node_modules/braces": { 461 | "version": "3.0.2", 462 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 463 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 464 | "dev": true, 465 | "dependencies": { 466 | "fill-range": "^7.0.1" 467 | }, 468 | "engines": { 469 | "node": ">=8" 470 | } 471 | }, 472 | "node_modules/bson": { 473 | "version": "6.10.3", 474 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", 475 | "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", 476 | "license": "Apache-2.0", 477 | "engines": { 478 | "node": ">=16.20.1" 479 | } 480 | }, 481 | "node_modules/chokidar": { 482 | "version": "3.5.3", 483 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 484 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 485 | "dev": true, 486 | "funding": [ 487 | { 488 | "type": "individual", 489 | "url": "https://paulmillr.com/funding/" 490 | } 491 | ], 492 | "dependencies": { 493 | "anymatch": "~3.1.2", 494 | "braces": "~3.0.2", 495 | "glob-parent": "~5.1.2", 496 | "is-binary-path": "~2.1.0", 497 | "is-glob": "~4.0.1", 498 | "normalize-path": "~3.0.0", 499 | "readdirp": "~3.6.0" 500 | }, 501 | "engines": { 502 | "node": ">= 8.10.0" 503 | }, 504 | "optionalDependencies": { 505 | "fsevents": "~2.3.2" 506 | } 507 | }, 508 | "node_modules/concat-map": { 509 | "version": "0.0.1", 510 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 511 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 512 | "dev": true 513 | }, 514 | "node_modules/debug": { 515 | "version": "4.3.4", 516 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 517 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 518 | "dev": true, 519 | "dependencies": { 520 | "ms": "2.1.2" 521 | }, 522 | "engines": { 523 | "node": ">=6.0" 524 | }, 525 | "peerDependenciesMeta": { 526 | "supports-color": { 527 | "optional": true 528 | } 529 | } 530 | }, 531 | "node_modules/dotenv": { 532 | "version": "16.5.0", 533 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", 534 | "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", 535 | "license": "BSD-2-Clause", 536 | "engines": { 537 | "node": ">=12" 538 | }, 539 | "funding": { 540 | "url": "https://dotenvx.com" 541 | } 542 | }, 543 | "node_modules/estree-walker": { 544 | "version": "2.0.2", 545 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 546 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 547 | "dev": true 548 | }, 549 | "node_modules/fill-range": { 550 | "version": "7.0.1", 551 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 552 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 553 | "dev": true, 554 | "dependencies": { 555 | "to-regex-range": "^5.0.1" 556 | }, 557 | "engines": { 558 | "node": ">=8" 559 | } 560 | }, 561 | "node_modules/fsevents": { 562 | "version": "2.3.3", 563 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 564 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 565 | "dev": true, 566 | "hasInstallScript": true, 567 | "optional": true, 568 | "os": [ 569 | "darwin" 570 | ], 571 | "engines": { 572 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 573 | } 574 | }, 575 | "node_modules/function-bind": { 576 | "version": "1.1.2", 577 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 578 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 579 | "dev": true, 580 | "funding": { 581 | "url": "https://github.com/sponsors/ljharb" 582 | } 583 | }, 584 | "node_modules/glob-parent": { 585 | "version": "5.1.2", 586 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 587 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 588 | "dev": true, 589 | "dependencies": { 590 | "is-glob": "^4.0.1" 591 | }, 592 | "engines": { 593 | "node": ">= 6" 594 | } 595 | }, 596 | "node_modules/has-flag": { 597 | "version": "3.0.0", 598 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 599 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 600 | "dev": true, 601 | "engines": { 602 | "node": ">=4" 603 | } 604 | }, 605 | "node_modules/hasown": { 606 | "version": "2.0.0", 607 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", 608 | "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", 609 | "dev": true, 610 | "dependencies": { 611 | "function-bind": "^1.1.2" 612 | }, 613 | "engines": { 614 | "node": ">= 0.4" 615 | } 616 | }, 617 | "node_modules/ignore-by-default": { 618 | "version": "1.0.1", 619 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 620 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 621 | "dev": true 622 | }, 623 | "node_modules/is-binary-path": { 624 | "version": "2.1.0", 625 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 626 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 627 | "dev": true, 628 | "dependencies": { 629 | "binary-extensions": "^2.0.0" 630 | }, 631 | "engines": { 632 | "node": ">=8" 633 | } 634 | }, 635 | "node_modules/is-core-module": { 636 | "version": "2.13.1", 637 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", 638 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", 639 | "dev": true, 640 | "dependencies": { 641 | "hasown": "^2.0.0" 642 | }, 643 | "funding": { 644 | "url": "https://github.com/sponsors/ljharb" 645 | } 646 | }, 647 | "node_modules/is-extglob": { 648 | "version": "2.1.1", 649 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 650 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 651 | "dev": true, 652 | "engines": { 653 | "node": ">=0.10.0" 654 | } 655 | }, 656 | "node_modules/is-glob": { 657 | "version": "4.0.3", 658 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 659 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 660 | "dev": true, 661 | "dependencies": { 662 | "is-extglob": "^2.1.1" 663 | }, 664 | "engines": { 665 | "node": ">=0.10.0" 666 | } 667 | }, 668 | "node_modules/is-number": { 669 | "version": "7.0.0", 670 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 671 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 672 | "dev": true, 673 | "engines": { 674 | "node": ">=0.12.0" 675 | } 676 | }, 677 | "node_modules/isomorphic.js": { 678 | "version": "0.2.5", 679 | "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", 680 | "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", 681 | "funding": { 682 | "type": "GitHub Sponsors ❤", 683 | "url": "https://github.com/sponsors/dmonad" 684 | } 685 | }, 686 | "node_modules/lib0": { 687 | "version": "0.2.108", 688 | "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.108.tgz", 689 | "integrity": "sha512-+3eK/B0SqYoZiQu9fNk4VEc6EX8cb0Li96tPGKgugzoGj/OdRdREtuTLvUW+mtinoB2mFiJjSqOJBIaMkAGhxQ==", 690 | "license": "MIT", 691 | "dependencies": { 692 | "isomorphic.js": "^0.2.4" 693 | }, 694 | "bin": { 695 | "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", 696 | "0gentesthtml": "bin/gentesthtml.js", 697 | "0serve": "bin/0serve.js" 698 | }, 699 | "engines": { 700 | "node": ">=16" 701 | }, 702 | "funding": { 703 | "type": "GitHub Sponsors ❤", 704 | "url": "https://github.com/sponsors/dmonad" 705 | } 706 | }, 707 | "node_modules/lru-cache": { 708 | "version": "6.0.0", 709 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 710 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 711 | "dev": true, 712 | "dependencies": { 713 | "yallist": "^4.0.0" 714 | }, 715 | "engines": { 716 | "node": ">=10" 717 | } 718 | }, 719 | "node_modules/memory-pager": { 720 | "version": "1.5.0", 721 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 722 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 723 | "license": "MIT" 724 | }, 725 | "node_modules/minimatch": { 726 | "version": "3.1.2", 727 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 728 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 729 | "dev": true, 730 | "dependencies": { 731 | "brace-expansion": "^1.1.7" 732 | }, 733 | "engines": { 734 | "node": "*" 735 | } 736 | }, 737 | "node_modules/mongodb": { 738 | "version": "6.16.0", 739 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", 740 | "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", 741 | "license": "Apache-2.0", 742 | "dependencies": { 743 | "@mongodb-js/saslprep": "^1.1.9", 744 | "bson": "^6.10.3", 745 | "mongodb-connection-string-url": "^3.0.0" 746 | }, 747 | "engines": { 748 | "node": ">=16.20.1" 749 | }, 750 | "peerDependencies": { 751 | "@aws-sdk/credential-providers": "^3.188.0", 752 | "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", 753 | "gcp-metadata": "^5.2.0", 754 | "kerberos": "^2.0.1", 755 | "mongodb-client-encryption": ">=6.0.0 <7", 756 | "snappy": "^7.2.2", 757 | "socks": "^2.7.1" 758 | }, 759 | "peerDependenciesMeta": { 760 | "@aws-sdk/credential-providers": { 761 | "optional": true 762 | }, 763 | "@mongodb-js/zstd": { 764 | "optional": true 765 | }, 766 | "gcp-metadata": { 767 | "optional": true 768 | }, 769 | "kerberos": { 770 | "optional": true 771 | }, 772 | "mongodb-client-encryption": { 773 | "optional": true 774 | }, 775 | "snappy": { 776 | "optional": true 777 | }, 778 | "socks": { 779 | "optional": true 780 | } 781 | } 782 | }, 783 | "node_modules/mongodb-connection-string-url": { 784 | "version": "3.0.2", 785 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", 786 | "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", 787 | "license": "Apache-2.0", 788 | "dependencies": { 789 | "@types/whatwg-url": "^11.0.2", 790 | "whatwg-url": "^14.1.0 || ^13.0.0" 791 | } 792 | }, 793 | "node_modules/ms": { 794 | "version": "2.1.2", 795 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 796 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 797 | "dev": true 798 | }, 799 | "node_modules/nodemon": { 800 | "version": "3.1.10", 801 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", 802 | "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", 803 | "dev": true, 804 | "license": "MIT", 805 | "dependencies": { 806 | "chokidar": "^3.5.2", 807 | "debug": "^4", 808 | "ignore-by-default": "^1.0.1", 809 | "minimatch": "^3.1.2", 810 | "pstree.remy": "^1.1.8", 811 | "semver": "^7.5.3", 812 | "simple-update-notifier": "^2.0.0", 813 | "supports-color": "^5.5.0", 814 | "touch": "^3.1.0", 815 | "undefsafe": "^2.0.5" 816 | }, 817 | "bin": { 818 | "nodemon": "bin/nodemon.js" 819 | }, 820 | "engines": { 821 | "node": ">=10" 822 | }, 823 | "funding": { 824 | "type": "opencollective", 825 | "url": "https://opencollective.com/nodemon" 826 | } 827 | }, 828 | "node_modules/nopt": { 829 | "version": "1.0.10", 830 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 831 | "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", 832 | "dev": true, 833 | "dependencies": { 834 | "abbrev": "1" 835 | }, 836 | "bin": { 837 | "nopt": "bin/nopt.js" 838 | }, 839 | "engines": { 840 | "node": "*" 841 | } 842 | }, 843 | "node_modules/normalize-path": { 844 | "version": "3.0.0", 845 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 846 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 847 | "dev": true, 848 | "engines": { 849 | "node": ">=0.10.0" 850 | } 851 | }, 852 | "node_modules/path-parse": { 853 | "version": "1.0.7", 854 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 855 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 856 | "dev": true 857 | }, 858 | "node_modules/picomatch": { 859 | "version": "2.3.1", 860 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 861 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 862 | "dev": true, 863 | "engines": { 864 | "node": ">=8.6" 865 | }, 866 | "funding": { 867 | "url": "https://github.com/sponsors/jonschlinkert" 868 | } 869 | }, 870 | "node_modules/pstree.remy": { 871 | "version": "1.1.8", 872 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 873 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 874 | "dev": true 875 | }, 876 | "node_modules/punycode": { 877 | "version": "2.3.1", 878 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 879 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 880 | "license": "MIT", 881 | "engines": { 882 | "node": ">=6" 883 | } 884 | }, 885 | "node_modules/readdirp": { 886 | "version": "3.6.0", 887 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 888 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 889 | "dev": true, 890 | "dependencies": { 891 | "picomatch": "^2.2.1" 892 | }, 893 | "engines": { 894 | "node": ">=8.10.0" 895 | } 896 | }, 897 | "node_modules/resolve": { 898 | "version": "1.22.8", 899 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 900 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 901 | "dev": true, 902 | "dependencies": { 903 | "is-core-module": "^2.13.0", 904 | "path-parse": "^1.0.7", 905 | "supports-preserve-symlinks-flag": "^1.0.0" 906 | }, 907 | "bin": { 908 | "resolve": "bin/resolve" 909 | }, 910 | "funding": { 911 | "url": "https://github.com/sponsors/ljharb" 912 | } 913 | }, 914 | "node_modules/rollup": { 915 | "version": "4.41.1", 916 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", 917 | "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", 918 | "dev": true, 919 | "license": "MIT", 920 | "dependencies": { 921 | "@types/estree": "1.0.7" 922 | }, 923 | "bin": { 924 | "rollup": "dist/bin/rollup" 925 | }, 926 | "engines": { 927 | "node": ">=18.0.0", 928 | "npm": ">=8.0.0" 929 | }, 930 | "optionalDependencies": { 931 | "@rollup/rollup-android-arm-eabi": "4.41.1", 932 | "@rollup/rollup-android-arm64": "4.41.1", 933 | "@rollup/rollup-darwin-arm64": "4.41.1", 934 | "@rollup/rollup-darwin-x64": "4.41.1", 935 | "@rollup/rollup-freebsd-arm64": "4.41.1", 936 | "@rollup/rollup-freebsd-x64": "4.41.1", 937 | "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", 938 | "@rollup/rollup-linux-arm-musleabihf": "4.41.1", 939 | "@rollup/rollup-linux-arm64-gnu": "4.41.1", 940 | "@rollup/rollup-linux-arm64-musl": "4.41.1", 941 | "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", 942 | "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", 943 | "@rollup/rollup-linux-riscv64-gnu": "4.41.1", 944 | "@rollup/rollup-linux-riscv64-musl": "4.41.1", 945 | "@rollup/rollup-linux-s390x-gnu": "4.41.1", 946 | "@rollup/rollup-linux-x64-gnu": "4.41.1", 947 | "@rollup/rollup-linux-x64-musl": "4.41.1", 948 | "@rollup/rollup-win32-arm64-msvc": "4.41.1", 949 | "@rollup/rollup-win32-ia32-msvc": "4.41.1", 950 | "@rollup/rollup-win32-x64-msvc": "4.41.1", 951 | "fsevents": "~2.3.2" 952 | } 953 | }, 954 | "node_modules/semver": { 955 | "version": "7.5.4", 956 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 957 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 958 | "dev": true, 959 | "dependencies": { 960 | "lru-cache": "^6.0.0" 961 | }, 962 | "bin": { 963 | "semver": "bin/semver.js" 964 | }, 965 | "engines": { 966 | "node": ">=10" 967 | } 968 | }, 969 | "node_modules/simple-update-notifier": { 970 | "version": "2.0.0", 971 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 972 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 973 | "dev": true, 974 | "dependencies": { 975 | "semver": "^7.5.3" 976 | }, 977 | "engines": { 978 | "node": ">=10" 979 | } 980 | }, 981 | "node_modules/sparse-bitfield": { 982 | "version": "3.0.3", 983 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 984 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", 985 | "license": "MIT", 986 | "dependencies": { 987 | "memory-pager": "^1.0.2" 988 | } 989 | }, 990 | "node_modules/supports-color": { 991 | "version": "5.5.0", 992 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 993 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 994 | "dev": true, 995 | "dependencies": { 996 | "has-flag": "^3.0.0" 997 | }, 998 | "engines": { 999 | "node": ">=4" 1000 | } 1001 | }, 1002 | "node_modules/supports-preserve-symlinks-flag": { 1003 | "version": "1.0.0", 1004 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1005 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1006 | "dev": true, 1007 | "engines": { 1008 | "node": ">= 0.4" 1009 | }, 1010 | "funding": { 1011 | "url": "https://github.com/sponsors/ljharb" 1012 | } 1013 | }, 1014 | "node_modules/to-regex-range": { 1015 | "version": "5.0.1", 1016 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1017 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1018 | "dev": true, 1019 | "dependencies": { 1020 | "is-number": "^7.0.0" 1021 | }, 1022 | "engines": { 1023 | "node": ">=8.0" 1024 | } 1025 | }, 1026 | "node_modules/touch": { 1027 | "version": "3.1.0", 1028 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", 1029 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", 1030 | "dev": true, 1031 | "dependencies": { 1032 | "nopt": "~1.0.10" 1033 | }, 1034 | "bin": { 1035 | "nodetouch": "bin/nodetouch.js" 1036 | } 1037 | }, 1038 | "node_modules/tr46": { 1039 | "version": "5.1.1", 1040 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", 1041 | "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", 1042 | "license": "MIT", 1043 | "dependencies": { 1044 | "punycode": "^2.3.1" 1045 | }, 1046 | "engines": { 1047 | "node": ">=18" 1048 | } 1049 | }, 1050 | "node_modules/tslib": { 1051 | "version": "2.8.1", 1052 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 1053 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 1054 | "dev": true, 1055 | "license": "0BSD" 1056 | }, 1057 | "node_modules/typescript": { 1058 | "version": "5.8.3", 1059 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 1060 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 1061 | "dev": true, 1062 | "license": "Apache-2.0", 1063 | "bin": { 1064 | "tsc": "bin/tsc", 1065 | "tsserver": "bin/tsserver" 1066 | }, 1067 | "engines": { 1068 | "node": ">=14.17" 1069 | } 1070 | }, 1071 | "node_modules/undefsafe": { 1072 | "version": "2.0.5", 1073 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 1074 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 1075 | "dev": true 1076 | }, 1077 | "node_modules/undici-types": { 1078 | "version": "6.21.0", 1079 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 1080 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 1081 | "dev": true, 1082 | "license": "MIT" 1083 | }, 1084 | "node_modules/webidl-conversions": { 1085 | "version": "7.0.0", 1086 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 1087 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 1088 | "license": "BSD-2-Clause", 1089 | "engines": { 1090 | "node": ">=12" 1091 | } 1092 | }, 1093 | "node_modules/whatwg-url": { 1094 | "version": "14.2.0", 1095 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", 1096 | "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", 1097 | "license": "MIT", 1098 | "dependencies": { 1099 | "tr46": "^5.1.0", 1100 | "webidl-conversions": "^7.0.0" 1101 | }, 1102 | "engines": { 1103 | "node": ">=18" 1104 | } 1105 | }, 1106 | "node_modules/ws": { 1107 | "version": "8.18.2", 1108 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", 1109 | "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", 1110 | "license": "MIT", 1111 | "engines": { 1112 | "node": ">=10.0.0" 1113 | }, 1114 | "peerDependencies": { 1115 | "bufferutil": "^4.0.1", 1116 | "utf-8-validate": ">=5.0.2" 1117 | }, 1118 | "peerDependenciesMeta": { 1119 | "bufferutil": { 1120 | "optional": true 1121 | }, 1122 | "utf-8-validate": { 1123 | "optional": true 1124 | } 1125 | } 1126 | }, 1127 | "node_modules/y-mongodb-provider": { 1128 | "version": "0.2.0", 1129 | "resolved": "https://registry.npmjs.org/y-mongodb-provider/-/y-mongodb-provider-0.2.0.tgz", 1130 | "integrity": "sha512-l2Qus1lix7TkxemLGzMJn8HYKiUD+vLJpZxYjtPvdBNbM7THhgVuyHvZYzknUDonvnBBjnpbDaZkKPWOMAsSAw==", 1131 | "license": "MIT", 1132 | "dependencies": { 1133 | "lib0": "^0.2.94", 1134 | "mongodb": "^6.7.0" 1135 | }, 1136 | "peerDependencies": { 1137 | "yjs": "^13.6.15" 1138 | } 1139 | }, 1140 | "node_modules/y-protocols": { 1141 | "version": "1.0.6", 1142 | "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", 1143 | "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", 1144 | "dependencies": { 1145 | "lib0": "^0.2.85" 1146 | }, 1147 | "engines": { 1148 | "node": ">=16.0.0", 1149 | "npm": ">=8.0.0" 1150 | }, 1151 | "funding": { 1152 | "type": "GitHub Sponsors ❤", 1153 | "url": "https://github.com/sponsors/dmonad" 1154 | }, 1155 | "peerDependencies": { 1156 | "yjs": "^13.0.0" 1157 | } 1158 | }, 1159 | "node_modules/y-websocket": { 1160 | "version": "3.0.0", 1161 | "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-3.0.0.tgz", 1162 | "integrity": "sha512-mUHy7AzkOZ834T/7piqtlA8Yk6AchqKqcrCXjKW8J1w2lPtRDjz8W5/CvXz9higKAHgKRKqpI3T33YkRFLkPtg==", 1163 | "license": "MIT", 1164 | "dependencies": { 1165 | "lib0": "^0.2.102", 1166 | "y-protocols": "^1.0.5" 1167 | }, 1168 | "engines": { 1169 | "node": ">=16.0.0", 1170 | "npm": ">=8.0.0" 1171 | }, 1172 | "funding": { 1173 | "type": "GitHub Sponsors ❤", 1174 | "url": "https://github.com/sponsors/dmonad" 1175 | }, 1176 | "peerDependencies": { 1177 | "yjs": "^13.5.6" 1178 | } 1179 | }, 1180 | "node_modules/yallist": { 1181 | "version": "4.0.0", 1182 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 1183 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 1184 | "dev": true 1185 | }, 1186 | "node_modules/yjs": { 1187 | "version": "13.6.27", 1188 | "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", 1189 | "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", 1190 | "license": "MIT", 1191 | "dependencies": { 1192 | "lib0": "^0.2.99" 1193 | }, 1194 | "engines": { 1195 | "node": ">=16.0.0", 1196 | "npm": ">=8.0.0" 1197 | }, 1198 | "funding": { 1199 | "type": "GitHub Sponsors ❤", 1200 | "url": "https://github.com/sponsors/dmonad" 1201 | } 1202 | } 1203 | } 1204 | } 1205 | -------------------------------------------------------------------------------- /example-servers/rollup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-todo-backend-rollup", 3 | "version": "1.0.0", 4 | "author": "max.noetzold@gmail.com", 5 | "license": "MIT", 6 | "main": "./src/server.ts", 7 | "scripts": { 8 | "build": "rollup -c", 9 | "start": "node dist/server.js", 10 | "dev": "nodemon --watch ./src -e ts --exec 'npm run build && npm run start'" 11 | }, 12 | "devDependencies": { 13 | "@rollup/plugin-typescript": "^12.1.2", 14 | "@types/lodash": "^4.17.17", 15 | "@types/node": "^22.15.24", 16 | "@types/ws": "^8.18.1", 17 | "nodemon": "^3.1.10", 18 | "rollup": "^4.41.1", 19 | "tslib": "^2.8.1", 20 | "typescript": "^5.8.3" 21 | }, 22 | "dependencies": { 23 | "dotenv": "^16.5.0", 24 | "ws": "^8.18.2", 25 | "y-mongodb-provider": "^0.2.0", 26 | "y-websocket": "^3.0.0", 27 | "yjs": "^13.6.27" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example-servers/rollup/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | 3 | export default { 4 | input: 'src/server.mts', 5 | output: { 6 | dir: 'dist', 7 | format: 'cjs', 8 | sourcemap: true, 9 | paths: (path) => { 10 | if (/^lib0\//.test(path)) { 11 | // return `lib0/dist/${path.slice(5, -3)}.cjs 12 | return `lib0/dist/${path.slice(5)}.cjs`; 13 | } else if (/^y-protocols\//.test(path)) { 14 | return `y-protocols/dist${path.slice(11)}.cjs`; 15 | } 16 | return path; 17 | }, 18 | }, 19 | external: (id) => /^(lib0|yjs|y-protocols|dotenv\/config|http|ws|y-mongodb-provider)/.test(id), 20 | plugins: [typescript()], 21 | }; 22 | -------------------------------------------------------------------------------- /example-servers/rollup/src/server.mts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import http from 'http'; 3 | import { WebSocketServer } from 'ws'; 4 | import * as Y from 'yjs'; 5 | import { MongodbPersistence } from 'y-mongodb-provider'; 6 | import { setPersistence, setupWSConnection } from './websocket/utils.mjs'; 7 | import { IWSSharedDoc } from './websocket/interfaces.mjs'; 8 | 9 | const server = http.createServer((request, response) => { 10 | response.writeHead(200, { 'Content-Type': 'text/plain' }); 11 | response.end('okay'); 12 | }); 13 | 14 | // y-websocket 15 | const wss = new WebSocketServer({ server }); 16 | wss.on('connection', setupWSConnection); 17 | 18 | /* 19 | * y-mongodb-provider 20 | */ 21 | if (!process.env.MONGO_URL) { 22 | throw new Error('Please define the MONGO_URL environment variable'); 23 | } 24 | const mdb = new MongodbPersistence(process.env.MONGO_URL, { 25 | flushSize: 100, 26 | multipleCollections: true, 27 | }); 28 | 29 | setPersistence({ 30 | bindState: async (docName: string, ydoc: IWSSharedDoc) => { 31 | const persistedYdoc = await mdb.getYDoc(docName); 32 | const newUpdates = Y.encodeStateAsUpdate(ydoc); 33 | mdb.storeUpdate(docName, newUpdates); 34 | Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc)); 35 | ydoc.on('update', async (update: Uint8Array) => { 36 | mdb.storeUpdate(docName, update); 37 | }); 38 | }, 39 | writeState: (docName: string, ydoc: IWSSharedDoc) => { 40 | return new Promise((resolve) => { 41 | resolve(true); 42 | }); 43 | }, 44 | }); 45 | 46 | server.listen(process.env.PORT, () => { 47 | console.log('listening on port:' + process.env.PORT); 48 | }); 49 | -------------------------------------------------------------------------------- /example-servers/rollup/src/websocket/interfaces.mts: -------------------------------------------------------------------------------- 1 | import * as awarenessProtocol from 'y-protocols/awareness.js'; 2 | import * as Y from 'yjs'; 3 | 4 | export interface IWSSharedDoc extends Y.Doc { 5 | name: string; 6 | conns: Map>; 7 | awareness: awarenessProtocol.Awareness; 8 | } 9 | 10 | export interface IPersistence { 11 | bindState: (arg1: string, arg2: IWSSharedDoc) => void; 12 | writeState: (arg1: string, arg2: IWSSharedDoc) => Promise; 13 | provider?: any; 14 | } 15 | -------------------------------------------------------------------------------- /example-servers/rollup/src/websocket/utils.mts: -------------------------------------------------------------------------------- 1 | import * as Y from 'yjs'; 2 | import * as syncProtocol from 'y-protocols/sync'; 3 | import * as awarenessProtocol from 'y-protocols/awareness'; 4 | 5 | import * as encoding from 'lib0/encoding'; 6 | import * as decoding from 'lib0/decoding'; 7 | import * as map from 'lib0/map'; 8 | 9 | import { IPersistence, IWSSharedDoc } from './interfaces.mjs'; 10 | 11 | const wsReadyStateConnecting = 0; 12 | const wsReadyStateOpen = 1; 13 | const wsReadyStateClosing = 2; // eslint-disable-line 14 | const wsReadyStateClosed = 3; // eslint-disable-line 15 | 16 | // disable gc when using snapshots! 17 | const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'; 18 | 19 | let persistence: IPersistence | null = null; 20 | 21 | export const setPersistence = (persistence_: IPersistence) => { 22 | persistence = persistence_; 23 | }; 24 | 25 | export const getPersistence = () => persistence; 26 | 27 | // exporting docs so that others can use it 28 | export const docs = new Map(); 29 | 30 | const messageSync = 0; 31 | const messageAwareness = 1; 32 | // const messageAuth = 2 33 | 34 | const updateHandler = (update: Uint8Array, origin: any, doc: Y.Doc) => { 35 | const sharedDoc = doc as IWSSharedDoc; 36 | 37 | const encoder = encoding.createEncoder(); 38 | encoding.writeVarUint(encoder, messageSync); 39 | syncProtocol.writeUpdate(encoder, update); 40 | const message = encoding.toUint8Array(encoder); 41 | sharedDoc.conns.forEach((_, conn) => send(sharedDoc, conn, message)); 42 | }; 43 | 44 | class WSSharedDoc extends Y.Doc implements IWSSharedDoc { 45 | name: string; 46 | conns: Map>; 47 | awareness: awarenessProtocol.Awareness; 48 | 49 | constructor(name: string) { 50 | super({ gc: gcEnabled }); 51 | this.name = name; 52 | this.conns = new Map(); 53 | this.awareness = new awarenessProtocol.Awareness(this); 54 | this.awareness.setLocalState(null); 55 | 56 | const awarenessChangeHandler = ( 57 | { 58 | added, 59 | updated, 60 | removed, 61 | }: { 62 | added: Array; 63 | updated: Array; 64 | removed: Array; 65 | }, 66 | conn: Object | null, 67 | ) => { 68 | const changedClients = added.concat(updated, removed); 69 | if (conn !== null) { 70 | const connControlledIDs = /** @type {Set} */ this.conns.get(conn); 71 | if (connControlledIDs !== undefined) { 72 | added.forEach((clientID) => { 73 | connControlledIDs.add(clientID); 74 | }); 75 | removed.forEach((clientID) => { 76 | connControlledIDs.delete(clientID); 77 | }); 78 | } 79 | } 80 | // broadcast awareness update 81 | const encoder = encoding.createEncoder(); 82 | encoding.writeVarUint(encoder, messageAwareness); 83 | encoding.writeVarUint8Array( 84 | encoder, 85 | awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients), 86 | ); 87 | const buff = encoding.toUint8Array(encoder); 88 | this.conns.forEach((_, c) => { 89 | send(this, c, buff); 90 | }); 91 | }; 92 | this.awareness.on('update', awarenessChangeHandler); 93 | this.on('update', updateHandler); 94 | } 95 | } 96 | 97 | /** 98 | * Gets a Y.Doc by name, whether in memory or on disk 99 | * 100 | * @param {string} docname - the name of the Y.Doc to find or create 101 | * @param {boolean} gc - whether to allow gc on the doc (applies only when created) 102 | * @return {WSSharedDoc} 103 | */ 104 | export const getYDoc = (docname: string, gc = true) => 105 | map.setIfUndefined(docs, docname, () => { 106 | const doc = new WSSharedDoc(docname); 107 | doc.gc = gc; 108 | if (persistence !== null) { 109 | persistence.bindState(docname, doc); 110 | } 111 | docs.set(docname, doc); 112 | return doc; 113 | }); 114 | 115 | const messageListener = (conn: any, doc: IWSSharedDoc, message: Uint8Array) => { 116 | try { 117 | const encoder = encoding.createEncoder(); 118 | const decoder = decoding.createDecoder(message); 119 | const messageType = decoding.readVarUint(decoder); 120 | switch (messageType) { 121 | case messageSync: 122 | encoding.writeVarUint(encoder, messageSync); 123 | syncProtocol.readSyncMessage(decoder, encoder, doc, conn); 124 | 125 | // If the `encoder` only contains the type of reply message and no 126 | // message, there is no need to send the message. When `encoder` only 127 | // contains the type of reply, its length is 1. 128 | if (encoding.length(encoder) > 1) { 129 | send(doc, conn, encoding.toUint8Array(encoder)); 130 | } 131 | break; 132 | case messageAwareness: { 133 | awarenessProtocol.applyAwarenessUpdate( 134 | doc.awareness, 135 | decoding.readVarUint8Array(decoder), 136 | conn, 137 | ); 138 | break; 139 | } 140 | } 141 | } catch (err) { 142 | console.error(err); 143 | } 144 | }; 145 | 146 | const closeConn = (doc: IWSSharedDoc, conn: any) => { 147 | if (doc.conns.has(conn)) { 148 | const controlledIds = doc.conns.get(conn); 149 | doc.conns.delete(conn); 150 | if (controlledIds) { 151 | awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null); 152 | } 153 | if (doc.conns.size === 0 && persistence !== null) { 154 | // if persisted, we store state and destroy ydocument 155 | persistence.writeState(doc.name, doc).then(() => { 156 | doc.destroy(); 157 | }); 158 | docs.delete(doc.name); 159 | } 160 | } 161 | conn.close(); 162 | }; 163 | 164 | const send = (doc: IWSSharedDoc, conn: any, m: Uint8Array) => { 165 | if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) { 166 | closeConn(doc, conn); 167 | } 168 | try { 169 | conn.send(m, (err: any) => { 170 | err != null && closeConn(doc, conn); 171 | }); 172 | } catch (e) { 173 | closeConn(doc, conn); 174 | } 175 | }; 176 | 177 | const pingTimeout = 30000; 178 | 179 | export const setupWSConnection = ( 180 | conn: any, 181 | req: any, 182 | { docName = req.url.slice(1).split('?')[0], gc = true } = {}, 183 | ) => { 184 | conn.binaryType = 'arraybuffer'; 185 | // get doc, initialize if it does not exist yet 186 | const doc = getYDoc(docName, gc); 187 | doc.conns.set(conn, new Set()); 188 | // listen and reply to events 189 | conn.on('message', (message: ArrayBuffer) => messageListener(conn, doc, new Uint8Array(message))); 190 | 191 | // Check if connection is still alive 192 | let pongReceived = true; 193 | const pingInterval = setInterval(() => { 194 | if (!pongReceived) { 195 | if (doc.conns.has(conn)) { 196 | closeConn(doc, conn); 197 | } 198 | clearInterval(pingInterval); 199 | } else if (doc.conns.has(conn)) { 200 | pongReceived = false; 201 | try { 202 | conn.ping(); 203 | } catch (e) { 204 | closeConn(doc, conn); 205 | clearInterval(pingInterval); 206 | } 207 | } 208 | }, pingTimeout); 209 | conn.on('close', () => { 210 | closeConn(doc, conn); 211 | clearInterval(pingInterval); 212 | }); 213 | conn.on('pong', () => { 214 | pongReceived = true; 215 | }); 216 | // put the following in a variables in a block so the interval handlers don't keep in in 217 | // scope 218 | { 219 | // send sync step 1 220 | const encoder = encoding.createEncoder(); 221 | encoding.writeVarUint(encoder, messageSync); 222 | syncProtocol.writeSyncStep1(encoder, doc); 223 | send(doc, conn, encoding.toUint8Array(encoder)); 224 | const awarenessStates = doc.awareness.getStates(); 225 | if (awarenessStates.size > 0) { 226 | const encoder = encoding.createEncoder(); 227 | encoding.writeVarUint(encoder, messageAwareness); 228 | encoding.writeVarUint8Array( 229 | encoder, 230 | awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())), 231 | ); 232 | send(doc, conn, encoding.toUint8Array(encoder)); 233 | } 234 | } 235 | }; 236 | -------------------------------------------------------------------------------- /example-servers/rollup/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // "target": "es2016", 4 | // "module": "ES2020", 5 | "rootDir": "./src", 6 | // "moduleResolution": "Node", 7 | // "resolveJsonModule": true, 8 | "outDir": "./dist", 9 | // "esModuleInterop": true, 10 | // "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | // "skipLibCheck": true 13 | 14 | /* Copied from: https://github.com/jupyterlab/jupyterlab/blob/main/tsconfigbase.json */ 15 | "allowSyntheticDefaultImports": true, 16 | "esModuleInterop": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "noEmitOnError": true, 20 | "noImplicitAny": true, 21 | "preserveWatchOutput": true, 22 | "resolveJsonModule": true, 23 | "sourceMap": true, 24 | "strictNullChecks": true, 25 | "target": "ES2018", 26 | "types": [] 27 | }, 28 | "include": ["./src"] 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-mongodb-provider", 3 | "version": "0.2.1", 4 | "description": "MongoDB database adapter for Yjs", 5 | "type": "module", 6 | "main": "./dist/y-mongodb.cjs", 7 | "module": "./src/y-mongodb.js", 8 | "types": "./dist/y-mongodb.d.ts", 9 | "exports": { 10 | ".": { 11 | "module": "./src/y-mongodb.js", 12 | "import": "./src/y-mongodb.js", 13 | "require": "./dist/y-mongodb.cjs", 14 | "types": "./dist/y-mongodb.d.ts" 15 | } 16 | }, 17 | "scripts": { 18 | "dist": "rollup -c && tsc", 19 | "clean": "rm -rf dist", 20 | "test": "npm run dist && jest", 21 | "lint": "npx eslint ./src/* && npx tsc", 22 | "build": "npm run dist" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/MaxNoetzold/y-mongodb-provider.git" 27 | }, 28 | "author": "Max Nötzold ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/MaxNoetzold/y-mongodb-provider/issues" 32 | }, 33 | "dependencies": { 34 | "lib0": "^0.2.108", 35 | "mongodb": "^6.16.0" 36 | }, 37 | "peerDependencies": { 38 | "yjs": "^13.6.15" 39 | }, 40 | "devDependencies": { 41 | "@types/jest": "^29.5.14", 42 | "@types/node": "^22.15.24", 43 | "eslint": "^8.57.1", 44 | "eslint-config-airbnb-base": "^15.0.0", 45 | "eslint-config-prettier": "^10.1.5", 46 | "eslint-plugin-prettier": "^5.4.0", 47 | "jest": "^29.7.0", 48 | "mongodb-memory-server": "^10.1.4", 49 | "rollup": "^4.41.1", 50 | "typescript": "^5.8.3", 51 | "yjs": "^13.6.27" 52 | }, 53 | "files": [ 54 | "dist/*", 55 | "src/*" 56 | ], 57 | "homepage": "https://github.com/MaxNoetzold/y-mongodb-provider#readme", 58 | "keywords": [ 59 | "Yjs", 60 | "MongoDB", 61 | "database", 62 | "adapter", 63 | "shared editing", 64 | "collaboration", 65 | "offline", 66 | "CRDT", 67 | "concurrency", 68 | "persistence" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | input: './src/y-mongodb.js', 4 | output: { 5 | name: 'Y', 6 | file: 'dist/y-mongodb.cjs', 7 | format: 'cjs', 8 | sourcemap: true, 9 | paths: (path) => { 10 | if (/^lib0\//.test(path)) { 11 | // return `lib0/dist/${path.slice(5, -3)}.cjs 12 | return `lib0/dist/${path.slice(5)}.cjs`; 13 | } 14 | return path; 15 | }, 16 | }, 17 | external: (id) => /^(lib0|yjs|mongodb|buffer)/.test(id), 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /src/mongo-adapter.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | 3 | /** 4 | * Parse a MongoDB connection string and return the database name. 5 | * 6 | * @param {string} connectionString 7 | * @returns {string} 8 | */ 9 | function getMongoDbDatabaseName(connectionString) { 10 | const url = new URL(connectionString); 11 | const database = url.pathname.slice(1); 12 | return database; 13 | } 14 | 15 | export class MongoAdapter { 16 | /** 17 | * Create a MongoAdapter instance. 18 | * @param {string|{client: MongoClient, db: import('mongodb').Db}} dbConnection A MongoDB connection string or an object containing a MongoClient instance (`client`) and a database instance (`db`). 19 | * @param {object} opts 20 | * @param {string} opts.collection Name of the collection where all documents are stored. 21 | * @param {boolean} opts.multipleCollections When set to true, each document gets an own 22 | * collection (instead of all documents stored in the same one). 23 | * When set to true, the option $collection gets ignored. 24 | */ 25 | constructor(dbConnection, { collection, multipleCollections }) { 26 | this.collection = collection; 27 | this.multipleCollections = multipleCollections; 28 | 29 | if (typeof dbConnection === 'string') { 30 | // Connection string logic 31 | const databaseName = getMongoDbDatabaseName(dbConnection); 32 | this.client = new MongoClient(dbConnection); 33 | this.db = this.client.db(databaseName); 34 | } else if (typeof dbConnection === 'object' && dbConnection.client && dbConnection.db) { 35 | // Connection object logic 36 | this.client = dbConnection.client; 37 | this.db = dbConnection.db; 38 | } else { 39 | throw new Error( 40 | 'Invalid dbConnection. Must be a connection string or an object with client and db.', 41 | ); 42 | } 43 | 44 | /* 45 | NOTE: client.connect() is optional since v4.7 46 | "However, MongoClient.connect can still be called manually and remains useful for 47 | learning about misconfiguration (auth, server not started, connection string correctness) 48 | early in your application's startup." 49 | 50 | I will not use it for now, but may change that in the future. 51 | */ 52 | } 53 | 54 | /** 55 | * Get the MongoDB collection name for any docName 56 | * @param {import('mongodb').Filter} query 57 | * @returns {string} collectionName 58 | */ 59 | _getCollectionName({ docName }) { 60 | if (this.multipleCollections) { 61 | return docName; 62 | } else { 63 | return this.collection; 64 | } 65 | } 66 | 67 | /** 68 | * 69 | * @param {import('mongodb').Filter} query 70 | * @param {{limit?: number; reverse?: boolean;}} [options] 71 | * @returns {Promise[]>} 72 | */ 73 | find(query, options) { 74 | const { limit = 0, reverse = false } = options || {}; 75 | 76 | /** @type {{ clock: 1 | -1, part: 1 | -1 }} */ 77 | const sortQuery = reverse ? { clock: -1, part: 1 } : { clock: 1, part: 1 }; 78 | 79 | const collection = this.db.collection(this._getCollectionName(query)); 80 | return collection.find(query, { limit, sort: sortQuery }).toArray(); 81 | } 82 | 83 | /** 84 | * Apply a $query and get one document from MongoDB. 85 | * @param {import('mongodb').Filter} query 86 | * @param {{limit?: number; reverse?: boolean;}} [options] 87 | * @returns {Promise | null>} 88 | */ 89 | findOne(query, options) { 90 | return this.find(query, options).then((docs) => docs[0] || null); 91 | } 92 | 93 | /** 94 | * Store one document in MongoDB. 95 | * @param {import('mongodb').Filter} query 96 | * @param {import('mongodb').UpdateFilter} values 97 | * @returns {Promise | null>} Stored document 98 | */ 99 | async put(query, values) { 100 | if (!query.docName || !query.version || !values.value) { 101 | throw new Error('Document and version must be provided'); 102 | } 103 | 104 | const collection = this.db.collection(this._getCollectionName(query)); 105 | 106 | await collection.updateOne(query, { $set: values }, { upsert: true }); 107 | return this.findOne(query); 108 | } 109 | 110 | /** 111 | * Removes all documents that fit the $query 112 | * @param {import('mongodb').Filter} query 113 | * @returns {Promise} Contains status of the operation 114 | */ 115 | delete(query) { 116 | const collection = this.db.collection(this._getCollectionName(query)); 117 | 118 | /* 119 | Note from mongodb v4.7 release notes: 120 | "It's a known limitation that explicit sessions (client.startSession) and 121 | initializeOrderedBulkOp, initializeUnorderedBulkOp cannot be used until 122 | MongoClient.connect is first called. 123 | Look forward to a future patch release that will correct these inconsistencies." 124 | 125 | I dont know yet if this is a problem for me here. 126 | */ 127 | const bulk = collection.initializeOrderedBulkOp(); 128 | bulk.find(query).delete(); 129 | return bulk.execute(); 130 | } 131 | 132 | /** 133 | * Close connection to MongoDB instance. 134 | */ 135 | async close() { 136 | await this.client.close(); 137 | } 138 | 139 | /** 140 | * Get all collection names stored on the MongoDB instance. 141 | * @returns {Promise} 142 | */ 143 | async getCollectionNames() { 144 | const collectionInfos = await this.db.listCollections().toArray(); 145 | return collectionInfos.map((c) => c.name); 146 | } 147 | 148 | /** 149 | * Delete database 150 | */ 151 | async flush() { 152 | await this.db.dropDatabase(); 153 | await this.client.close(); 154 | } 155 | 156 | /** 157 | * Delete collection 158 | * @param {string} collectionName 159 | */ 160 | dropCollection(collectionName) { 161 | return this.db.collection(collectionName).drop(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import * as Y from 'yjs'; 2 | import * as binary from 'lib0/binary'; 3 | import * as encoding from 'lib0/encoding'; 4 | import * as decoding from 'lib0/decoding'; 5 | import { Buffer } from 'buffer'; 6 | 7 | export const PREFERRED_TRIM_SIZE = 400; 8 | const MAX_DOCUMENT_SIZE = 15000000; // ~15MB (plus space for metadata) 9 | 10 | /** 11 | * Remove all documents from db with Clock between $from and $to 12 | * 13 | * @param {import('./mongo-adapter.js').MongoAdapter} db 14 | * @param {string} docName 15 | * @param {number} from Greater than or equal 16 | * @param {number} to lower than (not equal) 17 | * @returns {Promise} Contains status of the operation 18 | */ 19 | export const clearUpdatesRange = async (db, docName, from, to) => 20 | db.delete({ 21 | docName, 22 | clock: { 23 | $gte: from, 24 | $lt: to, 25 | }, 26 | }); 27 | 28 | /** 29 | * Create a unique key for a update message. 30 | * @param {string} docName 31 | * @param {number} [clock] must be unique 32 | * @return {{version: "v1"; docName: string; action: "update"; clock?: number; }} 33 | */ 34 | const createDocumentUpdateKey = (docName, clock) => { 35 | if (clock !== undefined) { 36 | return { 37 | version: 'v1', 38 | action: 'update', 39 | docName, 40 | clock, 41 | }; 42 | } else { 43 | return { 44 | version: 'v1', 45 | action: 'update', 46 | docName, 47 | }; 48 | } 49 | }; 50 | 51 | /** 52 | * We have a separate state vector key so we can iterate efficiently over all documents 53 | * @param {string} docName 54 | * @return {{docName: string; version: "v1_sv"}} 55 | */ 56 | export const createDocumentStateVectorKey = (docName) => ({ 57 | docName, 58 | version: 'v1_sv', 59 | }); 60 | 61 | /** 62 | * @param {string} docName 63 | * @param {string} metaKey 64 | * @return {{docName: string; version: "v1"; metaKey: string; }} 65 | */ 66 | export const createDocumentMetaKey = (docName, metaKey) => ({ 67 | version: 'v1', 68 | docName, 69 | metaKey: `meta_${metaKey}`, 70 | }); 71 | 72 | /** 73 | * @param {import('./mongo-adapter.js').MongoAdapter} db 74 | * @return {Promise} 75 | */ 76 | export const flushDB = (db) => db.flush(); 77 | 78 | /** 79 | * 80 | * This function converts MongoDB updates to a buffer that can be processed by the application. 81 | * It handles both complete documents and large documents that have been split into smaller 'parts' due to MongoDB's size limit. 82 | * For split documents, it collects all the parts and merges them together. 83 | * It assumes that the parts of a split document are ordered and located exactly after the document with part number 1. 84 | * 85 | * @param {{ _id: import("mongodb").ObjectId; action: string; version: string; docName: string; clock: number; part?: number; value: import("mongodb").Binary; }[]} docs 86 | * @return {Uint8Array[]} 87 | */ 88 | const convertMongoUpdates = (docs) => { 89 | if (!Array.isArray(docs) || !docs.length) return []; 90 | 91 | /** @type {Uint8Array[]} */ 92 | const updates = []; 93 | for (let i = 0; i < docs.length; i++) { 94 | const doc = docs[i]; 95 | if (!doc.part) { 96 | updates.push(doc.value.buffer); 97 | } else if (doc.part === 1) { 98 | // merge the docs together that got split because of mongodb size limits 99 | const parts = [doc.value.buffer]; 100 | let j; 101 | let currentPartId = doc.part; 102 | for (j = i + 1; j < docs.length; j++) { 103 | const part = docs[j]; 104 | if (part.part && part.clock === doc.clock) { 105 | if (currentPartId !== part.part - 1) { 106 | throw new Error('Couldnt merge updates together because a part is missing!'); 107 | } 108 | parts.push(part.value.buffer); 109 | currentPartId = part.part; 110 | } else { 111 | break; 112 | } 113 | } 114 | updates.push(Buffer.concat(parts)); 115 | // set i to j - 1 because we already processed all parts 116 | i = j - 1; 117 | } 118 | } 119 | return updates; 120 | }; 121 | 122 | /** 123 | * Get all document updates for a specific document. 124 | * 125 | * @param {import('./mongo-adapter.js').MongoAdapter} db 126 | * @param {string} docName 127 | * @return {Promise} 128 | */ 129 | export const getMongoUpdates = async (db, docName) => { 130 | const docs = await db.find(createDocumentUpdateKey(docName)); 131 | // TODO: I dont know how to type this without actual typescript 132 | // @ts-ignore 133 | return convertMongoUpdates(docs); 134 | }; 135 | 136 | /** 137 | * @param {import('./mongo-adapter.js').MongoAdapter} db 138 | * @param {string} docName 139 | * @return {Promise} Returns -1 if this document doesn't exist yet 140 | */ 141 | export const getCurrentUpdateClock = (db, docName) => 142 | db 143 | .findOne( 144 | { 145 | ...createDocumentUpdateKey(docName, 0), 146 | clock: { 147 | $gte: 0, 148 | $lt: binary.BITS32, 149 | }, 150 | }, 151 | { reverse: true }, 152 | ) 153 | .then((update) => { 154 | if (!update) { 155 | return -1; 156 | } else { 157 | return update.clock; 158 | } 159 | }); 160 | 161 | /** 162 | * @param {import('./mongo-adapter.js').MongoAdapter} db 163 | * @param {string} docName 164 | * @param {Uint8Array} sv state vector 165 | * @param {number} clock current clock of the document so we can determine 166 | * when this statevector was created 167 | */ 168 | const writeStateVector = async (db, docName, sv, clock) => { 169 | const encoder = encoding.createEncoder(); 170 | encoding.writeVarUint(encoder, clock); 171 | encoding.writeVarUint8Array(encoder, sv); 172 | await db.put(createDocumentStateVectorKey(docName), { 173 | value: encoding.toUint8Array(encoder), 174 | }); 175 | }; 176 | 177 | /** 178 | * @param {import('./mongo-adapter.js').MongoAdapter} db 179 | * @param {string} docName 180 | * @param {Uint8Array} update 181 | * @return {Promise} Returns the clock of the stored update 182 | */ 183 | export const storeUpdate = async (db, docName, update) => { 184 | const clock = await getCurrentUpdateClock(db, docName); 185 | if (clock === -1) { 186 | // make sure that a state vector is always written, so we can search for available documents 187 | const ydoc = new Y.Doc(); 188 | Y.applyUpdate(ydoc, update); 189 | const sv = Y.encodeStateVector(ydoc); 190 | await writeStateVector(db, docName, sv, 0); 191 | } 192 | 193 | // mongodb has a maximum document size of 16MB; 194 | // if our buffer exceeds it, we store the update in multiple documents 195 | if (update.length <= MAX_DOCUMENT_SIZE) { 196 | await db.put(createDocumentUpdateKey(docName, clock + 1), { 197 | value: update, 198 | }); 199 | } else { 200 | const totalChunks = Math.ceil(update.length / MAX_DOCUMENT_SIZE); 201 | 202 | const putPromises = []; 203 | for (let i = 0; i < totalChunks; i++) { 204 | const start = i * MAX_DOCUMENT_SIZE; 205 | const end = Math.min(start + MAX_DOCUMENT_SIZE, update.length); 206 | const chunk = update.subarray(start, end); 207 | 208 | putPromises.push( 209 | db.put({ ...createDocumentUpdateKey(docName, clock + 1), part: i + 1 }, { value: chunk }), 210 | ); 211 | } 212 | 213 | await Promise.all(putPromises); 214 | } 215 | 216 | return clock + 1; 217 | }; 218 | 219 | /** 220 | * For now this is a helper method that creates a Y.Doc and then re-encodes a document update. 221 | * In the future this will be handled by Yjs without creating a Y.Doc (constant memory consumption). 222 | * 223 | * @param {Array} updates 224 | * @return {{update:Uint8Array, sv: Uint8Array}} 225 | */ 226 | export const mergeUpdates = (updates) => { 227 | const ydoc = new Y.Doc(); 228 | ydoc.transact(() => { 229 | for (let i = 0; i < updates.length; i++) { 230 | Y.applyUpdate(ydoc, updates[i]); 231 | } 232 | }); 233 | return { update: Y.encodeStateAsUpdate(ydoc), sv: Y.encodeStateVector(ydoc) }; 234 | }; 235 | 236 | /** 237 | * @param {import("mongodb").Binary} buf 238 | * @return {{ sv: Uint8Array, clock: number }} 239 | */ 240 | export const decodeMongodbStateVector = (buf) => { 241 | let decoder; 242 | if (Buffer.isBuffer(buf)) { 243 | decoder = decoding.createDecoder(buf); 244 | } else if (Buffer.isBuffer(buf?.buffer)) { 245 | decoder = decoding.createDecoder(buf.buffer); 246 | } else { 247 | throw new Error('No buffer provided at decodeMongodbStateVector()'); 248 | } 249 | const clock = decoding.readVarUint(decoder); 250 | const sv = decoding.readVarUint8Array(decoder); 251 | return { sv, clock }; 252 | }; 253 | 254 | /** 255 | * @param {import('./mongo-adapter.js').MongoAdapter} db 256 | * @param {string} docName 257 | */ 258 | export const readStateVector = async (db, docName) => { 259 | const doc = await db.findOne({ ...createDocumentStateVectorKey(docName) }); 260 | if (!doc?.value) { 261 | // no state vector created yet or no document exists 262 | return { sv: null, clock: -1 }; 263 | } 264 | return decodeMongodbStateVector(doc.value); 265 | }; 266 | 267 | /** 268 | * 269 | * @param {import('./mongo-adapter.js').MongoAdapter} db 270 | */ 271 | export const getAllSVDocs = async (db) => db.find({ version: 'v1_sv' }); 272 | 273 | /** 274 | * Merge all MongoDB documents of the same yjs document together. 275 | * @param {import('./mongo-adapter.js').MongoAdapter} db 276 | * @param {string} docName 277 | * @param {Uint8Array} stateAsUpdate 278 | * @param {Uint8Array} stateVector 279 | * @return {Promise} returns the clock of the flushed doc 280 | */ 281 | export const flushDocument = async (db, docName, stateAsUpdate, stateVector) => { 282 | const clock = await storeUpdate(db, docName, stateAsUpdate); 283 | await writeStateVector(db, docName, stateVector, clock); 284 | await clearUpdatesRange(db, docName, 0, clock); 285 | return clock; 286 | }; 287 | -------------------------------------------------------------------------------- /src/y-mongodb.js: -------------------------------------------------------------------------------- 1 | import * as Y from 'yjs'; 2 | import * as binary from 'lib0/binary'; 3 | import * as promise from 'lib0/promise'; 4 | import { MongoAdapter } from './mongo-adapter.js'; 5 | import * as U from './utils.js'; 6 | 7 | export class MongodbPersistence { 8 | /** 9 | * Create a y-mongodb persistence instance. 10 | * @param {string|{client: import('mongodb').MongoClient, db: import('mongodb').Db}} connectionObj A MongoDB connection string or an object containing a MongoClient instance (`client`) and a database instance (`db`). 11 | * @param {object} [opts] Additional optional parameters. 12 | * @param {string} [opts.collectionName] Name of the collection where all 13 | * documents are stored. Default: "yjs-writings" 14 | * @param {boolean} [opts.multipleCollections] When set to true, each document gets 15 | * an own collection (instead of all documents stored in the same one). When set to true, 16 | * the option collectionName gets ignored. Default: false 17 | * @param {number} [opts.flushSize] The number of stored transactions needed until 18 | * they are merged automatically into one Mongodb document. Default: 400 19 | */ 20 | constructor(connectionObj, opts = {}) { 21 | const { collectionName = 'yjs-writings', multipleCollections = false, flushSize = 400 } = opts; 22 | if (typeof collectionName !== 'string' || !collectionName) { 23 | throw new Error( 24 | 'Constructor option "collectionName" is not a valid string. Either dont use this option (default is "yjs-writings") or use a valid string! Take a look into the Readme for more information: https://github.com/MaxNoetzold/y-mongodb-provider#persistence--mongodbpersistenceconnectionlink-string-options-object', 25 | ); 26 | } 27 | if (typeof multipleCollections !== 'boolean') { 28 | throw new Error( 29 | 'Constructor option "multipleCollections" is not a boolean. Either dont use this option (default is "false") or use a valid boolean! Take a look into the Readme for more information: https://github.com/MaxNoetzold/y-mongodb-provider#persistence--mongodbpersistenceconnectionlink-string-options-object', 30 | ); 31 | } 32 | if (typeof flushSize !== 'number' || flushSize <= 0) { 33 | throw new Error( 34 | 'Constructor option "flushSize" is not a valid number. Either dont use this option (default is "400") or use a valid number larger than 0! Take a look into the Readme for more information: https://github.com/MaxNoetzold/y-mongodb-provider#persistence--mongodbpersistenceconnectionlink-string-options-object', 35 | ); 36 | } 37 | const db = new MongoAdapter(connectionObj, { 38 | collection: collectionName, 39 | multipleCollections, 40 | }); 41 | this.flushSize = flushSize ?? U.PREFERRED_TRIM_SIZE; 42 | this.multipleCollections = multipleCollections; 43 | 44 | // scope the queue of the transaction to each docName 45 | // -> this should allow concurrency for different rooms 46 | // Idea and adjusted code from: https://github.com/fadiquader/y-mongodb/issues/10 47 | this.tr = {}; 48 | 49 | /** 50 | * Execute an transaction on a database. This will ensure that other processes are 51 | * currently not writing. 52 | * 53 | * This is a private method and might change in the future. 54 | * 55 | * @template T 56 | * 57 | * @param {function(MongoAdapter):Promise} f A transaction that receives the db object 58 | * @return {Promise} 59 | */ 60 | this._transact = (docName, f) => { 61 | if (!this.tr[docName]) { 62 | this.tr[docName] = promise.resolve(); 63 | } 64 | 65 | const currTr = this.tr[docName]; 66 | let nextTr = null; 67 | 68 | nextTr = (async () => { 69 | await currTr; 70 | 71 | let res = /** @type {any} */ (null); 72 | try { 73 | res = await f(db); 74 | } catch (err) { 75 | // eslint-disable-next-line no-console 76 | console.warn('Error during saving transaction', err); 77 | } 78 | 79 | // once the last transaction for a given docName resolves, remove it from the queue 80 | if (this.tr[docName] === nextTr) { 81 | delete this.tr[docName]; 82 | } 83 | 84 | return res; 85 | })(); 86 | 87 | this.tr[docName] = nextTr; 88 | 89 | return this.tr[docName]; 90 | }; 91 | } 92 | 93 | /** 94 | * Create a Y.Doc instance with the data persistet in mongodb. 95 | * Use this to temporarily create a Yjs document to sync changes or extract data. 96 | * 97 | * @param {string} docName 98 | * @return {Promise} 99 | */ 100 | getYDoc(docName) { 101 | return this._transact(docName, async (db) => { 102 | const updates = await U.getMongoUpdates(db, docName); 103 | const ydoc = new Y.Doc(); 104 | ydoc.transact(() => { 105 | for (let i = 0; i < updates.length; i++) { 106 | Y.applyUpdate(ydoc, updates[i]); 107 | } 108 | }); 109 | if (updates.length > this.flushSize) { 110 | await U.flushDocument(db, docName, Y.encodeStateAsUpdate(ydoc), Y.encodeStateVector(ydoc)); 111 | } 112 | return ydoc; 113 | }); 114 | } 115 | 116 | /** 117 | * Store a single document update to the database. 118 | * 119 | * @param {string} docName 120 | * @param {Uint8Array} update 121 | * @return {Promise} Returns the clock of the stored update 122 | */ 123 | storeUpdate(docName, update) { 124 | return this._transact(docName, (db) => U.storeUpdate(db, docName, update)); 125 | } 126 | 127 | /** 128 | * The state vector (describing the state of the persisted document - see https://github.com/yjs/yjs#Document-Updates) is maintained in a separate field and constantly updated. 129 | * 130 | * This allows you to sync changes without actually creating a Yjs document. 131 | * 132 | * @param {string} docName 133 | * @return {Promise} 134 | */ 135 | getStateVector(docName) { 136 | return this._transact(docName, async (db) => { 137 | const { clock, sv } = await U.readStateVector(db, docName); 138 | let curClock = -1; 139 | if (sv !== null) { 140 | curClock = await U.getCurrentUpdateClock(db, docName); 141 | } 142 | if (sv !== null && clock === curClock) { 143 | return sv; 144 | } else { 145 | // current state vector is outdated 146 | const updates = await U.getMongoUpdates(db, docName); 147 | const { update, sv: newSv } = U.mergeUpdates(updates); 148 | await U.flushDocument(db, docName, update, newSv); 149 | return newSv; 150 | } 151 | }); 152 | } 153 | 154 | /** 155 | * Get the differences directly from the database. 156 | * The same as Y.encodeStateAsUpdate(ydoc, stateVector). 157 | * @param {string} docName 158 | * @param {Uint8Array} stateVector 159 | */ 160 | async getDiff(docName, stateVector) { 161 | const ydoc = await this.getYDoc(docName); 162 | return Y.encodeStateAsUpdate(ydoc, stateVector); 163 | } 164 | 165 | /** 166 | * Delete a document, and all associated data from the database. 167 | * When option multipleCollections is set, it removes the corresponding collection 168 | * @param {string} docName 169 | * @return {Promise} 170 | */ 171 | clearDocument(docName) { 172 | return this._transact(docName, async (db) => { 173 | if (!this.multipleCollections) { 174 | await db.delete(U.createDocumentStateVectorKey(docName)); 175 | await U.clearUpdatesRange(db, docName, 0, binary.BITS32); 176 | } else { 177 | await db.dropCollection(docName); 178 | } 179 | }); 180 | } 181 | 182 | /** 183 | * Persist some meta information in the database and associate it 184 | * with a document. It is up to you what you store here. 185 | * You could, for example, store credentials here. 186 | * 187 | * @param {string} docName 188 | * @param {string} metaKey 189 | * @param {any} value 190 | * @return {Promise} 191 | */ 192 | setMeta(docName, metaKey, value) { 193 | /* Unlike y-leveldb, we simply store the value here without encoding 194 | it in a buffer beforehand. */ 195 | return this._transact(docName, async (db) => { 196 | await db.put(U.createDocumentMetaKey(docName, metaKey), { value }); 197 | }); 198 | } 199 | 200 | /** 201 | * Retrieve a store meta value from the database. Returns undefined if the 202 | * metaKey doesn't exist. 203 | * 204 | * @param {string} docName 205 | * @param {string} metaKey 206 | * @return {Promise} 207 | */ 208 | getMeta(docName, metaKey) { 209 | return this._transact(docName, async (db) => { 210 | const res = await db.findOne({ 211 | ...U.createDocumentMetaKey(docName, metaKey), 212 | }); 213 | if (!res?.value) { 214 | return undefined; 215 | } 216 | return res.value; 217 | }); 218 | } 219 | 220 | /** 221 | * Delete a store meta value. 222 | * 223 | * @param {string} docName 224 | * @param {string} metaKey 225 | * @return {Promise} 226 | */ 227 | delMeta(docName, metaKey) { 228 | return this._transact(docName, (db) => 229 | db.delete({ 230 | ...U.createDocumentMetaKey(docName, metaKey), 231 | }), 232 | ); 233 | } 234 | 235 | /** 236 | * Retrieve the names of all stored documents. 237 | * 238 | * @return {Promise} 239 | */ 240 | getAllDocNames() { 241 | return this._transact('global', async (db) => { 242 | if (this.multipleCollections) { 243 | // get all collection names from db 244 | return db.getCollectionNames(); 245 | } else { 246 | // when all docs are stored in the same collection we just need to get all 247 | // statevectors and return their names 248 | const docs = await U.getAllSVDocs(db); 249 | return docs.map((doc) => doc.docName); 250 | } 251 | }); 252 | } 253 | 254 | /** 255 | * Retrieve the state vectors of all stored documents. 256 | * You can use this to sync two y-mongodb instances. 257 | * !Note: The state vectors might be outdated if the associated document 258 | * is not yet flushed. So use with caution. 259 | * @return {Promise<{ name: string, sv: Uint8Array, clock: number }[]>} 260 | */ 261 | getAllDocStateVectors() { 262 | return this._transact('global', async (db) => { 263 | const docs = await U.getAllSVDocs(db); 264 | return docs.map((doc) => { 265 | const { sv, clock } = U.decodeMongodbStateVector(doc.value); 266 | return { name: doc.docName, sv, clock }; 267 | }); 268 | }); 269 | } 270 | 271 | /** 272 | * Internally y-mongodb stores incremental updates. You can merge all document 273 | * updates to a single entry. You probably never have to use this. 274 | * It is done automatically every $options.flushsize (default 400) transactions. 275 | * 276 | * @param {string} docName 277 | * @return {Promise} 278 | */ 279 | flushDocument(docName) { 280 | return this._transact(docName, async (db) => { 281 | const updates = await U.getMongoUpdates(db, docName); 282 | const { update, sv } = U.mergeUpdates(updates); 283 | await U.flushDocument(db, docName, update, sv); 284 | }); 285 | } 286 | 287 | /** 288 | * Delete the whole yjs mongodb 289 | * @return {Promise} 290 | */ 291 | flushDB() { 292 | return this._transact('global', async (db) => { 293 | await U.flushDB(db); 294 | }); 295 | } 296 | 297 | /** 298 | * Closes open database connection 299 | * @returns {Promise} 300 | */ 301 | destroy() { 302 | return this._transact('global', async (db) => { 303 | await db.close(); 304 | }); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /tests/generateLargeText.js: -------------------------------------------------------------------------------- 1 | function generateLargeText(sizeInMb) { 2 | const sizeInBytes = sizeInMb * 1024 * 1024; 3 | const numberOfChars = Math.floor(sizeInBytes / 2); // JavaScript uses UTF-16 encoding, which uses 2 bytes per character 4 | let largeText = ''; 5 | for (let i = 0; i < numberOfChars; i++) { 6 | largeText += 'a'; 7 | } 8 | return largeText; 9 | } 10 | 11 | module.exports = generateLargeText; 12 | -------------------------------------------------------------------------------- /tests/y-mongodb.test.js: -------------------------------------------------------------------------------- 1 | const Y = require('yjs'); 2 | const { MongoMemoryServer } = require('mongodb-memory-server'); 3 | const { MongoClient } = require('mongodb'); 4 | // I ignore it here because if you run "npm run test" it first builds the project and then runs the tests. 5 | // eslint-disable-next-line import/no-unresolved 6 | const { MongodbPersistence } = require('../dist/y-mongodb.cjs'); 7 | const generateLargeText = require('./generateLargeText.js'); 8 | 9 | const storeDocWithText = async (mongodbPersistence, docName, content) => { 10 | const ydoc = new Y.Doc(); 11 | // to wait for the update to be stored in the database before we check 12 | const updatePromise = new Promise((resolve) => { 13 | ydoc.on('update', async (update) => { 14 | await mongodbPersistence.storeUpdate(docName, update); 15 | resolve(); 16 | }); 17 | }); 18 | 19 | const yText = ydoc.getText('name'); 20 | yText.insert(0, content); 21 | 22 | // Wait for the update to be stored 23 | await updatePromise; 24 | }; 25 | 26 | describe('meta with single collection', () => { 27 | let mongoServer; 28 | let mongodbPersistence; 29 | let mongoConnection; 30 | const docName = 'testDoc'; 31 | const collectionName = 'testCollection'; 32 | 33 | beforeAll(async () => { 34 | mongoServer = await MongoMemoryServer.create(); 35 | mongodbPersistence = new MongodbPersistence(mongoServer.getUri(), { collectionName }); 36 | mongoConnection = await MongoClient.connect(mongoServer.getUri(), {}); 37 | }); 38 | 39 | afterAll(async () => { 40 | if (mongodbPersistence) { 41 | await mongodbPersistence.destroy(); 42 | } 43 | if (mongoConnection) { 44 | await mongoConnection.close(); 45 | } 46 | if (mongoServer) { 47 | await mongoServer.stop(); 48 | } 49 | }); 50 | 51 | it('should store meta data', async () => { 52 | const metaKey = 'testKey'; 53 | const expectedValue = 'testValue'; 54 | 55 | await mongodbPersistence.setMeta(docName, metaKey, expectedValue); 56 | 57 | // Check if meta data is stored in the database via the native mongo client 58 | const db = mongoConnection.db(mongoServer.instanceInfo.dbName); 59 | const collection = db.collection(collectionName); 60 | const count = await collection.countDocuments(); 61 | 62 | expect(count).toEqual(1); 63 | }); 64 | 65 | it('should retrieve meta data', async () => { 66 | const metaKey = 'testKey'; 67 | const expectedValue = 'testValue'; 68 | 69 | const value = await mongodbPersistence.getMeta(docName, metaKey); 70 | expect(value).toEqual(expectedValue); 71 | }); 72 | 73 | it('should delete meta data', async () => { 74 | const metaKey = 'testKey'; 75 | 76 | await mongodbPersistence.delMeta(docName, metaKey); 77 | 78 | const value = await mongodbPersistence.getMeta(docName, metaKey); 79 | 80 | expect(value).toEqual(undefined); 81 | }); 82 | }); 83 | 84 | describe('store and retrieve updates in single collection with connection uri', () => { 85 | let mongoServer; 86 | let mongodbPersistence; 87 | let mongoConnection; 88 | const dbName = 'test'; 89 | const docName = 'testDoc'; 90 | const collectionName = 'testCollection'; 91 | const content = 'Testtext'; 92 | 93 | beforeAll(async () => { 94 | mongoServer = await MongoMemoryServer.create(); 95 | const connectionStr = `${mongoServer.getUri()}${dbName}`; 96 | mongodbPersistence = new MongodbPersistence(connectionStr, { collectionName }); 97 | mongoConnection = await MongoClient.connect(connectionStr, {}); 98 | }); 99 | 100 | afterAll(async () => { 101 | if (mongodbPersistence) { 102 | await mongodbPersistence.destroy(); 103 | } 104 | if (mongoConnection) { 105 | await mongoConnection.close(); 106 | } 107 | if (mongoServer) { 108 | await mongoServer.stop(); 109 | } 110 | }); 111 | 112 | it('should store updates', async () => { 113 | await storeDocWithText(mongodbPersistence, docName, content); 114 | 115 | // Check data is stored in the database via the native mongo client 116 | const db = mongoConnection.db(dbName); 117 | const collection = db.collection(collectionName); 118 | const count = await collection.countDocuments(); 119 | 120 | // it will be two because one is the stateVector and the other is the update 121 | expect(count).toEqual(2); 122 | }); 123 | 124 | it('should retrieve stored docs', async () => { 125 | const persistedYdoc = await mongodbPersistence.getYDoc(docName); 126 | 127 | const yText = persistedYdoc.getText('name'); 128 | const yTextContent = yText.toString(); 129 | 130 | expect(yTextContent).toEqual(content); 131 | }); 132 | 133 | it('should store next update', async () => { 134 | const nextContent = 'NextTestText'; 135 | 136 | await storeDocWithText(mongodbPersistence, docName, nextContent); 137 | 138 | const db = mongoConnection.db(dbName); 139 | const collection = db.collection(collectionName); 140 | const count = await collection.countDocuments(); 141 | 142 | // it will be four because one is the stateVector and the other two are the updates 143 | expect(count).toEqual(3); 144 | }); 145 | 146 | it("should flush document's updates", async () => { 147 | const db = mongoConnection.db(dbName); 148 | const collection = db.collection(collectionName); 149 | const count = await collection.countDocuments(); 150 | 151 | // it will be four because one is the stateVector and the other two are the updates 152 | expect(count).toEqual(3); 153 | 154 | await mongodbPersistence.flushDocument(docName); 155 | 156 | const secondCount = await collection.countDocuments(); 157 | expect(secondCount).toEqual(2); 158 | }); 159 | }); 160 | 161 | describe('store and retrieve updates in single collection with external MongoClient', () => { 162 | let mongoServer; 163 | let mongodbPersistence; 164 | let mongoClient; 165 | const dbName = 'test'; 166 | const docName = 'testDoc'; 167 | const collectionName = 'testCollection'; 168 | const content = 'Testtext'; 169 | 170 | beforeAll(async () => { 171 | mongoServer = await MongoMemoryServer.create(); 172 | mongoClient = new MongoClient(mongoServer.getUri()); 173 | await mongoClient.connect(); 174 | const db = mongoClient.db(dbName); 175 | mongodbPersistence = new MongodbPersistence({ client: mongoClient, db }, { collectionName }); 176 | }); 177 | 178 | afterAll(async () => { 179 | if (mongodbPersistence) { 180 | await mongodbPersistence.destroy(); 181 | } 182 | if (mongoClient) { 183 | await mongoClient.close(); 184 | } 185 | if (mongoServer) { 186 | await mongoServer.stop(); 187 | } 188 | }); 189 | 190 | it('should store updates', async () => { 191 | await storeDocWithText(mongodbPersistence, docName, content); 192 | 193 | // Check data is stored in the database via the native mongo client 194 | const db = mongoClient.db(dbName); 195 | const collection = db.collection(collectionName); 196 | const count = await collection.countDocuments(); 197 | 198 | // it will be two because one is the stateVector and the other is the update 199 | expect(count).toEqual(2); 200 | }); 201 | 202 | it('should retrieve stored docs', async () => { 203 | const persistedYdoc = await mongodbPersistence.getYDoc(docName); 204 | 205 | const yText = persistedYdoc.getText('name'); 206 | const yTextContent = yText.toString(); 207 | 208 | expect(yTextContent).toEqual(content); 209 | }); 210 | }); 211 | 212 | describe('clearDocument with single collection', () => { 213 | let mongoServer; 214 | let mongodbPersistence; 215 | let mongoConnection; 216 | const docName = 'testDoc'; 217 | const collectionName = 'testCollection'; 218 | 219 | beforeAll(async () => { 220 | mongoServer = await MongoMemoryServer.create(); 221 | mongodbPersistence = new MongodbPersistence(mongoServer.getUri(), { collectionName }); 222 | mongoConnection = await MongoClient.connect(mongoServer.getUri(), {}); 223 | }); 224 | 225 | afterAll(async () => { 226 | if (mongodbPersistence) { 227 | await mongodbPersistence.destroy(); 228 | } 229 | if (mongoConnection) { 230 | await mongoConnection.close(); 231 | } 232 | if (mongoServer) { 233 | await mongoServer.stop(); 234 | } 235 | }); 236 | 237 | it('should clear document', async () => { 238 | /* 1. Store Data */ 239 | await storeDocWithText(mongodbPersistence, docName, 'blablabla'); 240 | 241 | // Check data is stored in the database via the native mongo client 242 | const db = mongoConnection.db(mongoServer.instanceInfo.dbName); 243 | const collection = db.collection(collectionName); 244 | const count = await collection.countDocuments(); 245 | 246 | // it will be two because one is the stateVector and the other is the update 247 | expect(count).toEqual(2); 248 | 249 | /* 2. Clear data */ 250 | await mongodbPersistence.clearDocument(docName); 251 | 252 | const secondCount = await collection.countDocuments(); 253 | expect(secondCount).toEqual(0); 254 | }); 255 | }); 256 | 257 | describe('store multiple documents in single collection', () => { 258 | let mongoServer; 259 | let mongodbPersistence; 260 | let mongoConnection; 261 | const collectionName = 'testCollection'; 262 | const docNameOne = 'testDocOne'; 263 | const docNameTwo = 'testDocTwo'; 264 | const contentOne = 'TestOne'; 265 | const contentTwo = 'TestTwo'; 266 | 267 | beforeAll(async () => { 268 | mongoServer = await MongoMemoryServer.create(); 269 | mongodbPersistence = new MongodbPersistence(mongoServer.getUri(), { collectionName }); 270 | mongoConnection = await MongoClient.connect(mongoServer.getUri(), {}); 271 | }); 272 | 273 | afterAll(async () => { 274 | if (mongodbPersistence) { 275 | await mongodbPersistence.destroy(); 276 | } 277 | if (mongoConnection) { 278 | await mongoConnection.close(); 279 | } 280 | if (mongoServer) { 281 | await mongoServer.stop(); 282 | } 283 | }); 284 | 285 | it('should store two docs', async () => { 286 | const db = mongoConnection.db(mongoServer.instanceInfo.dbName); 287 | const collection = db.collection(collectionName); 288 | 289 | /* Store first doc */ 290 | await storeDocWithText(mongodbPersistence, docNameOne, contentOne); 291 | 292 | // Check data is stored in the database via the native mongo client 293 | const count = await collection.countDocuments(); 294 | // it will be two because one is the stateVector and the other is the update 295 | expect(count).toEqual(2); 296 | 297 | /* Store second doc */ 298 | await storeDocWithText(mongodbPersistence, docNameTwo, contentTwo); 299 | const countTwo = await collection.countDocuments(); 300 | expect(countTwo).toEqual(4); 301 | }); 302 | 303 | it('getAllDocNames should return all doc names', async () => { 304 | const docNames = await mongodbPersistence.getAllDocNames(); 305 | expect(docNames).toEqual([docNameOne, docNameTwo]); 306 | }); 307 | 308 | it('should clear document one', async () => { 309 | await mongodbPersistence.clearDocument(docNameOne); 310 | 311 | const db = mongoConnection.db(mongoServer.instanceInfo.dbName); 312 | const collection = db.collection(collectionName); 313 | const count = await collection.countDocuments(); 314 | expect(count).toEqual(2); 315 | }); 316 | 317 | it('should clear document two', async () => { 318 | await mongodbPersistence.clearDocument(docNameTwo); 319 | 320 | const db = mongoConnection.db(mongoServer.instanceInfo.dbName); 321 | const collection = db.collection(collectionName); 322 | const count = await collection.countDocuments(); 323 | expect(count).toEqual(0); 324 | }); 325 | }); 326 | 327 | describe('store 40mb of data in single collection', () => { 328 | let mongoServer; 329 | let mongodbPersistence; 330 | let mongoConnection; 331 | const collectionName = 'testCollection'; 332 | const docNameOne = 'docOne'; 333 | const content = generateLargeText(40); 334 | 335 | beforeAll(async () => { 336 | mongoServer = await MongoMemoryServer.create(); 337 | mongodbPersistence = new MongodbPersistence(mongoServer.getUri(), { collectionName }); 338 | mongoConnection = await MongoClient.connect(mongoServer.getUri(), {}); 339 | }); 340 | 341 | afterAll(async () => { 342 | if (mongodbPersistence) { 343 | await mongodbPersistence.destroy(); 344 | } 345 | if (mongoConnection) { 346 | await mongoConnection.close(); 347 | } 348 | if (mongoServer) { 349 | await mongoServer.stop(); 350 | } 351 | }); 352 | 353 | it('should store 40mb of text in three documents', async () => { 354 | await storeDocWithText(mongodbPersistence, docNameOne, content); 355 | 356 | const db = mongoConnection.db(mongoServer.instanceInfo.dbName); 357 | const collection = db.collection(collectionName); 358 | const count = await collection.countDocuments(); 359 | expect(count).toEqual(3); 360 | }); 361 | 362 | it("should retrieve the text of the stored document's updates", async () => { 363 | const persistedYdoc = await mongodbPersistence.getYDoc(docNameOne); 364 | 365 | const yText = persistedYdoc.getText('name'); 366 | const yTextContent = yText.toString(); 367 | 368 | expect(yTextContent.length).toEqual(content.length); 369 | }); 370 | 371 | it("should clear the document's updates", async () => { 372 | await mongodbPersistence.clearDocument(docNameOne); 373 | 374 | const db = mongoConnection.db(mongoServer.instanceInfo.dbName); 375 | const collection = db.collection(collectionName); 376 | const count = await collection.countDocuments(); 377 | expect(count).toEqual(0); 378 | }); 379 | }); 380 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["es2018", "dom"], 5 | "allowJs": true, 6 | "checkJs": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "emitDeclarationOnly": true, 10 | "strict": true, 11 | "noImplicitAny": false, 12 | "moduleResolution": "node", 13 | "outDir": "./dist" 14 | }, 15 | "include": ["./src/**/*"], 16 | "exclude": ["../lib0/**/*", "node_modules/**/*", "dist/**/*", "./tests/**/*"] 17 | } 18 | --------------------------------------------------------------------------------