[
  {
    "path": ".github/workflows/node.js.yml",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n\nname: Node.js CI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [16.x, 18.x]\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v1\n      with:\n        node-version: ${{ matrix.node-version }}\n    - run: npm ci\n    - run: npm run build --if-present\n    - run: npm test\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\n"
  },
  {
    "path": ".markdownlint.json",
    "content": "{\n  \"default\": true,\n  \"no-inline-html\": false\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014\n  - Kevin Jahns <kevin.jahns@rwth-aachen.de>.\n  - Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# y-indexeddb\n\n> IndexedDB database provider for Yjs. [Documentation](https://docs.yjs.dev/ecosystem/database-provider/y-indexeddb)\n\nUse the IndexedDB database adapter to store your shared data persistently in\nthe browser. The next time you join the session, your changes will still be\nthere.\n\n* Minimizes the amount of data exchanged between server and client\n* Makes offline editing possible\n\n## Getting Started\n\nYou find the complete documentation published online: [API documentation](https://docs.yjs.dev/ecosystem/database-provider/y-indexeddb).\n\n```sh\nnpm i --save y-indexeddb\n```\n\n```js\nconst provider = new IndexeddbPersistence(docName, ydoc)\n\nprovider.on('synced', () => {\n  console.log('content from the database is loaded')\n})\n```\n\n## API\n\n<dl>\n  <b><code>provider = new IndexeddbPersistence(docName: string, ydoc: Y.Doc)</code></b>\n  <dd>\nCreate a y-indexeddb persistence provider. Specify docName as a unique string\nthat identifies this document. In most cases, you want to use the same identifier\nthat is used as the room-name in the connection provider.\n  </dd>\n  <b><code>provider.on('synced', function(idbPersistence: IndexeddbPersistence))</code></b>\n  <dd>\nThe \"synced\" event is fired when the connection to the database has been established\nand all available content has been loaded. The event is also fired if no content\nis found for the given doc name.\n  </dd>\n  <b><code>provider.set(key: any, value: any): Promise&lt;any&gt;</code></b>\n  <dd>\nSet a custom property on the provider instance. You can use this to store relevant\nmeta-information for the persisted document. However, the content will not be\nsynced with other peers.\n  </dd>\n  <b><code>provider.get(key: any): Promise&gt;any&lt;</code></b>\n  <dd>\nRetrieve a stored value.\n  </dd>\n  <b><code>provider.del(key: any): Promise&gt;undefined&lt;</code></b>\n  <dd>\nDelete a stored value.\n  </dd>\n  <b><code>provider.destroy(): Promise</code></b>\n  <dd>\nClose the connection to the database and stop syncing the document. This method is\nautomatically called when the Yjs document is destroyed (e.g. ydoc.destroy()).\n  </dd>\n  <b><code>provider.clearData(): Promise</code></b>\n  <dd>\nDestroy this database and remove the stored document and all related meta-information\nfrom the database.\n  </dd>\n</dl>\n\n## License\n\nYjs is licensed under the [MIT License](./LICENSE).\n\n<kevin.jahns@protonmail.com>\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>Testing y-indexeddb</title>\n</head>\n<body>\n  <script type=\"module\" src=\"./dist/test.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"y-indexeddb\",\n  \"version\": \"9.0.12\",\n  \"description\": \"IndexedDB database adapter for Yjs\",\n  \"type\": \"module\",\n  \"main\": \"./dist/y-indexeddb.cjs\",\n  \"module\": \"./src/y-indexeddb.js\",\n  \"types\": \"./dist/src/y-indexeddb.d.ts\",\n  \"sideEffects\": false,\n  \"funding\": {\n    \"type\": \"GitHub Sponsors ❤\",\n    \"url\": \"https://github.com/sponsors/dmonad\"\n  },\n  \"scripts\": {\n    \"clean\": \"rm -rf dist\",\n    \"test\": \"npm run lint\",\n    \"dist\": \"rollup -c\",\n    \"lint\": \"markdownlint README.md && standard && tsc\",\n    \"preversion\": \"npm run clean && npm run lint && npm run dist\",\n    \"debug\": \"concurrently 'rollup -wc' 'http-server -o .'\"\n  },\n  \"files\": [\n    \"dist/*\",\n    \"src/*\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/src/y-indexeddb.d.ts\",\n      \"module\": \"./src/y-indexeddb.js\",\n      \"import\": \"./src/y-indexeddb.js\",\n      \"require\": \"./dist/y-indexeddb.cjs\",\n      \"default\": \"./src/y-indexeddb.js\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"standard\": {\n    \"ignore\": [\n      \"/dist\",\n      \"/node_modules\",\n      \"/docs\"\n    ]\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/yjs/y-indexeddb.git\"\n  },\n  \"keywords\": [\n    \"Yjs\",\n    \"CRDT\",\n    \"offline\",\n    \"shared editing\",\n    \"collaboration\",\n    \"concurrency\"\n  ],\n  \"author\": \"Kevin Jahns <kevin.jahns@protonmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/yjs/y-indexeddb/issues\"\n  },\n  \"homepage\": \"https://yjs.dev\",\n  \"dependencies\": {\n    \"lib0\": \"^0.2.74\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^11.1.0\",\n    \"@rollup/plugin-node-resolve\": \"^7.1.3\",\n    \"concurrently\": \"^3.6.1\",\n    \"http-server\": \"^0.12.3\",\n    \"jsdoc\": \"^3.6.6\",\n    \"markdownlint-cli\": \"^0.19.0\",\n    \"rollup\": \"^1.32.1\",\n    \"standard\": \"^11.0.1\",\n    \"typescript\": \"^5.0.4\",\n    \"y-protocols\": \"^1.0.1\",\n    \"yjs\": \"^13.4.7\"\n  },\n  \"peerDependencies\": {\n    \"yjs\": \"^13.0.0\"\n  },\n  \"engines\": {\n    \"npm\": \">=8.0.0\",\n    \"node\": \">=16.0.0\"\n  }\n}\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import resolve from '@rollup/plugin-node-resolve'\nimport commonjs from '@rollup/plugin-commonjs'\n\nexport default [{\n  input: './tests/index.js',\n  output: {\n    file: './dist/test.js',\n    format: 'iife',\n    sourcemap: true\n  },\n  plugins: [\n    resolve({ mainFields: ['module', 'browser', 'main'] }),\n    commonjs()\n  ]\n}, {\n  input: './src/y-indexeddb.js',\n  output: {\n    name: 'Y',\n    file: 'dist/y-indexeddb.cjs',\n    format: 'cjs',\n    sourcemap: true\n  },\n  external: id => /^(lib0|yjs)\\//.test(id)\n}]\n"
  },
  {
    "path": "src/y-indexeddb.js",
    "content": "import * as Y from 'yjs'\nimport * as idb from 'lib0/indexeddb'\nimport * as promise from 'lib0/promise'\nimport { Observable } from 'lib0/observable'\n\nconst customStoreName = 'custom'\nconst updatesStoreName = 'updates'\n\nexport const PREFERRED_TRIM_SIZE = 500\n\n/**\n * @param {IndexeddbPersistence} idbPersistence\n * @param {function(IDBObjectStore):void} [beforeApplyUpdatesCallback]\n * @param {function(IDBObjectStore):void} [afterApplyUpdatesCallback]\n */\nexport const fetchUpdates = (idbPersistence, beforeApplyUpdatesCallback = () => {}, afterApplyUpdatesCallback = () => {}) => {\n  const [updatesStore] = idb.transact(/** @type {IDBDatabase} */ (idbPersistence.db), [updatesStoreName]) // , 'readonly')\n  return idb.getAll(updatesStore, idb.createIDBKeyRangeLowerBound(idbPersistence._dbref, false)).then(updates => {\n    if (!idbPersistence._destroyed) {\n      beforeApplyUpdatesCallback(updatesStore)\n      Y.transact(idbPersistence.doc, () => {\n        updates.forEach(val => Y.applyUpdate(idbPersistence.doc, val))\n      }, idbPersistence, false)\n      afterApplyUpdatesCallback(updatesStore)\n    }\n  })\n    .then(() => idb.getLastKey(updatesStore).then(lastKey => { idbPersistence._dbref = lastKey + 1 }))\n    .then(() => idb.count(updatesStore).then(cnt => { idbPersistence._dbsize = cnt }))\n    .then(() => updatesStore)\n}\n\n/**\n * @param {IndexeddbPersistence} idbPersistence\n * @param {boolean} forceStore\n */\nexport const storeState = (idbPersistence, forceStore = true) =>\n  fetchUpdates(idbPersistence)\n    .then(updatesStore => {\n      if (forceStore || idbPersistence._dbsize >= PREFERRED_TRIM_SIZE) {\n        idb.addAutoKey(updatesStore, Y.encodeStateAsUpdate(idbPersistence.doc))\n          .then(() => idb.del(updatesStore, idb.createIDBKeyRangeUpperBound(idbPersistence._dbref, true)))\n          .then(() => idb.count(updatesStore).then(cnt => { idbPersistence._dbsize = cnt }))\n      }\n    })\n\n/**\n * @param {string} name\n */\nexport const clearDocument = name => idb.deleteDB(name)\n\n/**\n * @extends Observable<string>\n */\nexport class IndexeddbPersistence extends Observable {\n  /**\n   * @param {string} name\n   * @param {Y.Doc} doc\n   */\n  constructor (name, doc) {\n    super()\n    this.doc = doc\n    this.name = name\n    this._dbref = 0\n    this._dbsize = 0\n    this._destroyed = false\n    /**\n     * @type {IDBDatabase|null}\n     */\n    this.db = null\n    this.synced = false\n    this._db = idb.openDB(name, db =>\n      idb.createStores(db, [\n        ['updates', { autoIncrement: true }],\n        ['custom']\n      ])\n    )\n    /**\n     * @type {Promise<IndexeddbPersistence>}\n     */\n    this.whenSynced = promise.create(resolve => this.on('synced', () => resolve(this)))\n\n    this._db.then(db => {\n      this.db = db\n      /**\n       * @param {IDBObjectStore} updatesStore\n       */\n      const beforeApplyUpdatesCallback = (updatesStore) => idb.addAutoKey(updatesStore, Y.encodeStateAsUpdate(doc))\n      const afterApplyUpdatesCallback = () => {\n        if (this._destroyed) return this\n        this.synced = true\n        this.emit('synced', [this])\n      }\n      fetchUpdates(this, beforeApplyUpdatesCallback, afterApplyUpdatesCallback)\n    })\n    /**\n     * Timeout in ms until data is merged and persisted in idb.\n     */\n    this._storeTimeout = 1000\n    /**\n     * @type {any}\n     */\n    this._storeTimeoutId = null\n    /**\n     * @param {Uint8Array} update\n     * @param {any} origin\n     */\n    this._storeUpdate = (update, origin) => {\n      if (this.db && origin !== this) {\n        const [updatesStore] = idb.transact(/** @type {IDBDatabase} */ (this.db), [updatesStoreName])\n        idb.addAutoKey(updatesStore, update)\n        if (++this._dbsize >= PREFERRED_TRIM_SIZE) {\n          // debounce store call\n          if (this._storeTimeoutId !== null) {\n            clearTimeout(this._storeTimeoutId)\n          }\n          this._storeTimeoutId = setTimeout(() => {\n            storeState(this, false)\n            this._storeTimeoutId = null\n          }, this._storeTimeout)\n        }\n      }\n    }\n    doc.on('update', this._storeUpdate)\n    this.destroy = this.destroy.bind(this)\n    doc.on('destroy', this.destroy)\n  }\n\n  destroy () {\n    if (this._storeTimeoutId) {\n      clearTimeout(this._storeTimeoutId)\n    }\n    this.doc.off('update', this._storeUpdate)\n    this.doc.off('destroy', this.destroy)\n    this._destroyed = true\n    return this._db.then(db => {\n      db.close()\n    })\n  }\n\n  /**\n   * Destroys this instance and removes all data from indexeddb.\n   *\n   * @return {Promise<void>}\n   */\n  clearData () {\n    return this.destroy().then(() => {\n      idb.deleteDB(this.name)\n    })\n  }\n\n  /**\n   * @param {String | number | ArrayBuffer | Date} key\n   * @return {Promise<String | number | ArrayBuffer | Date | any>}\n   */\n  get (key) {\n    return this._db.then(db => {\n      const [custom] = idb.transact(db, [customStoreName], 'readonly')\n      return idb.get(custom, key)\n    })\n  }\n\n  /**\n   * @param {String | number | ArrayBuffer | Date} key\n   * @param {String | number | ArrayBuffer | Date} value\n   * @return {Promise<String | number | ArrayBuffer | Date>}\n   */\n  set (key, value) {\n    return this._db.then(db => {\n      const [custom] = idb.transact(db, [customStoreName])\n      return idb.put(custom, value, key)\n    })\n  }\n\n  /**\n   * @param {String | number | ArrayBuffer | Date} key\n   * @return {Promise<undefined>}\n   */\n  del (key) {\n    return this._db.then(db => {\n      const [custom] = idb.transact(db, [customStoreName])\n      return idb.del(custom, key)\n    })\n  }\n}\n"
  },
  {
    "path": "tests/index.js",
    "content": "\nimport * as indexeddb from './y-indexeddb.tests.js'\n\nimport { runTests } from 'lib0/testing.js'\nimport { isBrowser, isNode } from 'lib0/environment.js'\nimport * as log from 'lib0/logging.js'\n\nif (isBrowser) {\n  log.createVConsole(document.body)\n}\nrunTests({\n  indexeddb\n}).then(success => {\n  /* istanbul ignore next */\n  if (isNode) {\n    process.exit(success ? 0 : 1)\n  }\n})\n"
  },
  {
    "path": "tests/y-indexeddb.tests.js",
    "content": "\nimport * as Y from 'yjs'\nimport { IndexeddbPersistence, clearDocument, PREFERRED_TRIM_SIZE, fetchUpdates } from '../src/y-indexeddb.js'\nimport * as t from 'lib0/testing.js'\nimport * as promise from 'lib0/promise.js'\n\n/**\n * @param {t.TestCase} tc\n */\nexport const testPerf = async tc => {\n  await t.measureTimeAsync('time to create a y-indexeddb instance', async () => {\n    const ydoc = new Y.Doc()\n    const provider = new IndexeddbPersistence(tc.testName, ydoc)\n    await provider.whenSynced\n    provider.destroy()\n  })\n}\n\n/**\n * @param {t.TestCase} tc\n */\nexport const testIdbUpdateAndMerge = async tc => {\n  await clearDocument(tc.testName)\n  const doc1 = new Y.Doc()\n  const arr1 = doc1.getArray('t')\n  const doc2 = new Y.Doc()\n  const arr2 = doc2.getArray('t')\n  arr1.insert(0, [0])\n  const persistence1 = new IndexeddbPersistence(tc.testName, doc1)\n  persistence1._storeTimeout = 0\n  await persistence1.whenSynced\n  arr1.insert(0, [1])\n  const persistence2 = new IndexeddbPersistence(tc.testName, doc2)\n  persistence2._storeTimeout = 0\n  let calledObserver = false\n  // @ts-ignore\n  arr2.observe((event, tr) => {\n    t.assert(!tr.local)\n    t.assert(tr.origin === persistence2)\n    calledObserver = true\n  })\n  await persistence2.whenSynced\n  t.assert(calledObserver)\n  t.assert(arr2.length === 2)\n  for (let i = 2; i < PREFERRED_TRIM_SIZE + 1; i++) {\n    arr1.insert(i, [i])\n  }\n  await promise.wait(100)\n  await fetchUpdates(persistence2)\n  t.assert(arr2.length === PREFERRED_TRIM_SIZE + 1)\n  t.assert(persistence1._dbsize === 1) // wait for dbsize === 0. db should be concatenated\n}\n\n/**\n * @param {t.TestCase} tc\n */\nexport const testIdbConcurrentMerge = async tc => {\n  await clearDocument(tc.testName)\n  const doc1 = new Y.Doc()\n  const arr1 = doc1.getArray('t')\n  const doc2 = new Y.Doc()\n  const arr2 = doc2.getArray('t')\n  arr1.insert(0, [0])\n  const persistence1 = new IndexeddbPersistence(tc.testName, doc1)\n  persistence1._storeTimeout = 0\n  await persistence1.whenSynced\n  arr1.insert(0, [1])\n  const persistence2 = new IndexeddbPersistence(tc.testName, doc2)\n  persistence2._storeTimeout = 0\n  await persistence2.whenSynced\n  t.assert(arr2.length === 2)\n  arr1.insert(0, ['left'])\n  for (let i = 0; i < PREFERRED_TRIM_SIZE + 1; i++) {\n    arr1.insert(i, [i])\n  }\n  arr2.insert(0, ['right'])\n  for (let i = 0; i < PREFERRED_TRIM_SIZE + 1; i++) {\n    arr2.insert(i, [i])\n  }\n  await promise.wait(100)\n  await fetchUpdates(persistence1)\n  await fetchUpdates(persistence2)\n  t.assert(persistence1._dbsize < 10)\n  t.assert(persistence2._dbsize < 10)\n  t.compareArrays(arr1.toArray(), arr2.toArray())\n}\n\n/**\n * @param {t.TestCase} tc\n */\nexport const testMetaStorage = async tc => {\n  await clearDocument(tc.testName)\n  const ydoc = new Y.Doc()\n  const persistence = new IndexeddbPersistence(tc.testName, ydoc)\n  persistence.set('a', 4)\n  persistence.set(4, 'meta!')\n  // @ts-ignore\n  persistence.set('obj', { a: 4 })\n  const resA = await persistence.get('a')\n  t.assert(resA === 4)\n  const resB = await persistence.get(4)\n  t.assert(resB === 'meta!')\n  const resC = await persistence.get('obj')\n  t.compareObjects(resC, { a: 4 })\n}\n\n/**\n * @param {t.TestCase} tc\n */\nexport const testEarlyDestroy = async tc => {\n  let hasbeenSyced = false\n  const ydoc = new Y.Doc()\n  const indexDBProvider = new IndexeddbPersistence(tc.testName, ydoc)\n  indexDBProvider.on('synced', () => {\n    hasbeenSyced = true\n  })\n  indexDBProvider.destroy()\n  await new Promise((resolve) => setTimeout(resolve, 500))\n  t.assert(!hasbeenSyced)\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2018\",\n    \"lib\": [\"es2018\", \"dom\"],\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"emitDeclarationOnly\": true,\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"moduleResolution\": \"node\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src/**/*\", \"./tests/**/*\"],\n  \"exclude\": [\"../lib0/**/*\", \"node_modules/**/*\", \"dist\", \"dist/**/*.js\"]\n}\n"
  }
]