diff --git a/docs/404.html b/docs/404.html index 32102a7283b..e211f8d6874 100644 --- a/docs/404.html +++ b/docs/404.html @@ -12,8 +12,8 @@ - - + +
Skip to main content

RxDB
404 Page Not Found

The page you are looking for does not exist anymore or never has existed. If you have found this page through a link, you should tell the link author to update it.

Maybe one of these can help you to find the desired content:

diff --git a/docs/adapters.html b/docs/adapters.html index a0d284ac93e..134c537d2f3 100644 --- a/docs/adapters.html +++ b/docs/adapters.html @@ -12,8 +12,8 @@ - - + +
Skip to main content

PouchDB Adapters

@@ -21,8 +21,7 @@ Depending on which environment you work in, you can choose between different adapters. For example, in the browser you want to store the data inside of IndexedDB but on NodeJS you want to store the data on the filesystem.

This page is an overview over the different adapters with recommendations on what to use where.


-

IMPORTANT:

-

The PouchDB RxStorage is removed from RxDB and can no longer be used in new projects. You should switch to a different RxStorage.

+
warning

The PouchDB RxStorage is removed from RxDB and can no longer be used in new projects. You should switch to a different RxStorage.


Please always ensure that your pouchdb adapter-version is the same as pouchdb-core in the rxdb package.json. Otherwise, you might have strange problems.

Any environment

@@ -43,7 +42,7 @@

IndexedDB
// npm install pouchdb-adapter-idb --save
addPouchPlugin(require('pouchdb-adapter-idb'));

const database = await createRxDatabase({
name: 'mydatabase',
storage: getRxStoragePouch('idb')
});

IndexedDB

A reimplementation of the indexeddb adapter which uses native secondary indexes. Should have a much better performance but can behave different on some edge cases.

-

Notice: Multiple users have reported problems with this adapter. It is not recommended to use this adapter.

+
note

Multiple users have reported problems with this adapter. It is not recommended to use this adapter.

// npm install pouchdb-adapter-indexeddb --save
addPouchPlugin(require('pouchdb-adapter-indexeddb'));

const database = await createRxDatabase({
name: 'mydatabase',
storage: getRxStoragePouch('indexeddb')
});

Websql

This adapter stores the data inside of websql. It has a different performance behavior. Websql is deprecated. You should not use the websql adapter unless you have a really good reason.

@@ -70,7 +69,7 @@

react-na
import { createRxDatabase } from 'rxdb';
import { addPouchPlugin, getRxStoragePouch } from 'rxdb/plugins/pouchdb';
import SQLite from 'react-native-sqlite-2'
import SQLiteAdapterFactory from 'pouchdb-adapter-react-native-sqlite'

const SQLiteAdapter = SQLiteAdapterFactory(SQLite)

addPouchPlugin(SQLiteAdapter);
addPouchPlugin(require('pouchdb-adapter-http'));

const database = await createRxDatabase({
name: 'mydatabase',
storage: getRxStoragePouch('react-native-sqlite') // the name of your adapter
});

asyncstorage

Uses react-native's asyncstorage.

-

Notice: There are known problems with this adapter and it is not recommended to use it.

+
note

There are known problems with this adapter and it is not recommended to use it.

// npm install pouchdb-adapter-asyncstorage --save
addPouchPlugin(require('pouchdb-adapter-asyncstorage'));

const database = await createRxDatabase({
name: 'mydatabase',
storage: getRxStoragePouch('node-asyncstorage') // the name of your adapter
});

asyncstorage-down

A leveldown adapter that stores on asyncstorage.

@@ -78,6 +77,6 @@

asyncstora

Cordova / Phonegap / Capacitor

cordova-sqlite

Uses cordova's global cordova.sqlitePlugin. It can be used with cordova and capacitor.

-
// npm install pouchdb-adapter-cordova-sqlite --save
addPouchPlugin(require('pouchdb-adapter-cordova-sqlite'));

/**
* In capacitor/cordova you have to wait until all plugins are loaded and 'window.sqlitePlugin'
* can be accessed.
* This function waits until document deviceready is called which ensures that everything is loaded.
* @link https://cordova.apache.org/docs/de/latest/cordova/events/events.deviceready.html
*/
export function awaitCapacitorDeviceReady(): Promise<void> {
return new Promise(res => {
document.addEventListener('deviceready', () => {
res();
});
});
}

async function getDatabase(){

// first wait until the deviceready event is fired
await awaitCapacitorDeviceReady();

const database = await createRxDatabase({
name: 'mydatabase',
storage: getRxStoragePouch(
'cordova-sqlite',
// pouch settings are passed as second parameter
{
// for ios devices, the cordova-sqlite adapter needs to know where to save the data.
iosDatabaseLocation: 'Library'
}
)
});
}
+
// npm install pouchdb-adapter-cordova-sqlite --save
addPouchPlugin(require('pouchdb-adapter-cordova-sqlite'));

/**
* In capacitor/cordova you have to wait until all plugins are loaded and 'window.sqlitePlugin'
* can be accessed.
* This function waits until document deviceready is called which ensures that everything is loaded.
* @link https://cordova.apache.org/docs/de/latest/cordova/events/events.deviceready.html
*/
export function awaitCapacitorDeviceReady(): Promise<void> {
return new Promise(res => {
document.addEventListener('deviceready', () => {
res();
});
});
}

async function getDatabase(){

// first wait until the deviceready event is fired
await awaitCapacitorDeviceReady();

const database = await createRxDatabase({
name: 'mydatabase',
storage: getRxStoragePouch(
'cordova-sqlite',
// pouch settings are passed as second parameter
{
// for ios devices, the cordova-sqlite adapter needs to know where to save the data.
iosDatabaseLocation: 'Library'
}
)
});
}
\ No newline at end of file diff --git a/docs/alternatives.html b/docs/alternatives.html index 565bebeac2f..e6c964002b1 100644 --- a/docs/alternatives.html +++ b/docs/alternatives.html @@ -12,8 +12,8 @@ - - + +
Skip to main content

Alternatives for realtime offline-first JavaScript applications

diff --git a/docs/articles/angular-database.html b/docs/articles/angular-database.html index 5912c865fe0..02e3d35b08c 100644 --- a/docs/articles/angular-database.html +++ b/docs/articles/angular-database.html @@ -12,8 +12,8 @@ - - + +

RxDB as a Database in an Angular Application

@@ -53,7 +53,9 @@

Patch Change Detection with zone.js

Angular uses change detection to detect and update UI elements when data changes. However, RxDB's data handling is based on observables, which can sometimes bypass Angular's change detection mechanism. To ensure that changes made in RxDB are detected by Angular, we need to patch the change detection mechanism using zone.js. Zone.js is a library that intercepts and tracks asynchronous operations, including observables. By patching zone.js, we can make sure that Angular is aware of changes happening in RxDB.

-
//> app.component.ts
/**
* IMPORTANT: RxDB creates rxjs observables outside of angulars zone
* So you have to import the rxjs patch to ensure changedetection works correctly.
* @link https://www.bennadel.com/blog/3448-binding-rxjs-observable-sources-outside-of-the-ngzone-in-angular-6-0-2.htm
*/
import 'zone.js/plugins/zone-patch-rxjs';
+
warning

RxDB creates rxjs observables outside of angulars zone +So you have to import the rxjs patch to ensure the angular change detection works correctly. +link

//> app.component.ts
import 'zone.js/plugins/zone-patch-rxjs';

Use the Angular async pipe to observe an RxDB Query

Angular provides the async pipe, which is a convenient way to subscribe to observables and handle the subscription lifecycle automatically. When working with RxDB, you can use the async pipe to observe an RxDB query and bind the results directly to your Angular template. This ensures that the UI stays in sync with the data changes emitted by the RxDB query.

    constructor(
private dbService: DatabaseService,
private dialog: MatDialog
) {
this.heroes$ = this.dbService
.db.hero // collection
.find({ // query
selector: {},
sort: [{ name: 'asc' }]
})
.$;
}
diff --git a/docs/articles/browser-database.html b/docs/articles/browser-database.html index 5285a691fa6..06f1c61326c 100644 --- a/docs/articles/browser-database.html +++ b/docs/articles/browser-database.html @@ -12,8 +12,8 @@ - - + +

RxDB: The benefits of Browser Databases

diff --git a/docs/articles/browser-storage.html b/docs/articles/browser-storage.html index 65b825a5886..dff5dec7c9b 100644 --- a/docs/articles/browser-storage.html +++ b/docs/articles/browser-storage.html @@ -12,8 +12,8 @@ - - + +

Browser Storage - RxDB as a Database for Browsers

diff --git a/docs/articles/data-base.html b/docs/articles/data-base.html index 00e976e4366..af536558d38 100644 --- a/docs/articles/data-base.html +++ b/docs/articles/data-base.html @@ -12,8 +12,8 @@ - - + +

RxDB as a data base: Empowering Web Applications with Reactive Data Handling

diff --git a/docs/articles/embedded-database.html b/docs/articles/embedded-database.html index a30a3ee614d..c04d33712d2 100644 --- a/docs/articles/embedded-database.html +++ b/docs/articles/embedded-database.html @@ -12,8 +12,8 @@ - - + +

Using RxDB as an Embedded Database

diff --git a/docs/articles/flutter-database.html b/docs/articles/flutter-database.html index d931f638697..e70e7133bbb 100644 --- a/docs/articles/flutter-database.html +++ b/docs/articles/flutter-database.html @@ -12,13 +12,13 @@ - - + +

RxDB as a Database in a Flutter Application

In the world of mobile application development, Flutter has gained significant popularity due to its cross-platform capabilities and rich UI framework. When it comes to building feature-rich Flutter applications, the choice of a robust and efficient database is crucial. In this article, we will explore RxDB as a database solution for Flutter applications. We'll delve into the core features of RxDB, its benefits over other database options, and how to integrate it into a Flutter app.

-

NOTICE: You can find the source code for an example RxDB Flutter Application at the github repo

+
note

You can find the source code for an example RxDB Flutter Application at the github repo

RxDB Flutter Database

Overview of Flutter Mobile Applications

Flutter is an open-source UI software development kit created by Google that allows developers to build high-performance mobile applications for iOS and Android platforms using a single codebase. Flutter's framework provides a wide range of widgets and tools that enable developers to create visually appealing and responsive applications.

@@ -81,6 +81,6 @@

JSON Ke

To minimize storage requirements and optimize performance, RxDB offers JSON key compression. This feature reduces the size of keys used in the database, resulting in more efficient storage and improved query performance.

Conclusion

RxDB offers a powerful and flexible database solution for Flutter applications. With its offline-first approach, real-time data synchronization, and reactive data handling capabilities, RxDB simplifies the development of feature-rich and scalable Flutter applications. By integrating RxDB into your Flutter projects, you can leverage its advanced features and techniques to build responsive and data-driven applications that provide an exceptional user experience.

-

NOTICE: You can find the source code for an example RxDB Flutter Application at the github repo

+
note

You can find the source code for an example RxDB Flutter Application at the github repo

\ No newline at end of file diff --git a/docs/articles/frontend-database.html b/docs/articles/frontend-database.html index f8e94912944..40f2c4376da 100644 --- a/docs/articles/frontend-database.html +++ b/docs/articles/frontend-database.html @@ -12,8 +12,8 @@ - - + +

RxDB JavaScript Frontend Database: Efficient Data Storage in Frontend Applications

diff --git a/docs/articles/in-memory-nosql-database.html b/docs/articles/in-memory-nosql-database.html index 52396823c8f..0484a734cfe 100644 --- a/docs/articles/in-memory-nosql-database.html +++ b/docs/articles/in-memory-nosql-database.html @@ -12,8 +12,8 @@ - - + +

RxDB as In-memory NoSQL Database: Empowering Real-Time Applications

diff --git a/docs/articles/ionic-database.html b/docs/articles/ionic-database.html index c90800cc88d..1b770502531 100644 --- a/docs/articles/ionic-database.html +++ b/docs/articles/ionic-database.html @@ -12,8 +12,8 @@ - - + +

Ionic Storage - RxDB as database for hybrid apps

diff --git a/docs/articles/json-database.html b/docs/articles/json-database.html index 9b6e96405ec..758f12f32ce 100644 --- a/docs/articles/json-database.html +++ b/docs/articles/json-database.html @@ -12,8 +12,8 @@ - - + +

RxDB - JSON Database for JavaScript

diff --git a/docs/articles/localstorage.html b/docs/articles/localstorage.html index 768a7957ee1..b35ed2c9569 100644 --- a/docs/articles/localstorage.html +++ b/docs/articles/localstorage.html @@ -12,8 +12,8 @@ - - + +

Using localStorage in Modern Applications: A Comprehensive Guide

diff --git a/docs/articles/mobile-database.html b/docs/articles/mobile-database.html index 99dfa65a77f..f7ec9b7dc25 100644 --- a/docs/articles/mobile-database.html +++ b/docs/articles/mobile-database.html @@ -12,8 +12,8 @@ - - + +

Mobile Database - RxDB as Database for Mobile Applications

diff --git a/docs/articles/progressive-web-app-database.html b/docs/articles/progressive-web-app-database.html index 5474fd23f15..4624de1ddd7 100644 --- a/docs/articles/progressive-web-app-database.html +++ b/docs/articles/progressive-web-app-database.html @@ -12,8 +12,8 @@ - - + +

RxDB as a Database for Progressive Web Apps (PWA)

diff --git a/docs/articles/react-database.html b/docs/articles/react-database.html index e152a208b9a..5c017824cba 100644 --- a/docs/articles/react-database.html +++ b/docs/articles/react-database.html @@ -12,8 +12,8 @@ - - + +

RxDB as a Database for React Applications

diff --git a/docs/articles/realtime-database.html b/docs/articles/realtime-database.html index c1c63e0197e..18e7c3011d4 100644 --- a/docs/articles/realtime-database.html +++ b/docs/articles/realtime-database.html @@ -12,8 +12,8 @@ - - + +

What is a realtime database?

diff --git a/docs/assets/js/0027230a.a0a9db8b.js b/docs/assets/js/0027230a.a0a9db8b.js deleted file mode 100644 index 9ac2ee6f499..00000000000 --- a/docs/assets/js/0027230a.a0a9db8b.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[8382],{2081:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>c,frontMatter:()=>o,metadata:()=>r,toc:()=>d});var n=a(4848),s=a(8453);const o={title:"LokiJS RxStorage",slug:"rx-storage-lokijs.html"},i="RxStorage LokiJS",r={id:"rx-storage-lokijs",title:"LokiJS RxStorage",description:"The LokiJS RxStorage is based on LokiJS which is an in-memory database that processes all data in memory and only saves to disc when the app is closed or an interval is reached. This makes it very fast but you have the possibility to lose semingly persisted writes when the JavaScript process ends before the persistence loop has been done.",source:"@site/docs/rx-storage-lokijs.md",sourceDirName:".",slug:"/rx-storage-lokijs.html",permalink:"/rx-storage-lokijs.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"LokiJS RxStorage",slug:"rx-storage-lokijs.html"},sidebar:"tutorialSidebar",previous:{title:"FoundationDB RxStorage",permalink:"/rx-storage-foundationdb.html"},next:{title:"Remote RxStorage",permalink:"/rx-storage-remote.html"}},l={},d=[{value:"Pros",id:"pros",level:3},{value:"Cons",id:"cons",level:3},{value:"Usage",id:"usage",level:2},{value:"Adapters",id:"adapters",level:2},{value:"Multi-Tab support",id:"multi-tab-support",level:2},{value:"Autosave and autoload",id:"autosave-and-autoload",level:2},{value:"Known problems",id:"known-problems",level:2},{value:"Using the internal LokiJS database",id:"using-the-internal-lokijs-database",level:2}];function h(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"rxstorage-lokijs",children:"RxStorage LokiJS"}),"\n",(0,n.jsxs)(t.p,{children:["The LokiJS RxStorage is based on ",(0,n.jsx)(t.a,{href:"https://github.com/techfort/LokiJS",children:"LokiJS"})," which is an ",(0,n.jsx)(t.strong,{children:"in-memory"})," database that processes all data in memory and only saves to disc when the app is closed or an interval is reached. This makes it very fast but you have the possibility to lose semingly persisted writes when the JavaScript process ends before the persistence loop has been done."]}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.strong,{children:"WARNING:"})," The LokiJS project itself is ",(0,n.jsx)(t.a,{href:"https://github.com/techfort/LokiJS/issues/917",children:"no longer"})," in development or maintained. There are known bugs like having wrong query results of lossing data. LokiJS bugs that occur outside of the RxDB layer will not be fixed and the LokiJS RxStorage might get deprecated soon. Using LokiJS as storage is only recommended for proof-of-concepts or quick sample apps. In production it is recommended to use another ",(0,n.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"})," instead. For browsers better use the ",(0,n.jsx)(t.a,{href:"/rx-storage-dexie.html",children:"Dexie.js"})," or ",(0,n.jsx)(t.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB"})," storage. For fast lazy persistend in memory data you can use the ",(0,n.jsx)(t.a,{href:"/rx-storage-memory-synced.html",children:"Memory Synced"})," storage."]}),"\n",(0,n.jsx)(t.h3,{id:"pros",children:"Pros"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"Queries can run faster because all data is processed in memory."}),"\n",(0,n.jsx)(t.li,{children:"It has a much faster initial load time because it loads all data from IndexedDB in a single request. But this is only true for small datasets. If much data must is stored, the initial load time can be higher than on other RxStorage implementations."}),"\n"]}),"\n",(0,n.jsx)(t.h3,{id:"cons",children:"Cons"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"It does not support attachments."}),"\n",(0,n.jsx)(t.li,{children:"Data can be lost when the JavaScript process is killed ungracefully like when the browser crashes or the power of the PC is terminated."}),"\n",(0,n.jsx)(t.li,{children:"All data must fit into the memory."}),"\n",(0,n.jsxs)(t.li,{children:["Slow initialisation time when used with ",(0,n.jsx)(t.code,{children:"multiInstance: true"})," because it has to await the leader election process."]}),"\n",(0,n.jsxs)(t.li,{children:["Slow initialisation time when really much data is stored inside of the database because it has to parse a big ",(0,n.jsx)(t.code,{children:"JSON"})," string."]}),"\n"]}),"\n",(0,n.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport {\n getRxStorageLoki\n} from 'rxdb/plugins/storage-lokijs';\n\n// in the browser, we want to persist data in IndexedDB, so we use the indexeddb adapter.\nconst LokiIncrementalIndexedDBAdapter = require('lokijs/src/incremental-indexeddb-adapter');\n\nconst db = await createRxDatabase({\n name: 'exampledb',\n storage: getRxStorageLoki({\n adapter: new LokiIncrementalIndexedDBAdapter(),\n /* \n * Do not set lokiJS persistence options like autoload and autosave,\n * RxDB will pick proper defaults based on the given adapter\n */\n })\n});\n"})}),"\n",(0,n.jsx)(t.h2,{id:"adapters",children:"Adapters"}),"\n",(0,n.jsxs)(t.p,{children:["LokiJS is based on adapters that determine where to store persistent data. For LokiJS there are adapters for IndexedDB, AWS S3, the NodeJS filesystem or NativeScript.\nFind more about the possible adapters at the ",(0,n.jsx)(t.a,{href:"https://github.com/techfort/LokiJS/blob/master/tutorials/Persistence%20Adapters.md",children:"LokiJS docs"}),"."]}),"\n",(0,n.jsx)(t.h2,{id:"multi-tab-support",children:"Multi-Tab support"}),"\n",(0,n.jsxs)(t.p,{children:["When you use plain LokiJS, you cannot build an app that can be used in multiple browser tabs. The reason is that LokiJS loads data in bulk and then only regularly persists the in-memory state to disc. When opened in multiple tabs, it would happen that the LokiJS instances overwrite each other and data is lost.\nWith the RxDB LokiJS-plugin, this problem is fixed with the ",(0,n.jsx)(t.a,{href:"https://github.com/pubkey/broadcast-channel#using-the-leaderelection",children:"LeaderElection"})," module. Between all open tabs, a leading tab is elected and only in this tab a database is created. All other tabs do not run queries against their own database, but instead call the leading tab to send and retrieve data. When the leading tab is closed, a new leader is elected that reopens the database and processes queries. You can disable this by setting ",(0,n.jsx)(t.code,{children:"multiInstance: false"})," when creating the ",(0,n.jsx)(t.code,{children:"RxDatabase"}),"."]}),"\n",(0,n.jsx)(t.h2,{id:"autosave-and-autoload",children:"Autosave and autoload"}),"\n",(0,n.jsxs)(t.p,{children:["When using plain LokiJS, you could set the ",(0,n.jsx)(t.code,{children:"autosave"})," option to ",(0,n.jsx)(t.code,{children:"true"})," to make sure that LokiJS persists the database state after each write into the persistence adapter. Same goes to ",(0,n.jsx)(t.code,{children:"autoload"})," which loads the persisted state on database creation.\nBut RxDB knows better when to persist the database state and when to load it, so it has its own autosave logic. This will ensure that running the persistence handler does not affect the performance of more important tasks. Instead RxDB will always wait until the database is idle and then runs the persistence handler.\nA load of the persisted state is done on database or collection creation and it is ensured that multiple load calls do not run in parallel and interfere with each other or with ",(0,n.jsx)(t.code,{children:"saveDatabase()"})," calls."]}),"\n",(0,n.jsx)(t.h2,{id:"known-problems",children:"Known problems"}),"\n",(0,n.jsxs)(t.p,{children:["When you bundle the LokiJS Plugin with webpack, you might get the error ",(0,n.jsx)(t.code,{children:'Cannot find module "fs"'}),". This is because LokiJS uses a ",(0,n.jsx)(t.code,{children:"require('fs')"})," statement that cannot work in the browser.\nYou can fix that by telling webpack to not resolve the ",(0,n.jsx)(t.code,{children:"fs"})," module with the following block in your webpack config:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-js",children:'// in your webpack.config.js\n{\n /* ... */\n resolve: {\n fallback: {\n fs: false\n }\n }\n /* ... */\n}\n\n// Or if you do not have a webpack.config.js like you do with angular,\n// you might fix it by setting the browser field in the package.json\n{\n /* ... */\n "browser": {\n "fs": false\n }\n /* ... */\n}\n\n'})}),"\n",(0,n.jsx)(t.h2,{id:"using-the-internal-lokijs-database",children:"Using the internal LokiJS database"}),"\n",(0,n.jsx)(t.p,{children:"For custom operations, you can access the internal LokiJS database.\nThis is dangerous because you might do changes that are not compatible with RxDB.\nOnly use this when there is no way to achieve your goals via the RxDB API."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-javascript",children:"\nconst storageInstance = myRxCollection.storageInstance;\nconst localState = await storageInstance.internals.localState;\nlocalState.collection.insert({\n key: 'foo',\n value: 'bar',\n _deleted: false,\n _attachments: {},\n _rev: '1-62080c42d471e3d2625e49dcca3b8e3e',\n _meta: {\n lwt: new Date().getTime()\n }\n});\n\n// manually trigger the save queue because we did a write to the internal loki db. \nawait localState.databaseState.saveQueue.addWrite();\n"})})]})}function c(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}},8453:(e,t,a)=>{a.d(t,{R:()=>i,x:()=>r});var n=a(6540);const s={},o=n.createContext(s);function i(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/0027230a.e4ea0c9b.js b/docs/assets/js/0027230a.e4ea0c9b.js new file mode 100644 index 00000000000..420c1ceb0bc --- /dev/null +++ b/docs/assets/js/0027230a.e4ea0c9b.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[8382],{2081:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>c,frontMatter:()=>o,metadata:()=>r,toc:()=>d});var n=a(4848),s=a(8453);const o={title:"LokiJS RxStorage",slug:"rx-storage-lokijs.html"},i="RxStorage LokiJS",r={id:"rx-storage-lokijs",title:"LokiJS RxStorage",description:"The LokiJS RxStorage is based on LokiJS which is an in-memory database that processes all data in memory and only saves to disc when the app is closed or an interval is reached. This makes it very fast but you have the possibility to lose semingly persisted writes when the JavaScript process ends before the persistence loop has been done.",source:"@site/docs/rx-storage-lokijs.md",sourceDirName:".",slug:"/rx-storage-lokijs.html",permalink:"/rx-storage-lokijs.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"LokiJS RxStorage",slug:"rx-storage-lokijs.html"},sidebar:"tutorialSidebar",previous:{title:"FoundationDB RxStorage",permalink:"/rx-storage-foundationdb.html"},next:{title:"Remote RxStorage",permalink:"/rx-storage-remote.html"}},l={},d=[{value:"Pros",id:"pros",level:3},{value:"Cons",id:"cons",level:3},{value:"Usage",id:"usage",level:2},{value:"Adapters",id:"adapters",level:2},{value:"Multi-Tab support",id:"multi-tab-support",level:2},{value:"Autosave and autoload",id:"autosave-and-autoload",level:2},{value:"Known problems",id:"known-problems",level:2},{value:"Using the internal LokiJS database",id:"using-the-internal-lokijs-database",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"rxstorage-lokijs",children:"RxStorage LokiJS"}),"\n",(0,n.jsxs)(t.p,{children:["The LokiJS RxStorage is based on ",(0,n.jsx)(t.a,{href:"https://github.com/techfort/LokiJS",children:"LokiJS"})," which is an ",(0,n.jsx)(t.strong,{children:"in-memory"})," database that processes all data in memory and only saves to disc when the app is closed or an interval is reached. This makes it very fast but you have the possibility to lose semingly persisted writes when the JavaScript process ends before the persistence loop has been done."]}),"\n",(0,n.jsx)(t.admonition,{type:"warning",children:(0,n.jsxs)(t.p,{children:["The LokiJS project itself is no longer in development or maintained. There are known bugs like having wrong query results of lossing data. LokiJS bugs that occur outside of the RxDB layer will not be fixed and the LokiJS RxStorage might get deprecated soon. Using LokiJS as storage is only recommended for proof-of-concepts or quick sample apps. In production it is recommended to use another ",(0,n.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"})," instead. For browsers better use the ",(0,n.jsx)(t.a,{href:"/rx-storage-dexie.html",children:"Dexie.js"})," or ",(0,n.jsx)(t.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB"})," storage. For fast lazy persistend in memory data you can use the ",(0,n.jsx)(t.a,{href:"/rx-storage-memory-synced.html",children:"Memory Synced"})," storage."]})}),"\n",(0,n.jsx)(t.h3,{id:"pros",children:"Pros"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"Queries can run faster because all data is processed in memory."}),"\n",(0,n.jsx)(t.li,{children:"It has a much faster initial load time because it loads all data from IndexedDB in a single request. But this is only true for small datasets. If much data must is stored, the initial load time can be higher than on other RxStorage implementations."}),"\n"]}),"\n",(0,n.jsx)(t.h3,{id:"cons",children:"Cons"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"It does not support attachments."}),"\n",(0,n.jsx)(t.li,{children:"Data can be lost when the JavaScript process is killed ungracefully like when the browser crashes or the power of the PC is terminated."}),"\n",(0,n.jsx)(t.li,{children:"All data must fit into the memory."}),"\n",(0,n.jsxs)(t.li,{children:["Slow initialisation time when used with ",(0,n.jsx)(t.code,{children:"multiInstance: true"})," because it has to await the leader election process."]}),"\n",(0,n.jsxs)(t.li,{children:["Slow initialisation time when really much data is stored inside of the database because it has to parse a big ",(0,n.jsx)(t.code,{children:"JSON"})," string."]}),"\n"]}),"\n",(0,n.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport {\n getRxStorageLoki\n} from 'rxdb/plugins/storage-lokijs';\n\n// in the browser, we want to persist data in IndexedDB, so we use the indexeddb adapter.\nconst LokiIncrementalIndexedDBAdapter = require('lokijs/src/incremental-indexeddb-adapter');\n\nconst db = await createRxDatabase({\n name: 'exampledb',\n storage: getRxStorageLoki({\n adapter: new LokiIncrementalIndexedDBAdapter(),\n /* \n * Do not set lokiJS persistence options like autoload and autosave,\n * RxDB will pick proper defaults based on the given adapter\n */\n })\n});\n"})}),"\n",(0,n.jsx)(t.h2,{id:"adapters",children:"Adapters"}),"\n",(0,n.jsxs)(t.p,{children:["LokiJS is based on adapters that determine where to store persistent data. For LokiJS there are adapters for IndexedDB, AWS S3, the NodeJS filesystem or NativeScript.\nFind more about the possible adapters at the ",(0,n.jsx)(t.a,{href:"https://github.com/techfort/LokiJS/blob/master/tutorials/Persistence%20Adapters.md",children:"LokiJS docs"}),"."]}),"\n",(0,n.jsx)(t.h2,{id:"multi-tab-support",children:"Multi-Tab support"}),"\n",(0,n.jsxs)(t.p,{children:["When you use plain LokiJS, you cannot build an app that can be used in multiple browser tabs. The reason is that LokiJS loads data in bulk and then only regularly persists the in-memory state to disc. When opened in multiple tabs, it would happen that the LokiJS instances overwrite each other and data is lost.\nWith the RxDB LokiJS-plugin, this problem is fixed with the ",(0,n.jsx)(t.a,{href:"https://github.com/pubkey/broadcast-channel#using-the-leaderelection",children:"LeaderElection"})," module. Between all open tabs, a leading tab is elected and only in this tab a database is created. All other tabs do not run queries against their own database, but instead call the leading tab to send and retrieve data. When the leading tab is closed, a new leader is elected that reopens the database and processes queries. You can disable this by setting ",(0,n.jsx)(t.code,{children:"multiInstance: false"})," when creating the ",(0,n.jsx)(t.code,{children:"RxDatabase"}),"."]}),"\n",(0,n.jsx)(t.h2,{id:"autosave-and-autoload",children:"Autosave and autoload"}),"\n",(0,n.jsxs)(t.p,{children:["When using plain LokiJS, you could set the ",(0,n.jsx)(t.code,{children:"autosave"})," option to ",(0,n.jsx)(t.code,{children:"true"})," to make sure that LokiJS persists the database state after each write into the persistence adapter. Same goes to ",(0,n.jsx)(t.code,{children:"autoload"})," which loads the persisted state on database creation.\nBut RxDB knows better when to persist the database state and when to load it, so it has its own autosave logic. This will ensure that running the persistence handler does not affect the performance of more important tasks. Instead RxDB will always wait until the database is idle and then runs the persistence handler.\nA load of the persisted state is done on database or collection creation and it is ensured that multiple load calls do not run in parallel and interfere with each other or with ",(0,n.jsx)(t.code,{children:"saveDatabase()"})," calls."]}),"\n",(0,n.jsx)(t.h2,{id:"known-problems",children:"Known problems"}),"\n",(0,n.jsxs)(t.p,{children:["When you bundle the LokiJS Plugin with webpack, you might get the error ",(0,n.jsx)(t.code,{children:'Cannot find module "fs"'}),". This is because LokiJS uses a ",(0,n.jsx)(t.code,{children:"require('fs')"})," statement that cannot work in the browser.\nYou can fix that by telling webpack to not resolve the ",(0,n.jsx)(t.code,{children:"fs"})," module with the following block in your webpack config:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-js",children:'// in your webpack.config.js\n{\n /* ... */\n resolve: {\n fallback: {\n fs: false\n }\n }\n /* ... */\n}\n\n// Or if you do not have a webpack.config.js like you do with angular,\n// you might fix it by setting the browser field in the package.json\n{\n /* ... */\n "browser": {\n "fs": false\n }\n /* ... */\n}\n\n'})}),"\n",(0,n.jsx)(t.h2,{id:"using-the-internal-lokijs-database",children:"Using the internal LokiJS database"}),"\n",(0,n.jsx)(t.p,{children:"For custom operations, you can access the internal LokiJS database.\nThis is dangerous because you might do changes that are not compatible with RxDB.\nOnly use this when there is no way to achieve your goals via the RxDB API."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-javascript",children:"\nconst storageInstance = myRxCollection.storageInstance;\nconst localState = await storageInstance.internals.localState;\nlocalState.collection.insert({\n key: 'foo',\n value: 'bar',\n _deleted: false,\n _attachments: {},\n _rev: '1-62080c42d471e3d2625e49dcca3b8e3e',\n _meta: {\n lwt: new Date().getTime()\n }\n});\n\n// manually trigger the save queue because we did a write to the internal loki db. \nawait localState.databaseState.saveQueue.addWrite();\n"})})]})}function c(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}},8453:(e,t,a)=>{a.d(t,{R:()=>i,x:()=>r});var n=a(6540);const s={},o=n.createContext(s);function i(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/01684a0a.20dd541a.js b/docs/assets/js/01684a0a.20dd541a.js new file mode 100644 index 00000000000..0c97f86818a --- /dev/null +++ b/docs/assets/js/01684a0a.20dd541a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6616],{3395:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>l});var a=n(4848),r=n(8453);const o={title:"Memory Synced RxStorage \ud83d\udc51",slug:"rx-storage-memory-synced.html"},s="Memory Synced RxStorage",i={id:"rx-storage-memory-synced",title:"Memory Synced RxStorage \ud83d\udc51",description:"The memory synced RxStorage is a wrapper around any other RxStorage. The wrapper creates an in-memory storage that is used for query and write operations. This memory instance is replicated with the underlying storage for persistence.",source:"@site/docs/rx-storage-memory-synced.md",sourceDirName:".",slug:"/rx-storage-memory-synced.html",permalink:"/rx-storage-memory-synced.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Memory Synced RxStorage \ud83d\udc51",slug:"rx-storage-memory-synced.html"},sidebar:"tutorialSidebar",previous:{title:"SharedWorker RxStorage \ud83d\udc51",permalink:"/rx-storage-shared-worker.html"},next:{title:"Sharding RxStorage \ud83d\udc51",permalink:"/rx-storage-sharding.html"}},d={},l=[{value:"Pros",id:"pros",level:2},{value:"Cons",id:"cons",level:2},{value:"Usage",id:"usage",level:2},{value:"Options",id:"options",level:2},{value:"Comparison with the LokiJS RxStorage",id:"comparison-with-the-lokijs-rxstorage",level:2},{value:"Replication and Migration with the memory-synced storage",id:"replication-and-migration-with-the-memory-synced-storage",level:2}];function c(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,r.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.h1,{id:"memory-synced-rxstorage",children:"Memory Synced RxStorage"}),"\n",(0,a.jsxs)(t.p,{children:["The memory synced ",(0,a.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"})," is a wrapper around any other RxStorage. The wrapper creates an in-memory storage that is used for query and write operations. This memory instance is replicated with the underlying storage for persistence.\nThe main reason to use this is to improve initial page load and query/write times. This is mostly useful in browser based applications."]}),"\n",(0,a.jsx)(t.h2,{id:"pros",children:"Pros"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"Improves read/write performance because these operations run against the in-memory storage."}),"\n",(0,a.jsx)(t.li,{children:"Decreases initial page load because it load all data in a single bulk request. It even detects if the database is used for the first time and then it does not have to await the creation of the persistent storage."}),"\n"]}),"\n",(0,a.jsx)(t.h2,{id:"cons",children:"Cons"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"It does not support attachments."}),"\n",(0,a.jsxs)(t.li,{children:["When the JavaScript process is killed ungracefully like when the browser crashes or the power of the PC is terminated, it might happen that some memory writes are not persisted to the parent storage. This can be prevented with the ",(0,a.jsx)(t.code,{children:"awaitWritePersistence"})," flag."]}),"\n",(0,a.jsx)(t.li,{children:"This can only be used if all data fits into the memory of the JavaScript process. This is normally not a problem because a browser has much memory these days and plain json document data is not that big."}),"\n",(0,a.jsxs)(t.li,{children:["Because it has to await an initial replication from the parent storage into the memory, initial page load time can increase when much data is already stored. This is likely not a problem when you store less then ",(0,a.jsx)(t.code,{children:"10k"})," documents."]}),"\n",(0,a.jsx)(t.li,{children:"The memory-synced storage itself does not support replication and migration. Instead you have to replicate the underlying parent storage."}),"\n"]}),"\n",(0,a.jsx)(t.admonition,{title:"Premium",type:"note",children:(0,a.jsxs)(t.p,{children:["The ",(0,a.jsx)(t.code,{children:"memory-synced"})," plugin is part of ",(0,a.jsx)(t.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]})}),"\n",(0,a.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"\nimport {\n getRxStorageIndexedDB\n} from 'rxdb-premium/plugins/storage-indexeddb';\nimport {\n getMemorySyncedRxStorage\n} from 'rxdb-premium/plugins/storage-memory-synced';\n\n/**\n * Here we use the IndexedDB RxStorage as persistence storage.\n * Any other RxStorage can also be used.\n */\nconst parentStorage = getRxStorageIndexedDB();\n\n// wrap the persistent storage with the memory synced one.\nconst storage = getMemorySyncedRxStorage({\n storage: parentStorage\n});\n\n// create the RxDatabase like you would do with any other RxStorage\nconst db = await createRxDatabase({\n name: 'myDatabase,\n storage,\n});\n/** ... **/\n\n"})}),"\n",(0,a.jsx)(t.h2,{id:"options",children:"Options"}),"\n",(0,a.jsx)(t.p,{children:"Some options can be provided to fine tune the performance and behavior."}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"\nimport {\n requestIdlePromise\n} from 'rxdb';\n\nconst storage = getMemorySyncedRxStorage({\n storage: parentStorage,\n\n /**\n * Defines how many document\n * get replicated in a single batch.\n * [default=50]\n * \n * (optional)\n */\n batchSize: 50,\n\n /**\n * By default, the parent storage will be created without indexes for a faster page load.\n * Indexes are not needed because the queries will anyway run on the memory storage.\n * You can disable this behavior by setting keepIndexesOnParent to true.\n * \n * (optional)\n */\n keepIndexesOnParent: true,\n\n /**\n * If set to true, all write operations will resolve AFTER the writes\n * have been persisted from the memory to the parentStorage.\n * This ensures writes are not lost even if the JavaScript process exits\n * between memory writes and the persistence interval.\n * default=false\n */\n awaitWritePersistence: true,\n\n /**\n * After a write, await until the return value of this method resolves\n * before replicating with the master storage.\n * \n * By returning requestIdlePromise() we can ensure that the CPU is idle\n * and no other, more important operation is running. By doing so we can be sure\n * that the replication does not slow down any rendering of the browser process.\n * \n * (optional)\n */\n waitBeforePersist: () => requestIdlePromise();\n});\n"})}),"\n",(0,a.jsx)(t.h2,{id:"comparison-with-the-lokijs-rxstorage",children:"Comparison with the LokiJS RxStorage"}),"\n",(0,a.jsxs)(t.p,{children:["The ",(0,a.jsx)(t.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"})," also loads the whole database state into the memory to improve operation time.\nIn comparison to LokiJS, the ",(0,a.jsx)(t.code,{children:"Memory Synced"})," RxStorage has many improvements and performance optimizations to reduce initial load time. Also it uses replication instead of the leader election to handle multi-tab usage. This alone decreases the initial page load by about 200 milliseconds."]}),"\n",(0,a.jsx)(t.h2,{id:"replication-and-migration-with-the-memory-synced-storage",children:"Replication and Migration with the memory-synced storage"}),"\n",(0,a.jsxs)(t.p,{children:["The memory-synced storage itself does not support replication and migration. Instead you have to replicate the underlying parent storage.\nFor example when you use it on top of an ",(0,a.jsx)(t.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB storage"}),", you have to run replication on that storage instead by creating a different ",(0,a.jsx)(t.a,{href:"/rx-database.html",children:"RxDatabase"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-js",children:"const parentStorage = getRxStorageIndexedDB({\n indexedDB,\n IDBKeyRange\n});\n\nconst memorySyncedStorage = getMemorySyncedRxStorage({\n storage: parentStorage\n});\n\nconst databaseName = 'mydata';\n\n/**\n * Create a parent database with the same name+collections\n * and use it for replication and migration.\n * The parent database must be created BEFORE the memory-synced database\n * to ensure migration has already been run.\n */\nconst parentDatabase = await createRxDatabase({\n name: databaseName,\n storage: parentStorage\n});\nawait parentDatabase.addCollections(/* ... */);\n\nreplicateRxCollection({\n collection: parentDatabase.myCollection,\n /* ... */\n});\n\n\n/**\n * Create an equal memory-synced database with the same name+collections\n * and use it for writes and queries.\n */\nconst memoryDatabase = await createRxDatabase({\n name: databaseName,\n storage: memorySyncedStorage\n});\nawait memoryDatabase.addCollections(/* ... */);\n"})})]})}function h(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(c,{...e})}):c(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>s,x:()=>i});var a=n(6540);const r={},o=a.createContext(r);function s(e){const t=a.useContext(o);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),a.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/01684a0a.9badf68c.js b/docs/assets/js/01684a0a.9badf68c.js deleted file mode 100644 index ec4935a1359..00000000000 --- a/docs/assets/js/01684a0a.9badf68c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6616],{3395:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>l});var a=n(4848),r=n(8453);const o={title:"Memory Synced RxStorage \ud83d\udc51",slug:"rx-storage-memory-synced.html"},s="Memory Synced RxStorage",i={id:"rx-storage-memory-synced",title:"Memory Synced RxStorage \ud83d\udc51",description:"The memory synced RxStorage is a wrapper around any other RxStorage. The wrapper creates an in-memory storage that is used for query and write operations. This memory instance is replicated with the underlying storage for persistence.",source:"@site/docs/rx-storage-memory-synced.md",sourceDirName:".",slug:"/rx-storage-memory-synced.html",permalink:"/rx-storage-memory-synced.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Memory Synced RxStorage \ud83d\udc51",slug:"rx-storage-memory-synced.html"},sidebar:"tutorialSidebar",previous:{title:"SharedWorker RxStorage \ud83d\udc51",permalink:"/rx-storage-shared-worker.html"},next:{title:"Sharding RxStorage \ud83d\udc51",permalink:"/rx-storage-sharding.html"}},d={},l=[{value:"Pros",id:"pros",level:2},{value:"Cons",id:"cons",level:2},{value:"Usage",id:"usage",level:2},{value:"Options",id:"options",level:2},{value:"Comparison with the LokiJS RxStorage",id:"comparison-with-the-lokijs-rxstorage",level:2},{value:"Replication and Migration with the memory-synced storage",id:"replication-and-migration-with-the-memory-synced-storage",level:2}];function c(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.h1,{id:"memory-synced-rxstorage",children:"Memory Synced RxStorage"}),"\n",(0,a.jsxs)(t.p,{children:["The memory synced ",(0,a.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"})," is a wrapper around any other RxStorage. The wrapper creates an in-memory storage that is used for query and write operations. This memory instance is replicated with the underlying storage for persistence.\nThe main reason to use this is to improve initial page load and query/write times. This is mostly useful in browser based applications."]}),"\n",(0,a.jsx)(t.h2,{id:"pros",children:"Pros"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"Improves read/write performance because these operations run against the in-memory storage."}),"\n",(0,a.jsx)(t.li,{children:"Decreases initial page load because it load all data in a single bulk request. It even detects if the database is used for the first time and then it does not have to await the creation of the persistent storage."}),"\n"]}),"\n",(0,a.jsx)(t.h2,{id:"cons",children:"Cons"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"It does not support attachments."}),"\n",(0,a.jsxs)(t.li,{children:["When the JavaScript process is killed ungracefully like when the browser crashes or the power of the PC is terminated, it might happen that some memory writes are not persisted to the parent storage. This can be prevented with the ",(0,a.jsx)(t.code,{children:"awaitWritePersistence"})," flag."]}),"\n",(0,a.jsx)(t.li,{children:"This can only be used if all data fits into the memory of the JavaScript process. This is normally not a problem because a browser has much memory these days and plain json document data is not that big."}),"\n",(0,a.jsxs)(t.li,{children:["Because it has to await an initial replication from the parent storage into the memory, initial page load time can increase when much data is already stored. This is likely not a problem when you store less then ",(0,a.jsx)(t.code,{children:"10k"})," documents."]}),"\n",(0,a.jsx)(t.li,{children:"The memory-synced storage itself does not support replication and migration. Instead you have to replicate the underlying parent storage."}),"\n"]}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.strong,{children:"NOTICE:"})," The ",(0,a.jsx)(t.code,{children:"memory-synced"})," plugin is part of ",(0,a.jsx)(t.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]}),"\n",(0,a.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"\nimport {\n getRxStorageIndexedDB\n} from 'rxdb-premium/plugins/storage-indexeddb';\nimport {\n getMemorySyncedRxStorage\n} from 'rxdb-premium/plugins/storage-memory-synced';\n\n/**\n * Here we use the IndexedDB RxStorage as persistence storage.\n * Any other RxStorage can also be used.\n */\nconst parentStorage = getRxStorageIndexedDB();\n\n// wrap the persistent storage with the memory synced one.\nconst storage = getMemorySyncedRxStorage({\n storage: parentStorage\n});\n\n// create the RxDatabase like you would do with any other RxStorage\nconst db = await createRxDatabase({\n name: 'myDatabase,\n storage,\n});\n/** ... **/\n\n"})}),"\n",(0,a.jsx)(t.h2,{id:"options",children:"Options"}),"\n",(0,a.jsx)(t.p,{children:"Some options can be provided to fine tune the performance and behavior."}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"\nimport {\n requestIdlePromise\n} from 'rxdb';\n\nconst storage = getMemorySyncedRxStorage({\n storage: parentStorage,\n\n /**\n * Defines how many document\n * get replicated in a single batch.\n * [default=50]\n * \n * (optional)\n */\n batchSize: 50,\n\n /**\n * By default, the parent storage will be created without indexes for a faster page load.\n * Indexes are not needed because the queries will anyway run on the memory storage.\n * You can disable this behavior by setting keepIndexesOnParent to true.\n * \n * (optional)\n */\n keepIndexesOnParent: true,\n\n /**\n * If set to true, all write operations will resolve AFTER the writes\n * have been persisted from the memory to the parentStorage.\n * This ensures writes are not lost even if the JavaScript process exits\n * between memory writes and the persistence interval.\n * default=false\n */\n awaitWritePersistence: true,\n\n /**\n * After a write, await until the return value of this method resolves\n * before replicating with the master storage.\n * \n * By returning requestIdlePromise() we can ensure that the CPU is idle\n * and no other, more important operation is running. By doing so we can be sure\n * that the replication does not slow down any rendering of the browser process.\n * \n * (optional)\n */\n waitBeforePersist: () => requestIdlePromise();\n});\n"})}),"\n",(0,a.jsx)(t.h2,{id:"comparison-with-the-lokijs-rxstorage",children:"Comparison with the LokiJS RxStorage"}),"\n",(0,a.jsxs)(t.p,{children:["The ",(0,a.jsx)(t.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"})," also loads the whole database state into the memory to improve operation time.\nIn comparison to LokiJS, the ",(0,a.jsx)(t.code,{children:"Memory Synced"})," RxStorage has many improvements and performance optimizations to reduce initial load time. Also it uses replication instead of the leader election to handle multi-tab usage. This alone decreases the initial page load by about 200 milliseconds."]}),"\n",(0,a.jsx)(t.h2,{id:"replication-and-migration-with-the-memory-synced-storage",children:"Replication and Migration with the memory-synced storage"}),"\n",(0,a.jsxs)(t.p,{children:["The memory-synced storage itself does not support replication and migration. Instead you have to replicate the underlying parent storage.\nFor example when you use it on top of an ",(0,a.jsx)(t.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB storage"}),", you have to run replication on that storage instead by creating a different ",(0,a.jsx)(t.a,{href:"/rx-database.html",children:"RxDatabase"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-js",children:"const parentStorage = getRxStorageIndexedDB({\n indexedDB,\n IDBKeyRange\n});\n\nconst memorySyncedStorage = getMemorySyncedRxStorage({\n storage: parentStorage\n});\n\nconst databaseName = 'mydata';\n\n/**\n * Create a parent database with the same name+collections\n * and use it for replication and migration.\n * The parent database must be created BEFORE the memory-synced database\n * to ensure migration has already been run.\n */\nconst parentDatabase = await createRxDatabase({\n name: databaseName,\n storage: parentStorage\n});\nawait parentDatabase.addCollections(/* ... */);\n\nreplicateRxCollection({\n collection: parentDatabase.myCollection,\n /* ... */\n});\n\n\n/**\n * Create an equal memory-synced database with the same name+collections\n * and use it for writes and queries.\n */\nconst memoryDatabase = await createRxDatabase({\n name: databaseName,\n storage: memorySyncedStorage\n});\nawait memoryDatabase.addCollections(/* ... */);\n"})})]})}function h(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(c,{...e})}):c(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>s,x:()=>i});var a=n(6540);const r={},o=a.createContext(r);function s(e){const t=a.useContext(o);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),a.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/1c0701dd.25197a80.js b/docs/assets/js/1c0701dd.25197a80.js deleted file mode 100644 index c8bb9f044fd..00000000000 --- a/docs/assets/js/1c0701dd.25197a80.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6953],{668:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>a,contentTitle:()=>i,default:()=>h,frontMatter:()=>l,metadata:()=>r,toc:()=>c});var s=o(4848),t=o(8453);const l={title:"Middleware",slug:"middleware.html"},i="Middleware",r={id:"middleware",title:"Middleware",description:"RxDB supports middleware-hooks like mongoose.",source:"@site/docs/middleware.md",sourceDirName:".",slug:"/middleware.html",permalink:"/middleware.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Middleware",slug:"middleware.html"},sidebar:"tutorialSidebar",previous:{title:"Transactions, Conflicts and Revisions",permalink:"/transactions-conflicts-revisions.html"},next:{title:"Query Cache",permalink:"/query-cache.html"}},a={},c=[{value:"List",id:"list",level:2},{value:"Why is there no validate-hook?",id:"why-is-there-no-validate-hook",level:3},{value:"Use Cases",id:"use-cases",level:2},{value:"Usage",id:"usage",level:2},{value:"Insert",id:"insert",level:3},{value:"lifecycle",id:"lifecycle",level:4},{value:"preInsert",id:"preinsert",level:4},{value:"postInsert",id:"postinsert",level:4},{value:"Save",id:"save",level:3},{value:"lifecycle",id:"lifecycle-1",level:4},{value:"preSave",id:"presave",level:4},{value:"postSave",id:"postsave",level:4},{value:"Remove",id:"remove",level:3},{value:"lifecycle",id:"lifecycle-2",level:4},{value:"preRemove",id:"preremove",level:4},{value:"postRemove",id:"postremove",level:4},{value:"postCreate",id:"postcreate",level:3}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"middleware",children:"Middleware"}),"\n",(0,s.jsxs)(n.p,{children:["RxDB supports middleware-hooks like ",(0,s.jsx)(n.a,{href:"http://mongoosejs.com/docs/middleware.html",children:"mongoose"}),".\nMiddleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions.\nThe hooks are specified on RxCollection-level and help to create a clear what-happens-when-structure of your code."]}),"\n",(0,s.jsx)(n.p,{children:"Hooks can be defined to run parallel or as series one after another.\nHooks can be synchronous or asynchronous when they return a Promise.\nTo stop the operation at a specific hook, throw an error."}),"\n",(0,s.jsx)(n.h2,{id:"list",children:"List"}),"\n",(0,s.jsx)(n.p,{children:"RxDB supports the following hooks:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"preInsert"}),"\n",(0,s.jsx)(n.li,{children:"postInsert"}),"\n",(0,s.jsx)(n.li,{children:"preSave"}),"\n",(0,s.jsx)(n.li,{children:"postSave"}),"\n",(0,s.jsx)(n.li,{children:"preRemove"}),"\n",(0,s.jsx)(n.li,{children:"postRemove"}),"\n",(0,s.jsx)(n.li,{children:"postCreate"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"why-is-there-no-validate-hook",children:"Why is there no validate-hook?"}),"\n",(0,s.jsxs)(n.p,{children:["Different to mongoose, the validation on document-data is running on the field-level for every change to a document.\nThis means if you set the value ",(0,s.jsx)(n.code,{children:"lastName"})," of a RxDocument, then the validation will only run on the changed field, not the whole document.\nTherefore it is not useful to have validate-hooks when a document is written to the database."]}),"\n",(0,s.jsx)(n.h2,{id:"use-cases",children:"Use Cases"}),"\n",(0,s.jsx)(n.p,{children:"Middleware are useful for atomizing model logic and avoiding nested blocks of async code.\nHere are some other ideas:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"complex validation"}),"\n",(0,s.jsx)(n.li,{children:"removing dependent documents"}),"\n",(0,s.jsx)(n.li,{children:"asynchronous defaults"}),"\n",(0,s.jsx)(n.li,{children:"asynchronous tasks that a certain action triggers"}),"\n",(0,s.jsx)(n.li,{children:"triggering custom events"}),"\n",(0,s.jsx)(n.li,{children:"notifications"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsxs)(n.p,{children:["All hooks have the plain data as first parameter, and all but ",(0,s.jsx)(n.code,{children:"preInsert"})," also have the ",(0,s.jsx)(n.code,{children:"RxDocument"}),"-instance as second parameter. If you want to modify the data in the hook, change attributes of the first parameter."]}),"\n",(0,s.jsxs)(n.p,{children:["All hook functions are also ",(0,s.jsx)(n.code,{children:"this"}),"-bind to the ",(0,s.jsx)(n.code,{children:"RxCollection"}),"-instance."]}),"\n",(0,s.jsx)(n.h3,{id:"insert",children:"Insert"}),"\n",(0,s.jsx)(n.p,{children:"An insert-hook receives the data-object of the new document."}),"\n",(0,s.jsx)(n.h4,{id:"lifecycle",children:"lifecycle"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"RxCollection.insert is called"}),"\n",(0,s.jsx)(n.li,{children:"preInsert series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"preInsert parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"schema validation runs"}),"\n",(0,s.jsx)(n.li,{children:"new document is written to database"}),"\n",(0,s.jsx)(n.li,{children:"postInsert series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"postInsert parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"event is emitted to RxDatabase and RxCollection"}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"preinsert",children:"preInsert"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.preInsert(function(plainData){\n // set age to 50 before saving\n plainData.age = 50;\n}, false);\n\n// parallel\nmyCollection.preInsert(function(plainData){\n\n}, true);\n\n// async\nmyCollection.preInsert(function(plainData){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n\n// stop the insert-operation\nmyCollection.preInsert(function(plainData){\n throw new Error('stop');\n}, false);\n"})}),"\n",(0,s.jsx)(n.h4,{id:"postinsert",children:"postInsert"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.postInsert(function(plainData, rxDocument){\n\n}, false);\n\n// parallel\nmyCollection.postInsert(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.postInsert(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n"})}),"\n",(0,s.jsx)(n.h3,{id:"save",children:"Save"}),"\n",(0,s.jsx)(n.p,{children:"A save-hook receives the document which is saved."}),"\n",(0,s.jsx)(n.h4,{id:"lifecycle-1",children:"lifecycle"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"RxDocument.save is called"}),"\n",(0,s.jsx)(n.li,{children:"preSave series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"preSave parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"updated document is written to database"}),"\n",(0,s.jsx)(n.li,{children:"postSave series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"postSave parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"event is emitted to RxDatabase and RxCollection"}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"presave",children:"preSave"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.preSave(function(plainData, rxDocument){\n // modify anyField before saving\n plainData.anyField = 'anyValue';\n}, false);\n\n// parallel\nmyCollection.preSave(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.preSave(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n\n// stop the save-operation\nmyCollection.preSave(function(plainData, rxDocument){\n throw new Error('stop');\n}, false);\n"})}),"\n",(0,s.jsx)(n.h4,{id:"postsave",children:"postSave"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.postSave(function(plainData, rxDocument){\n\n}, false);\n\n// parallel\nmyCollection.postSave(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.postSave(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n"})}),"\n",(0,s.jsx)(n.h3,{id:"remove",children:"Remove"}),"\n",(0,s.jsx)(n.p,{children:"An remove-hook receives the document which is removed."}),"\n",(0,s.jsx)(n.h4,{id:"lifecycle-2",children:"lifecycle"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"RxDocument.remove is called"}),"\n",(0,s.jsx)(n.li,{children:"preRemove series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"preRemove parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"deleted document is written to database"}),"\n",(0,s.jsx)(n.li,{children:"postRemove series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"postRemove parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"event is emitted to RxDatabase and RxCollection"}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"preremove",children:"preRemove"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.preRemove(function(plainData, rxDocument){\n\n}, false);\n\n// parallel\nmyCollection.preRemove(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.preRemove(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n\n// stop the remove-operation\nmyCollection.preRemove(function(plainData, rxDocument){\n throw new Error('stop');\n}, false);\n"})}),"\n",(0,s.jsx)(n.h4,{id:"postremove",children:"postRemove"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.postRemove(function(plainData, rxDocument){\n\n}, false);\n\n// parallel\nmyCollection.postRemove(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.postRemove(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n"})}),"\n",(0,s.jsx)(n.h3,{id:"postcreate",children:"postCreate"}),"\n",(0,s.jsxs)(n.p,{children:["This hook is called whenever a ",(0,s.jsx)(n.code,{children:"RxDocument"})," is constructed.\nYou can use ",(0,s.jsx)(n.code,{children:"postCreate"})," to modify every RxDocument-instance of the collection.\nThis adds a flexible way to add specify behavior to every document. You can also use it to add custom getter/setter to documents. PostCreate-hooks cannot be ",(0,s.jsx)(n.strong,{children:"asynchronous"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"myCollection.postCreate(function(plainData, rxDocument){\n Object.defineProperty(rxDocument, 'myField', {\n get: () => 'foobar',\n });\n});\n\nconst doc = await myCollection.findOne().exec();\n\nconsole.log(doc.myField);\n// 'foobar'\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Notice: This hook does not run on already created or cached documents. Make sure to add ",(0,s.jsx)(n.code,{children:"postCreate"}),"-hooks before interacting with the collection."]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},8453:(e,n,o)=>{o.d(n,{R:()=>i,x:()=>r});var s=o(6540);const t={},l=s.createContext(t);function i(e){const n=s.useContext(l);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),s.createElement(l.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/1c0701dd.6bb8de2c.js b/docs/assets/js/1c0701dd.6bb8de2c.js new file mode 100644 index 00000000000..6221c789a82 --- /dev/null +++ b/docs/assets/js/1c0701dd.6bb8de2c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6953],{668:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>a,contentTitle:()=>i,default:()=>h,frontMatter:()=>l,metadata:()=>r,toc:()=>c});var s=o(4848),t=o(8453);const l={title:"Middleware",slug:"middleware.html"},i="Middleware",r={id:"middleware",title:"Middleware",description:"RxDB supports middleware-hooks like mongoose.",source:"@site/docs/middleware.md",sourceDirName:".",slug:"/middleware.html",permalink:"/middleware.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Middleware",slug:"middleware.html"},sidebar:"tutorialSidebar",previous:{title:"Transactions, Conflicts and Revisions",permalink:"/transactions-conflicts-revisions.html"},next:{title:"Query Cache",permalink:"/query-cache.html"}},a={},c=[{value:"List",id:"list",level:2},{value:"Why is there no validate-hook?",id:"why-is-there-no-validate-hook",level:3},{value:"Use Cases",id:"use-cases",level:2},{value:"Usage",id:"usage",level:2},{value:"Insert",id:"insert",level:3},{value:"lifecycle",id:"lifecycle",level:4},{value:"preInsert",id:"preinsert",level:4},{value:"postInsert",id:"postinsert",level:4},{value:"Save",id:"save",level:3},{value:"lifecycle",id:"lifecycle-1",level:4},{value:"preSave",id:"presave",level:4},{value:"postSave",id:"postsave",level:4},{value:"Remove",id:"remove",level:3},{value:"lifecycle",id:"lifecycle-2",level:4},{value:"preRemove",id:"preremove",level:4},{value:"postRemove",id:"postremove",level:4},{value:"postCreate",id:"postcreate",level:3}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"middleware",children:"Middleware"}),"\n",(0,s.jsxs)(n.p,{children:["RxDB supports middleware-hooks like ",(0,s.jsx)(n.a,{href:"http://mongoosejs.com/docs/middleware.html",children:"mongoose"}),".\nMiddleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions.\nThe hooks are specified on RxCollection-level and help to create a clear what-happens-when-structure of your code."]}),"\n",(0,s.jsx)(n.p,{children:"Hooks can be defined to run parallel or as series one after another.\nHooks can be synchronous or asynchronous when they return a Promise.\nTo stop the operation at a specific hook, throw an error."}),"\n",(0,s.jsx)(n.h2,{id:"list",children:"List"}),"\n",(0,s.jsx)(n.p,{children:"RxDB supports the following hooks:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"preInsert"}),"\n",(0,s.jsx)(n.li,{children:"postInsert"}),"\n",(0,s.jsx)(n.li,{children:"preSave"}),"\n",(0,s.jsx)(n.li,{children:"postSave"}),"\n",(0,s.jsx)(n.li,{children:"preRemove"}),"\n",(0,s.jsx)(n.li,{children:"postRemove"}),"\n",(0,s.jsx)(n.li,{children:"postCreate"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"why-is-there-no-validate-hook",children:"Why is there no validate-hook?"}),"\n",(0,s.jsxs)(n.p,{children:["Different to mongoose, the validation on document-data is running on the field-level for every change to a document.\nThis means if you set the value ",(0,s.jsx)(n.code,{children:"lastName"})," of a RxDocument, then the validation will only run on the changed field, not the whole document.\nTherefore it is not useful to have validate-hooks when a document is written to the database."]}),"\n",(0,s.jsx)(n.h2,{id:"use-cases",children:"Use Cases"}),"\n",(0,s.jsx)(n.p,{children:"Middleware are useful for atomizing model logic and avoiding nested blocks of async code.\nHere are some other ideas:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"complex validation"}),"\n",(0,s.jsx)(n.li,{children:"removing dependent documents"}),"\n",(0,s.jsx)(n.li,{children:"asynchronous defaults"}),"\n",(0,s.jsx)(n.li,{children:"asynchronous tasks that a certain action triggers"}),"\n",(0,s.jsx)(n.li,{children:"triggering custom events"}),"\n",(0,s.jsx)(n.li,{children:"notifications"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsxs)(n.p,{children:["All hooks have the plain data as first parameter, and all but ",(0,s.jsx)(n.code,{children:"preInsert"})," also have the ",(0,s.jsx)(n.code,{children:"RxDocument"}),"-instance as second parameter. If you want to modify the data in the hook, change attributes of the first parameter."]}),"\n",(0,s.jsxs)(n.p,{children:["All hook functions are also ",(0,s.jsx)(n.code,{children:"this"}),"-bind to the ",(0,s.jsx)(n.code,{children:"RxCollection"}),"-instance."]}),"\n",(0,s.jsx)(n.h3,{id:"insert",children:"Insert"}),"\n",(0,s.jsx)(n.p,{children:"An insert-hook receives the data-object of the new document."}),"\n",(0,s.jsx)(n.h4,{id:"lifecycle",children:"lifecycle"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"RxCollection.insert is called"}),"\n",(0,s.jsx)(n.li,{children:"preInsert series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"preInsert parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"schema validation runs"}),"\n",(0,s.jsx)(n.li,{children:"new document is written to database"}),"\n",(0,s.jsx)(n.li,{children:"postInsert series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"postInsert parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"event is emitted to RxDatabase and RxCollection"}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"preinsert",children:"preInsert"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.preInsert(function(plainData){\n // set age to 50 before saving\n plainData.age = 50;\n}, false);\n\n// parallel\nmyCollection.preInsert(function(plainData){\n\n}, true);\n\n// async\nmyCollection.preInsert(function(plainData){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n\n// stop the insert-operation\nmyCollection.preInsert(function(plainData){\n throw new Error('stop');\n}, false);\n"})}),"\n",(0,s.jsx)(n.h4,{id:"postinsert",children:"postInsert"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.postInsert(function(plainData, rxDocument){\n\n}, false);\n\n// parallel\nmyCollection.postInsert(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.postInsert(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n"})}),"\n",(0,s.jsx)(n.h3,{id:"save",children:"Save"}),"\n",(0,s.jsx)(n.p,{children:"A save-hook receives the document which is saved."}),"\n",(0,s.jsx)(n.h4,{id:"lifecycle-1",children:"lifecycle"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"RxDocument.save is called"}),"\n",(0,s.jsx)(n.li,{children:"preSave series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"preSave parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"updated document is written to database"}),"\n",(0,s.jsx)(n.li,{children:"postSave series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"postSave parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"event is emitted to RxDatabase and RxCollection"}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"presave",children:"preSave"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.preSave(function(plainData, rxDocument){\n // modify anyField before saving\n plainData.anyField = 'anyValue';\n}, false);\n\n// parallel\nmyCollection.preSave(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.preSave(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n\n// stop the save-operation\nmyCollection.preSave(function(plainData, rxDocument){\n throw new Error('stop');\n}, false);\n"})}),"\n",(0,s.jsx)(n.h4,{id:"postsave",children:"postSave"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.postSave(function(plainData, rxDocument){\n\n}, false);\n\n// parallel\nmyCollection.postSave(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.postSave(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n"})}),"\n",(0,s.jsx)(n.h3,{id:"remove",children:"Remove"}),"\n",(0,s.jsx)(n.p,{children:"An remove-hook receives the document which is removed."}),"\n",(0,s.jsx)(n.h4,{id:"lifecycle-2",children:"lifecycle"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"RxDocument.remove is called"}),"\n",(0,s.jsx)(n.li,{children:"preRemove series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"preRemove parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"deleted document is written to database"}),"\n",(0,s.jsx)(n.li,{children:"postRemove series-hooks"}),"\n",(0,s.jsx)(n.li,{children:"postRemove parallel-hooks"}),"\n",(0,s.jsx)(n.li,{children:"event is emitted to RxDatabase and RxCollection"}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"preremove",children:"preRemove"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.preRemove(function(plainData, rxDocument){\n\n}, false);\n\n// parallel\nmyCollection.preRemove(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.preRemove(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n\n// stop the remove-operation\nmyCollection.preRemove(function(plainData, rxDocument){\n throw new Error('stop');\n}, false);\n"})}),"\n",(0,s.jsx)(n.h4,{id:"postremove",children:"postRemove"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"// series\nmyCollection.postRemove(function(plainData, rxDocument){\n\n}, false);\n\n// parallel\nmyCollection.postRemove(function(plainData, rxDocument){\n\n}, true);\n\n// async\nmyCollection.postRemove(function(plainData, rxDocument){\n return new Promise(res => setTimeout(res, 100));\n}, false);\n"})}),"\n",(0,s.jsx)(n.h3,{id:"postcreate",children:"postCreate"}),"\n",(0,s.jsxs)(n.p,{children:["This hook is called whenever a ",(0,s.jsx)(n.code,{children:"RxDocument"})," is constructed.\nYou can use ",(0,s.jsx)(n.code,{children:"postCreate"})," to modify every RxDocument-instance of the collection.\nThis adds a flexible way to add specify behavior to every document. You can also use it to add custom getter/setter to documents. PostCreate-hooks cannot be ",(0,s.jsx)(n.strong,{children:"asynchronous"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"myCollection.postCreate(function(plainData, rxDocument){\n Object.defineProperty(rxDocument, 'myField', {\n get: () => 'foobar',\n });\n});\n\nconst doc = await myCollection.findOne().exec();\n\nconsole.log(doc.myField);\n// 'foobar'\n"})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["This hook does not run on already created or cached documents. Make sure to add ",(0,s.jsx)(n.code,{children:"postCreate"}),"-hooks before interacting with the collection."]})})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},8453:(e,n,o)=>{o.d(n,{R:()=>i,x:()=>r});var s=o(6540);const t={},l=s.createContext(t);function i(e){const n=s.useContext(l);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),s.createElement(l.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/2456d5e0.e40eb42f.js b/docs/assets/js/2456d5e0.32eb1f3e.js similarity index 91% rename from docs/assets/js/2456d5e0.e40eb42f.js rename to docs/assets/js/2456d5e0.32eb1f3e.js index 78d297fc60a..375d2261b81 100644 --- a/docs/assets/js/2456d5e0.e40eb42f.js +++ b/docs/assets/js/2456d5e0.32eb1f3e.js @@ -1 +1 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[2966],{5654:(e,a,t)=>{t.r(a),t.d(a,{assets:()=>d,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>o,toc:()=>l});var n=t(4848),s=t(8453);const r={title:"RxDatabase",slug:"rx-database.html"},i="RxDatabase",o={id:"rx-database",title:"RxDatabase",description:"A RxDatabase-Object contains your collections and handles the synchronization of change-events.",source:"@site/docs/rx-database.md",sourceDirName:".",slug:"/rx-database.html",permalink:"/rx-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDatabase",slug:"rx-database.html"},sidebar:"tutorialSidebar",previous:{title:"Dev-Mode Plugin",permalink:"/dev-mode.html"},next:{title:"RxSchema",permalink:"/rx-schema.html"}},d={},l=[{value:"Creation",id:"creation",level:2},{value:"name",id:"name",level:3},{value:"storage",id:"storage",level:3},{value:"password",id:"password",level:3},{value:"multiInstance",id:"multiinstance",level:3},{value:"eventReduce",id:"eventreduce",level:3},{value:"ignoreDuplicate",id:"ignoreduplicate",level:3},{value:"Methods",id:"methods",level:2},{value:"Observe with $",id:"observe-with-",level:3},{value:"exportJSON()",id:"exportjson",level:3},{value:"importJSON()",id:"importjson",level:3},{value:"backup()",id:"backup",level:3},{value:"waitForLeadership()",id:"waitforleadership",level:3},{value:"requestIdlePromise()",id:"requestidlepromise",level:3},{value:"destroy()",id:"destroy",level:3},{value:"remove()",id:"remove",level:3},{value:"isRxDatabase",id:"isrxdatabase",level:3}];function c(e){const a={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(a.h1,{id:"rxdatabase",children:"RxDatabase"}),"\n",(0,n.jsx)(a.p,{children:"A RxDatabase-Object contains your collections and handles the synchronization of change-events."}),"\n",(0,n.jsx)(a.h2,{id:"creation",children:"Creation"}),"\n",(0,n.jsxs)(a.p,{children:["The database is created by the asynchronous ",(0,n.jsx)(a.code,{children:".createRxDatabase()"})," function of the core RxDB module. It has the following parameters:"]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { createRxDatabase } from 'rxdb';\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\nconst db = await createRxDatabase({\n name: 'heroesdb', // <- name\n storage: getRxStorageDexie(), // <- RxStorage\n\n /* Optional parameters: */\n password: 'myPassword', // <- password (optional)\n multiInstance: true, // <- multiInstance (optional, default: true)\n eventReduce: true, // <- eventReduce (optional, default: false)\n cleanupPolicy: {} // <- custom cleanup policy (optional) \n});\n"})}),"\n",(0,n.jsx)(a.h3,{id:"name",children:"name"}),"\n",(0,n.jsxs)(a.p,{children:["The database-name is a string which uniquely identifies the database. When two RxDatabases have the same name and use the same ",(0,n.jsx)(a.code,{children:"RxStorage"}),", their data can be assumed as equal and they will share events between each other.\nDepending on the storage or adapter this can also be used to define the filesystem folder of your data."]}),"\n",(0,n.jsx)(a.h3,{id:"storage",children:"storage"}),"\n",(0,n.jsxs)(a.p,{children:["RxDB works on top of an implementation of the ",(0,n.jsx)(a.a,{href:"/rx-storage.html",children:"RxStorage"})," interface. This interface is an abstraction that allows you to use different underlying databases that actually handle the documents. Depending on your use case you might use a different ",(0,n.jsx)(a.code,{children:"storage"})," with different tradeoffs in performance, bundle size or supported runtimes."]}),"\n",(0,n.jsxs)(a.p,{children:["There are many ",(0,n.jsx)(a.code,{children:"RxStorage"})," implementations that can be used depending on the JavaScript environment and performance requirements.\nFor example you can use the ",(0,n.jsx)(a.a,{href:"/rx-storage-dexie.html",children:"Dexie RxStorage"})," in the browser or use the LokiJS storage with the filesystem adapter in Node.js."]}),"\n",(0,n.jsxs)(a.ul,{children:["\n",(0,n.jsx)(a.li,{children:(0,n.jsx)(a.a,{href:"/rx-storage.html",children:"List of RxStorage implementations"})}),"\n"]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"\n// use the Dexie.js RxStorage that stores data in IndexedDB.\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\nconst dbDexie = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageDexie()\n});\n\n\n// ...or use the LokiJS RxStorage with the indexeddb adapter.\nimport { getRxStorageLoki } from 'rxdb/plugins/storage-lokijs';\nconst LokiIncrementalIndexedDBAdapter = require('lokijs/src/incremental-indexeddb-adapter');\n\nconst dbLoki = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageLoki({\n adapter: new LokiIncrementalIndexedDBAdapter()\n })\n});\n"})}),"\n",(0,n.jsx)(a.h3,{id:"password",children:"password"}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.code,{children:"(optional)"}),"\nIf you want to use encrypted fields in the collections of a database, you have to set a password for it. The password must be a string with at least 12 characters."]}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.a,{href:"/encryption.html",children:"Read more about encryption here"}),"."]}),"\n",(0,n.jsx)(a.h3,{id:"multiinstance",children:"multiInstance"}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.code,{children:"(optional=true)"}),"\nWhen you create more than one instance of the same database in a single javascript-runtime, you should set ",(0,n.jsx)(a.code,{children:"multiInstance"})," to ",(0,n.jsx)(a.code,{children:"true"}),". This will enable the event sharing between the two instances. For example when the user has opened multiple browser windows, events will be shared between them so that both windows react to the same changes.\n",(0,n.jsx)(a.code,{children:"multiInstance"})," should be set to ",(0,n.jsx)(a.code,{children:"false"})," when you have single-instances like a single Node.js-process, a react-native-app, a cordova-app or a single-window electron app which can decrease the startup time because no instance coordination has to be done."]}),"\n",(0,n.jsx)(a.h3,{id:"eventreduce",children:"eventReduce"}),"\n",(0,n.jsx)(a.p,{children:(0,n.jsx)(a.code,{children:"(optional=false)"})}),"\n",(0,n.jsxs)(a.p,{children:["One big benefit of having a realtime database is that big performance optimizations can be done when the database knows a query is observed and the updated results are needed continuously. RxDB uses the ",(0,n.jsx)(a.a,{href:"https://github.com/pubkey/event-reduce",children:"EventReduce Algorithm"})," to optimize observer or recurring queries."]}),"\n",(0,n.jsxs)(a.p,{children:["For better performance, you should always set ",(0,n.jsx)(a.code,{children:"eventReduce: true"}),". This will also be the default in the next major RxDB version."]}),"\n",(0,n.jsx)(a.h3,{id:"ignoreduplicate",children:"ignoreDuplicate"}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.code,{children:"(optional=false)"}),"\nIf you create multiple RxDatabase-instances with the same name and same adapter, it's very likely that you have done something wrong.\nTo prevent this common mistake, RxDB will throw an error when you do this.\nIn some rare cases like unit-tests, you want to do this intentional by setting ",(0,n.jsx)(a.code,{children:"ignoreDuplicate"})," to ",(0,n.jsx)(a.code,{children:"true"}),"."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-js",children:"const db1 = await createRxDatabase({\n name: 'heroesdb',\n storage: getRxStorageDexie(),\n ignoreDuplicate: true\n});\nconst db2 = await createRxDatabase({\n name: 'heroesdb',\n storage: getRxStorageDexie(),\n ignoreDuplicate: true // this create-call will not throw because you explicitly allow it\n});\n"})}),"\n",(0,n.jsx)(a.h2,{id:"methods",children:"Methods"}),"\n",(0,n.jsx)(a.h3,{id:"observe-with-",children:"Observe with $"}),"\n",(0,n.jsxs)(a.p,{children:["Calling this will return an ",(0,n.jsx)(a.a,{href:"http://reactivex.io/documentation/observable.html",children:"rxjs-Observable"})," which streams all write events of the ",(0,n.jsx)(a.code,{children:"RxDatabase"}),"."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"myDb.$.subscribe(changeEvent => console.dir(changeEvent));\n"})}),"\n",(0,n.jsx)(a.h3,{id:"exportjson",children:"exportJSON()"}),"\n",(0,n.jsxs)(a.p,{children:["Use this function to create a json-export from every piece of data in every collection of this database. You can pass ",(0,n.jsx)(a.code,{children:"true"})," as a parameter to decrypt the encrypted data-fields of your document."]}),"\n",(0,n.jsxs)(a.p,{children:["Before ",(0,n.jsx)(a.code,{children:"exportJSON()"})," and ",(0,n.jsx)(a.code,{children:"importJSON()"})," can be used, you have to add the ",(0,n.jsx)(a.code,{children:"json-dump"})," plugin."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBJsonDumpPlugin } from 'rxdb/plugins/json-dump';\naddRxPlugin(RxDBJsonDumpPlugin);\n"})}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"myDatabase.exportJSON()\n .then(json => console.dir(json));\n"})}),"\n",(0,n.jsx)(a.h3,{id:"importjson",children:"importJSON()"}),"\n",(0,n.jsx)(a.p,{children:"To import the json-dumps into your database, use this function."}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"// import the dump to the database\nemptyDatabase.importJSON(json)\n .then(() => console.log('done'));\n"})}),"\n",(0,n.jsx)(a.h3,{id:"backup",children:"backup()"}),"\n",(0,n.jsxs)(a.p,{children:["Writes the current (or ongoing) database state to the filesystem. ",(0,n.jsx)(a.a,{href:"/backup.html",children:"Read more"})]}),"\n",(0,n.jsx)(a.h3,{id:"waitforleadership",children:"waitForLeadership()"}),"\n",(0,n.jsxs)(a.p,{children:["Returns a Promise which resolves when the RxDatabase becomes ",(0,n.jsx)(a.a,{href:"/leader-election.html",children:"elected leader"}),"."]}),"\n",(0,n.jsx)(a.h3,{id:"requestidlepromise",children:"requestIdlePromise()"}),"\n",(0,n.jsxs)(a.p,{children:["Returns a promise which resolves when the database is in idle. This works similar to ",(0,n.jsx)(a.a,{href:"https://developer.mozilla.org/de/docs/Web/API/Window/requestIdleCallback",children:"requestIdleCallback"})," but tracks the idle-ness of the database instead of the CPU.\nUse this for semi-important tasks like cleanups which should not affect the speed of important tasks."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"\nmyDatabase.requestIdlePromise().then(() => {\n // this will run at the moment the database has nothing else to do\n myCollection.customCleanupFunction();\n});\n\n// with timeout\nmyDatabase.requestIdlePromise(1000 /* time in ms */).then(() => {\n // this will run at the moment the database has nothing else to do\n // or the timeout has passed\n myCollection.customCleanupFunction();\n});\n\n"})}),"\n",(0,n.jsx)(a.h3,{id:"destroy",children:"destroy()"}),"\n",(0,n.jsxs)(a.p,{children:["Destroys the databases object-instance. This is to free up memory and stop all observers and replications.\nReturns a ",(0,n.jsx)(a.code,{children:"Promise"})," that resolves when the database is destroyed."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"await myDatabase.destroy();\n"})}),"\n",(0,n.jsx)(a.h3,{id:"remove",children:"remove()"}),"\n",(0,n.jsx)(a.p,{children:"Wipes all documents from the storage. Use this to free up disc space."}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"await myDatabase.remove();\n// database instance is now gone\n\n// NOTICE: You can also clear a database without removing its instance\nimport { removeRxDatabase } from 'rxdb';\nremoveRxDatabase('mydatabasename', 'localstorage');\n"})}),"\n",(0,n.jsx)(a.h3,{id:"isrxdatabase",children:"isRxDatabase"}),"\n",(0,n.jsx)(a.p,{children:"Returns true if the given object is an instance of RxDatabase. Returns false if not."}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { isRxDatabase } from 'rxdb';\nconst is = isRxDatabase(myObj);\n"})})]})}function h(e={}){const{wrapper:a}={...(0,s.R)(),...e.components};return a?(0,n.jsx)(a,{...e,children:(0,n.jsx)(c,{...e})}):c(e)}},8453:(e,a,t)=>{t.d(a,{R:()=>i,x:()=>o});var n=t(6540);const s={},r=n.createContext(s);function i(e){const a=n.useContext(r);return n.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function o(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),n.createElement(r.Provider,{value:a},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[2966],{5654:(e,a,t)=>{t.r(a),t.d(a,{assets:()=>d,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>o,toc:()=>l});var n=t(4848),s=t(8453);const r={title:"RxDatabase",slug:"rx-database.html"},i="RxDatabase",o={id:"rx-database",title:"RxDatabase",description:"A RxDatabase-Object contains your collections and handles the synchronization of change-events.",source:"@site/docs/rx-database.md",sourceDirName:".",slug:"/rx-database.html",permalink:"/rx-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDatabase",slug:"rx-database.html"},sidebar:"tutorialSidebar",previous:{title:"Dev-Mode Plugin",permalink:"/dev-mode.html"},next:{title:"RxSchema",permalink:"/rx-schema.html"}},d={},l=[{value:"Creation",id:"creation",level:2},{value:"name",id:"name",level:3},{value:"storage",id:"storage",level:3},{value:"password",id:"password",level:3},{value:"multiInstance",id:"multiinstance",level:3},{value:"eventReduce",id:"eventreduce",level:3},{value:"ignoreDuplicate",id:"ignoreduplicate",level:3},{value:"Methods",id:"methods",level:2},{value:"Observe with $",id:"observe-with-",level:3},{value:"exportJSON()",id:"exportjson",level:3},{value:"importJSON()",id:"importjson",level:3},{value:"backup()",id:"backup",level:3},{value:"waitForLeadership()",id:"waitforleadership",level:3},{value:"requestIdlePromise()",id:"requestidlepromise",level:3},{value:"destroy()",id:"destroy",level:3},{value:"remove()",id:"remove",level:3},{value:"isRxDatabase",id:"isrxdatabase",level:3}];function c(e){const a={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(a.h1,{id:"rxdatabase",children:"RxDatabase"}),"\n",(0,n.jsx)(a.p,{children:"A RxDatabase-Object contains your collections and handles the synchronization of change-events."}),"\n",(0,n.jsx)(a.h2,{id:"creation",children:"Creation"}),"\n",(0,n.jsxs)(a.p,{children:["The database is created by the asynchronous ",(0,n.jsx)(a.code,{children:".createRxDatabase()"})," function of the core RxDB module. It has the following parameters:"]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { createRxDatabase } from 'rxdb';\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\nconst db = await createRxDatabase({\n name: 'heroesdb', // <- name\n storage: getRxStorageDexie(), // <- RxStorage\n\n /* Optional parameters: */\n password: 'myPassword', // <- password (optional)\n multiInstance: true, // <- multiInstance (optional, default: true)\n eventReduce: true, // <- eventReduce (optional, default: false)\n cleanupPolicy: {} // <- custom cleanup policy (optional) \n});\n"})}),"\n",(0,n.jsx)(a.h3,{id:"name",children:"name"}),"\n",(0,n.jsxs)(a.p,{children:["The database-name is a string which uniquely identifies the database. When two RxDatabases have the same name and use the same ",(0,n.jsx)(a.code,{children:"RxStorage"}),", their data can be assumed as equal and they will share events between each other.\nDepending on the storage or adapter this can also be used to define the filesystem folder of your data."]}),"\n",(0,n.jsx)(a.h3,{id:"storage",children:"storage"}),"\n",(0,n.jsxs)(a.p,{children:["RxDB works on top of an implementation of the ",(0,n.jsx)(a.a,{href:"/rx-storage.html",children:"RxStorage"})," interface. This interface is an abstraction that allows you to use different underlying databases that actually handle the documents. Depending on your use case you might use a different ",(0,n.jsx)(a.code,{children:"storage"})," with different tradeoffs in performance, bundle size or supported runtimes."]}),"\n",(0,n.jsxs)(a.p,{children:["There are many ",(0,n.jsx)(a.code,{children:"RxStorage"})," implementations that can be used depending on the JavaScript environment and performance requirements.\nFor example you can use the ",(0,n.jsx)(a.a,{href:"/rx-storage-dexie.html",children:"Dexie RxStorage"})," in the browser or use the LokiJS storage with the filesystem adapter in Node.js."]}),"\n",(0,n.jsxs)(a.ul,{children:["\n",(0,n.jsx)(a.li,{children:(0,n.jsx)(a.a,{href:"/rx-storage.html",children:"List of RxStorage implementations"})}),"\n"]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"\n// use the Dexie.js RxStorage that stores data in IndexedDB.\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\nconst dbDexie = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageDexie()\n});\n\n\n// ...or use the LokiJS RxStorage with the indexeddb adapter.\nimport { getRxStorageLoki } from 'rxdb/plugins/storage-lokijs';\nconst LokiIncrementalIndexedDBAdapter = require('lokijs/src/incremental-indexeddb-adapter');\n\nconst dbLoki = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageLoki({\n adapter: new LokiIncrementalIndexedDBAdapter()\n })\n});\n"})}),"\n",(0,n.jsx)(a.h3,{id:"password",children:"password"}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.code,{children:"(optional)"}),"\nIf you want to use encrypted fields in the collections of a database, you have to set a password for it. The password must be a string with at least 12 characters."]}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.a,{href:"/encryption.html",children:"Read more about encryption here"}),"."]}),"\n",(0,n.jsx)(a.h3,{id:"multiinstance",children:"multiInstance"}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.code,{children:"(optional=true)"}),"\nWhen you create more than one instance of the same database in a single javascript-runtime, you should set ",(0,n.jsx)(a.code,{children:"multiInstance"})," to ",(0,n.jsx)(a.code,{children:"true"}),". This will enable the event sharing between the two instances. For example when the user has opened multiple browser windows, events will be shared between them so that both windows react to the same changes.\n",(0,n.jsx)(a.code,{children:"multiInstance"})," should be set to ",(0,n.jsx)(a.code,{children:"false"})," when you have single-instances like a single Node.js-process, a react-native-app, a cordova-app or a single-window electron app which can decrease the startup time because no instance coordination has to be done."]}),"\n",(0,n.jsx)(a.h3,{id:"eventreduce",children:"eventReduce"}),"\n",(0,n.jsx)(a.p,{children:(0,n.jsx)(a.code,{children:"(optional=false)"})}),"\n",(0,n.jsxs)(a.p,{children:["One big benefit of having a realtime database is that big performance optimizations can be done when the database knows a query is observed and the updated results are needed continuously. RxDB uses the ",(0,n.jsx)(a.a,{href:"https://github.com/pubkey/event-reduce",children:"EventReduce Algorithm"})," to optimize observer or recurring queries."]}),"\n",(0,n.jsxs)(a.p,{children:["For better performance, you should always set ",(0,n.jsx)(a.code,{children:"eventReduce: true"}),". This will also be the default in the next major RxDB version."]}),"\n",(0,n.jsx)(a.h3,{id:"ignoreduplicate",children:"ignoreDuplicate"}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.code,{children:"(optional=false)"}),"\nIf you create multiple RxDatabase-instances with the same name and same adapter, it's very likely that you have done something wrong.\nTo prevent this common mistake, RxDB will throw an error when you do this.\nIn some rare cases like unit-tests, you want to do this intentional by setting ",(0,n.jsx)(a.code,{children:"ignoreDuplicate"})," to ",(0,n.jsx)(a.code,{children:"true"}),"."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-js",children:"const db1 = await createRxDatabase({\n name: 'heroesdb',\n storage: getRxStorageDexie(),\n ignoreDuplicate: true\n});\nconst db2 = await createRxDatabase({\n name: 'heroesdb',\n storage: getRxStorageDexie(),\n ignoreDuplicate: true // this create-call will not throw because you explicitly allow it\n});\n"})}),"\n",(0,n.jsx)(a.h2,{id:"methods",children:"Methods"}),"\n",(0,n.jsx)(a.h3,{id:"observe-with-",children:"Observe with $"}),"\n",(0,n.jsxs)(a.p,{children:["Calling this will return an ",(0,n.jsx)(a.a,{href:"http://reactivex.io/documentation/observable.html",children:"rxjs-Observable"})," which streams all write events of the ",(0,n.jsx)(a.code,{children:"RxDatabase"}),"."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"myDb.$.subscribe(changeEvent => console.dir(changeEvent));\n"})}),"\n",(0,n.jsx)(a.h3,{id:"exportjson",children:"exportJSON()"}),"\n",(0,n.jsxs)(a.p,{children:["Use this function to create a json-export from every piece of data in every collection of this database. You can pass ",(0,n.jsx)(a.code,{children:"true"})," as a parameter to decrypt the encrypted data-fields of your document."]}),"\n",(0,n.jsxs)(a.p,{children:["Before ",(0,n.jsx)(a.code,{children:"exportJSON()"})," and ",(0,n.jsx)(a.code,{children:"importJSON()"})," can be used, you have to add the ",(0,n.jsx)(a.code,{children:"json-dump"})," plugin."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBJsonDumpPlugin } from 'rxdb/plugins/json-dump';\naddRxPlugin(RxDBJsonDumpPlugin);\n"})}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"myDatabase.exportJSON()\n .then(json => console.dir(json));\n"})}),"\n",(0,n.jsx)(a.h3,{id:"importjson",children:"importJSON()"}),"\n",(0,n.jsx)(a.p,{children:"To import the json-dumps into your database, use this function."}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"// import the dump to the database\nemptyDatabase.importJSON(json)\n .then(() => console.log('done'));\n"})}),"\n",(0,n.jsx)(a.h3,{id:"backup",children:"backup()"}),"\n",(0,n.jsxs)(a.p,{children:["Writes the current (or ongoing) database state to the filesystem. ",(0,n.jsx)(a.a,{href:"/backup.html",children:"Read more"})]}),"\n",(0,n.jsx)(a.h3,{id:"waitforleadership",children:"waitForLeadership()"}),"\n",(0,n.jsxs)(a.p,{children:["Returns a Promise which resolves when the RxDatabase becomes ",(0,n.jsx)(a.a,{href:"/leader-election.html",children:"elected leader"}),"."]}),"\n",(0,n.jsx)(a.h3,{id:"requestidlepromise",children:"requestIdlePromise()"}),"\n",(0,n.jsxs)(a.p,{children:["Returns a promise which resolves when the database is in idle. This works similar to ",(0,n.jsx)(a.a,{href:"https://developer.mozilla.org/de/docs/Web/API/Window/requestIdleCallback",children:"requestIdleCallback"})," but tracks the idle-ness of the database instead of the CPU.\nUse this for semi-important tasks like cleanups which should not affect the speed of important tasks."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"\nmyDatabase.requestIdlePromise().then(() => {\n // this will run at the moment the database has nothing else to do\n myCollection.customCleanupFunction();\n});\n\n// with timeout\nmyDatabase.requestIdlePromise(1000 /* time in ms */).then(() => {\n // this will run at the moment the database has nothing else to do\n // or the timeout has passed\n myCollection.customCleanupFunction();\n});\n\n"})}),"\n",(0,n.jsx)(a.h3,{id:"destroy",children:"destroy()"}),"\n",(0,n.jsxs)(a.p,{children:["Destroys the databases object-instance. This is to free up memory and stop all observers and replications.\nReturns a ",(0,n.jsx)(a.code,{children:"Promise"})," that resolves when the database is destroyed."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"await myDatabase.destroy();\n"})}),"\n",(0,n.jsx)(a.h3,{id:"remove",children:"remove()"}),"\n",(0,n.jsx)(a.p,{children:"Wipes all documents from the storage. Use this to free up disc space."}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"await myDatabase.remove();\n// database instance is now gone\n\n// You can also clear a database without removing its instance\nimport { removeRxDatabase } from 'rxdb';\nremoveRxDatabase('mydatabasename', 'localstorage');\n"})}),"\n",(0,n.jsx)(a.h3,{id:"isrxdatabase",children:"isRxDatabase"}),"\n",(0,n.jsx)(a.p,{children:"Returns true if the given object is an instance of RxDatabase. Returns false if not."}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { isRxDatabase } from 'rxdb';\nconst is = isRxDatabase(myObj);\n"})})]})}function h(e={}){const{wrapper:a}={...(0,s.R)(),...e.components};return a?(0,n.jsx)(a,{...e,children:(0,n.jsx)(c,{...e})}):c(e)}},8453:(e,a,t)=>{t.d(a,{R:()=>i,x:()=>o});var n=t(6540);const s={},r=n.createContext(s);function i(e){const a=n.useContext(r);return n.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function o(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),n.createElement(r.Provider,{value:a},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/25a43fd4.28bb4bd8.js b/docs/assets/js/25a43fd4.28bb4bd8.js deleted file mode 100644 index 11e5a3ec80b..00000000000 --- a/docs/assets/js/25a43fd4.28bb4bd8.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[4812],{3917:(e,r,n)=>{n.r(r),n.d(r,{assets:()=>d,contentTitle:()=>s,default:()=>c,frontMatter:()=>o,metadata:()=>i,toc:()=>h});var a=n(4848),t=n(8453);const o={title:"SharedWorker RxStorage \ud83d\udc51",slug:"rx-storage-shared-worker.html"},s="SharedWorker RxStorage",i={id:"rx-storage-shared-worker",title:"SharedWorker RxStorage \ud83d\udc51",description:"The SharedWorker RxStorage uses the SharedWorker API to run the storage inside of a separate JavaScript process in browsers. Compared to a normal WebWorker, the SharedWorker is created exactly once, even when there are multiple browser tabs opened. Because of having exactly one worker, multiple performance optimizations can be done because the storage itself does not have to handle multiple opened database connections.",source:"@site/docs/rx-storage-shared-worker.md",sourceDirName:".",slug:"/rx-storage-shared-worker.html",permalink:"/rx-storage-shared-worker.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"SharedWorker RxStorage \ud83d\udc51",slug:"rx-storage-shared-worker.html"},sidebar:"tutorialSidebar",previous:{title:"Worker RxStorage \ud83d\udc51",permalink:"/rx-storage-worker.html"},next:{title:"Memory Synced RxStorage \ud83d\udc51",permalink:"/rx-storage-memory-synced.html"}},d={},h=[{value:"Usage",id:"usage",level:2},{value:"On the SharedWorker process",id:"on-the-sharedworker-process",level:3},{value:"On the main process",id:"on-the-main-process",level:3},{value:"Pre-build workers",id:"pre-build-workers",level:2},{value:"Building a custom worker",id:"building-a-custom-worker",level:2},{value:"Passing in a SharedWorker instance",id:"passing-in-a-sharedworker-instance",level:2},{value:"Replication with SharedWorker",id:"replication-with-sharedworker",level:2}];function l(e){const r={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,t.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(r.h1,{id:"sharedworker-rxstorage",children:"SharedWorker RxStorage"}),"\n",(0,a.jsxs)(r.p,{children:["The SharedWorker ",(0,a.jsx)(r.a,{href:"/rx-storage.html",children:"RxStorage"})," uses the ",(0,a.jsx)(r.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker",children:"SharedWorker API"})," to run the storage inside of a separate JavaScript process ",(0,a.jsx)(r.strong,{children:"in browsers"}),". Compared to a normal ",(0,a.jsx)(r.a,{href:"/rx-storage-worker.html",children:"WebWorker"}),", the SharedWorker is created exactly once, even when there are multiple browser tabs opened. Because of having exactly one worker, multiple performance optimizations can be done because the storage itself does not have to handle multiple opened database connections."]}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.strong,{children:"NOTICE:"})," This plugin is part of ",(0,a.jsx)(r.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]}),"\n",(0,a.jsx)(r.h2,{id:"usage",children:"Usage"}),"\n",(0,a.jsx)(r.h3,{id:"on-the-sharedworker-process",children:"On the SharedWorker process"}),"\n",(0,a.jsxs)(r.p,{children:["In the worker process JavaScript file, you have wrap the original RxStorage with ",(0,a.jsx)(r.code,{children:"getRxStorageIndexedDB()"}),"."]}),"\n",(0,a.jsx)(r.pre,{children:(0,a.jsx)(r.code,{className:"language-ts",children:"// shared-worker.ts\n\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\nimport { \n getRxStorageIndexedDB\n} from 'rxdb-premium/plugins/indexeddb';\n\nexposeWorkerRxStorage({\n /**\n * You can wrap any implementation of the RxStorage interface\n * into a worker.\n * Here we use the IndexedDB RxStorage.\n */\n storage: getRxStorageIndexedDB()\n});\n"})}),"\n",(0,a.jsx)(r.h3,{id:"on-the-main-process",children:"On the main process"}),"\n",(0,a.jsx)(r.pre,{children:(0,a.jsx)(r.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageSharedWorker } from 'rxdb-premium/plugins/storage-worker';\nimport { getRxStorageIndexedDB } from 'rxdb/plugins/storage-indexeddb';\n\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageSharedWorker(\n {\n /**\n * Contains any value that can be used as parameter\n * to the SharedWorker constructor of thread.js\n * Most likely you want to put the path to the shared-worker.js file in here.\n * \n * @link https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker?retiredLocale=de\n */\n workerInput: 'path/to/shared-worker.js',\n /**\n * (Optional) options\n * for the worker.\n */\n workerOptions: {\n type: 'module',\n credentials: 'omit'\n }\n }\n )\n});\n"})}),"\n",(0,a.jsx)(r.h2,{id:"pre-build-workers",children:"Pre-build workers"}),"\n",(0,a.jsxs)(r.p,{children:["The ",(0,a.jsx)(r.code,{children:"shared-worker.js"})," must be a self containing JavaScript file that contains all dependencies in a bundle.\nTo make it easier for you, RxDB ships with pre-bundles worker files that are ready to use.\nYou can find them in the folder ",(0,a.jsx)(r.code,{children:"node_modules/rxdb-premium/dist/workers"})," after you have installed the ",(0,a.jsx)(r.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51 Plugin"}),". From there you can copy them to a location where it can be served from the webserver and then use their path to create the ",(0,a.jsx)(r.code,{children:"RxDatabase"})]}),"\n",(0,a.jsxs)(r.p,{children:["Any valid ",(0,a.jsx)(r.code,{children:"worker.js"})," JavaScript file can be used both, for normal Workers and SharedWorkers."]}),"\n",(0,a.jsx)(r.pre,{children:(0,a.jsx)(r.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageSharedWorker } from 'rxdb-premium/plugins/storage-worker';\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageSharedWorker(\n {\n /**\n * Path to where the copied file from node_modules/rxdb-premium/dist/workers\n * is reachable from the webserver.\n */\n workerInput: '/indexeddb.shared-worker.js'\n }\n )\n});\n"})}),"\n",(0,a.jsx)(r.h2,{id:"building-a-custom-worker",children:"Building a custom worker"}),"\n",(0,a.jsxs)(r.p,{children:["To build a custom ",(0,a.jsx)(r.code,{children:"worker.js"})," file, check out the webpack config at the ",(0,a.jsx)(r.a,{href:"/rx-storage-worker.html#building-a-custom-worker",children:"worker"})," documentation. Any worker file form the worker storage can also be used in a shared worker because ",(0,a.jsx)(r.code,{children:"exposeWorkerRxStorage"})," detects where it runs and exposes the correct messaging endpoints."]}),"\n",(0,a.jsx)(r.h2,{id:"passing-in-a-sharedworker-instance",children:"Passing in a SharedWorker instance"}),"\n",(0,a.jsxs)(r.p,{children:["Instead of setting an url as ",(0,a.jsx)(r.code,{children:"workerInput"}),", you can also specify a function that returns a new ",(0,a.jsx)(r.code,{children:"SharedWorker"})," instance when called. This is mostly used when you have a custom worker file and dynamically import it.\nThis works equal to the ",(0,a.jsx)(r.a,{href:"/rx-storage-worker.html#passing-in-a-worker-instance",children:"workerInput of the Worker Storage"})]}),"\n",(0,a.jsx)(r.h2,{id:"replication-with-sharedworker",children:"Replication with SharedWorker"}),"\n",(0,a.jsxs)(r.p,{children:["When a SharedWorker RxStorage is used, it is recommended to run the replication ",(0,a.jsx)(r.strong,{children:"inside"})," of the worker. You can do that by opening another ",(0,a.jsx)(r.a,{href:"/rx-database.html",children:"RxDatabase"})," inside of it and starting the replication there."]}),"\n",(0,a.jsx)(r.pre,{children:(0,a.jsx)(r.code,{className:"language-ts",children:"// shared-worker.ts\n\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\nimport { \n getRxStorageIndexedDB\n} from 'rxdb-premium/plugins/storage-indexeddb';\nimport {\n createRxDatabase,\n addRxPlugin\n} from 'rxdb';\nimport {\n RxDBReplicationGraphQLPlugin\n} from 'rxdb/plugins/replication-graphql';\naddRxPlugin(RxDBReplicationGraphQLPlugin);\n\nconst baseStorage = getRxStorageIndexedDB();\n\n// first expose the RxStorage to the outside\nexposeWorkerRxStorage({\n storage: baseStorage\n});\n\n/**\n * Then create a normal RxDatabase and RxCollections\n * and start the replication.\n */\nconst database = await createRxDatabase({\n name: 'mydatabase',\n /**\n * Important: INSIDE of your SharedWorker, you can\n * be sure that there is exactly one instance running.\n * Therefore you MUST set multiInstance=false for better performance.\n */\n multiInstance: false,\n storage: baseStorage\n});\nawait db.addCollections({\n humans: {/* ... */}\n});\nconst replicationState = db.humans.syncGraphQL({/* ... */});\n"})})]})}function c(e={}){const{wrapper:r}={...(0,t.R)(),...e.components};return r?(0,a.jsx)(r,{...e,children:(0,a.jsx)(l,{...e})}):l(e)}},8453:(e,r,n)=>{n.d(r,{R:()=>s,x:()=>i});var a=n(6540);const t={},o=a.createContext(t);function s(e){const r=a.useContext(o);return a.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function i(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),a.createElement(o.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/25a43fd4.54504271.js b/docs/assets/js/25a43fd4.54504271.js new file mode 100644 index 00000000000..7b44dff3704 --- /dev/null +++ b/docs/assets/js/25a43fd4.54504271.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[4812],{3917:(e,r,n)=>{n.r(r),n.d(r,{assets:()=>d,contentTitle:()=>s,default:()=>c,frontMatter:()=>o,metadata:()=>i,toc:()=>h});var a=n(4848),t=n(8453);const o={title:"SharedWorker RxStorage \ud83d\udc51",slug:"rx-storage-shared-worker.html"},s="SharedWorker RxStorage",i={id:"rx-storage-shared-worker",title:"SharedWorker RxStorage \ud83d\udc51",description:"The SharedWorker RxStorage uses the SharedWorker API to run the storage inside of a separate JavaScript process in browsers. Compared to a normal WebWorker, the SharedWorker is created exactly once, even when there are multiple browser tabs opened. Because of having exactly one worker, multiple performance optimizations can be done because the storage itself does not have to handle multiple opened database connections.",source:"@site/docs/rx-storage-shared-worker.md",sourceDirName:".",slug:"/rx-storage-shared-worker.html",permalink:"/rx-storage-shared-worker.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"SharedWorker RxStorage \ud83d\udc51",slug:"rx-storage-shared-worker.html"},sidebar:"tutorialSidebar",previous:{title:"Worker RxStorage \ud83d\udc51",permalink:"/rx-storage-worker.html"},next:{title:"Memory Synced RxStorage \ud83d\udc51",permalink:"/rx-storage-memory-synced.html"}},d={},h=[{value:"Usage",id:"usage",level:2},{value:"On the SharedWorker process",id:"on-the-sharedworker-process",level:3},{value:"On the main process",id:"on-the-main-process",level:3},{value:"Pre-build workers",id:"pre-build-workers",level:2},{value:"Building a custom worker",id:"building-a-custom-worker",level:2},{value:"Passing in a SharedWorker instance",id:"passing-in-a-sharedworker-instance",level:2},{value:"Replication with SharedWorker",id:"replication-with-sharedworker",level:2}];function l(e){const r={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,t.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(r.h1,{id:"sharedworker-rxstorage",children:"SharedWorker RxStorage"}),"\n",(0,a.jsxs)(r.p,{children:["The SharedWorker ",(0,a.jsx)(r.a,{href:"/rx-storage.html",children:"RxStorage"})," uses the ",(0,a.jsx)(r.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker",children:"SharedWorker API"})," to run the storage inside of a separate JavaScript process ",(0,a.jsx)(r.strong,{children:"in browsers"}),". Compared to a normal ",(0,a.jsx)(r.a,{href:"/rx-storage-worker.html",children:"WebWorker"}),", the SharedWorker is created exactly once, even when there are multiple browser tabs opened. Because of having exactly one worker, multiple performance optimizations can be done because the storage itself does not have to handle multiple opened database connections."]}),"\n",(0,a.jsx)(r.admonition,{title:"Premium",type:"note",children:(0,a.jsxs)(r.p,{children:["This plugin is part of ",(0,a.jsx)(r.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]})}),"\n",(0,a.jsx)(r.h2,{id:"usage",children:"Usage"}),"\n",(0,a.jsx)(r.h3,{id:"on-the-sharedworker-process",children:"On the SharedWorker process"}),"\n",(0,a.jsxs)(r.p,{children:["In the worker process JavaScript file, you have wrap the original RxStorage with ",(0,a.jsx)(r.code,{children:"getRxStorageIndexedDB()"}),"."]}),"\n",(0,a.jsx)(r.pre,{children:(0,a.jsx)(r.code,{className:"language-ts",children:"// shared-worker.ts\n\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\nimport { \n getRxStorageIndexedDB\n} from 'rxdb-premium/plugins/indexeddb';\n\nexposeWorkerRxStorage({\n /**\n * You can wrap any implementation of the RxStorage interface\n * into a worker.\n * Here we use the IndexedDB RxStorage.\n */\n storage: getRxStorageIndexedDB()\n});\n"})}),"\n",(0,a.jsx)(r.h3,{id:"on-the-main-process",children:"On the main process"}),"\n",(0,a.jsx)(r.pre,{children:(0,a.jsx)(r.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageSharedWorker } from 'rxdb-premium/plugins/storage-worker';\nimport { getRxStorageIndexedDB } from 'rxdb/plugins/storage-indexeddb';\n\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageSharedWorker(\n {\n /**\n * Contains any value that can be used as parameter\n * to the SharedWorker constructor of thread.js\n * Most likely you want to put the path to the shared-worker.js file in here.\n * \n * @link https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker?retiredLocale=de\n */\n workerInput: 'path/to/shared-worker.js',\n /**\n * (Optional) options\n * for the worker.\n */\n workerOptions: {\n type: 'module',\n credentials: 'omit'\n }\n }\n )\n});\n"})}),"\n",(0,a.jsx)(r.h2,{id:"pre-build-workers",children:"Pre-build workers"}),"\n",(0,a.jsxs)(r.p,{children:["The ",(0,a.jsx)(r.code,{children:"shared-worker.js"})," must be a self containing JavaScript file that contains all dependencies in a bundle.\nTo make it easier for you, RxDB ships with pre-bundles worker files that are ready to use.\nYou can find them in the folder ",(0,a.jsx)(r.code,{children:"node_modules/rxdb-premium/dist/workers"})," after you have installed the ",(0,a.jsx)(r.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51 Plugin"}),". From there you can copy them to a location where it can be served from the webserver and then use their path to create the ",(0,a.jsx)(r.code,{children:"RxDatabase"})]}),"\n",(0,a.jsxs)(r.p,{children:["Any valid ",(0,a.jsx)(r.code,{children:"worker.js"})," JavaScript file can be used both, for normal Workers and SharedWorkers."]}),"\n",(0,a.jsx)(r.pre,{children:(0,a.jsx)(r.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageSharedWorker } from 'rxdb-premium/plugins/storage-worker';\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageSharedWorker(\n {\n /**\n * Path to where the copied file from node_modules/rxdb-premium/dist/workers\n * is reachable from the webserver.\n */\n workerInput: '/indexeddb.shared-worker.js'\n }\n )\n});\n"})}),"\n",(0,a.jsx)(r.h2,{id:"building-a-custom-worker",children:"Building a custom worker"}),"\n",(0,a.jsxs)(r.p,{children:["To build a custom ",(0,a.jsx)(r.code,{children:"worker.js"})," file, check out the webpack config at the ",(0,a.jsx)(r.a,{href:"/rx-storage-worker.html#building-a-custom-worker",children:"worker"})," documentation. Any worker file form the worker storage can also be used in a shared worker because ",(0,a.jsx)(r.code,{children:"exposeWorkerRxStorage"})," detects where it runs and exposes the correct messaging endpoints."]}),"\n",(0,a.jsx)(r.h2,{id:"passing-in-a-sharedworker-instance",children:"Passing in a SharedWorker instance"}),"\n",(0,a.jsxs)(r.p,{children:["Instead of setting an url as ",(0,a.jsx)(r.code,{children:"workerInput"}),", you can also specify a function that returns a new ",(0,a.jsx)(r.code,{children:"SharedWorker"})," instance when called. This is mostly used when you have a custom worker file and dynamically import it.\nThis works equal to the ",(0,a.jsx)(r.a,{href:"/rx-storage-worker.html#passing-in-a-worker-instance",children:"workerInput of the Worker Storage"})]}),"\n",(0,a.jsx)(r.h2,{id:"replication-with-sharedworker",children:"Replication with SharedWorker"}),"\n",(0,a.jsxs)(r.p,{children:["When a SharedWorker RxStorage is used, it is recommended to run the replication ",(0,a.jsx)(r.strong,{children:"inside"})," of the worker. You can do that by opening another ",(0,a.jsx)(r.a,{href:"/rx-database.html",children:"RxDatabase"})," inside of it and starting the replication there."]}),"\n",(0,a.jsx)(r.pre,{children:(0,a.jsx)(r.code,{className:"language-ts",children:"// shared-worker.ts\n\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\nimport { \n getRxStorageIndexedDB\n} from 'rxdb-premium/plugins/storage-indexeddb';\nimport {\n createRxDatabase,\n addRxPlugin\n} from 'rxdb';\nimport {\n RxDBReplicationGraphQLPlugin\n} from 'rxdb/plugins/replication-graphql';\naddRxPlugin(RxDBReplicationGraphQLPlugin);\n\nconst baseStorage = getRxStorageIndexedDB();\n\n// first expose the RxStorage to the outside\nexposeWorkerRxStorage({\n storage: baseStorage\n});\n\n/**\n * Then create a normal RxDatabase and RxCollections\n * and start the replication.\n */\nconst database = await createRxDatabase({\n name: 'mydatabase',\n /**\n * Important: INSIDE of your SharedWorker, you can\n * be sure that there is exactly one instance running.\n * Therefore you MUST set multiInstance=false for better performance.\n */\n multiInstance: false,\n storage: baseStorage\n});\nawait db.addCollections({\n humans: {/* ... */}\n});\nconst replicationState = db.humans.syncGraphQL({/* ... */});\n"})})]})}function c(e={}){const{wrapper:r}={...(0,t.R)(),...e.components};return r?(0,a.jsx)(r,{...e,children:(0,a.jsx)(l,{...e})}):l(e)}},8453:(e,r,n)=>{n.d(r,{R:()=>s,x:()=>i});var a=n(6540);const t={},o=a.createContext(t);function s(e){const r=a.useContext(o);return a.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function i(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),a.createElement(o.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/39600c95.48bb2633.js b/docs/assets/js/39600c95.48bb2633.js deleted file mode 100644 index c5da642331e..00000000000 --- a/docs/assets/js/39600c95.48bb2633.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3148],{755:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>u,frontMatter:()=>r,metadata:()=>i,toc:()=>c});var o=t(4848),s=t(8453);const r={title:"RxQuery",slug:"rx-query.html"},a="RxQuery",i={id:"rx-query",title:"RxQuery",description:"A query allows to find documents in your collection.",source:"@site/docs/rx-query.md",sourceDirName:".",slug:"/rx-query.html",permalink:"/rx-query.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxQuery",slug:"rx-query.html"},sidebar:"tutorialSidebar",previous:{title:"RxDocument",permalink:"/rx-document.html"},next:{title:"Attachments",permalink:"/rx-attachment.html"}},l={},c=[{value:"find()",id:"find",level:2},{value:"findOne()",id:"findone",level:2},{value:"exec()",id:"exec",level:2},{value:"Query Builder",id:"query-builder",level:2},{value:"Observe $",id:"observe-",level:2},{value:"update()",id:"update",level:2},{value:"remove()",id:"remove",level:2},{value:"doesDocumentDataMatch()",id:"doesdocumentdatamatch",level:2},{value:"Query Examples",id:"query-examples",level:2},{value:"Setting a specific index",id:"setting-a-specific-index",level:2},{value:"Count",id:"count",level:2},{value:"allowSlowCount",id:"allowslowcount",level:3},{value:"NOTICE: RxDB will always append the primary key to the sort parameters",id:"notice-rxdb-will-always-append-the-primary-key-to-the-sort-parameters",level:2},{value:"NOTICE: RxQuery's are immutable",id:"notice-rxquerys-are-immutable",level:2},{value:"isRxQuery",id:"isrxquery",level:3}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"rxquery",children:"RxQuery"}),"\n",(0,o.jsxs)(n.p,{children:["A query allows to find documents in your collection.\nLike most other noSQL-Databases, RxDB uses the ",(0,o.jsx)(n.a,{href:"https://github.com/cloudant/mango",children:"mango-query-syntax"}),". It is also possible to use ",(0,o.jsx)(n.a,{href:"https://docs.mongodb.com/manual/reference/method/db.collection.find/#combine-cursor-methods",children:"chained methods"})," with the ",(0,o.jsx)(n.code,{children:"query-builder"})," plugin."]}),"\n",(0,o.jsx)(n.h2,{id:"find",children:"find()"}),"\n",(0,o.jsxs)(n.p,{children:["To create a basic ",(0,o.jsx)(n.code,{children:"RxQuery"}),", call ",(0,o.jsx)(n.code,{children:".find()"})," on a collection and insert selectors. The result-set of normal queries is an array with documents."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find all that are older then 18\nconst query = myCollection\n .find({\n selector: {\n age: {\n $gt: 18\n }\n }\n });\n"})}),"\n",(0,o.jsx)(n.h2,{id:"findone",children:"findOne()"}),"\n",(0,o.jsxs)(n.p,{children:["A findOne-query has only a single ",(0,o.jsx)(n.code,{children:"RxDocument"})," or ",(0,o.jsx)(n.code,{children:"null"})," as result-set."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find alice\nconst query = myCollection\n .findOne({\n selector: {\n name: 'alice'\n }\n });\n"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find the youngest one\nconst query = myCollection\n .findOne({\n selector: {},\n sort: [\n {age: 'asc'}\n ]\n });\n"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find one document by the primary key\nconst query = myCollection.findOne('foobar');\n"})}),"\n",(0,o.jsx)(n.h2,{id:"exec",children:"exec()"}),"\n",(0,o.jsxs)(n.p,{children:["Returns a ",(0,o.jsx)(n.code,{children:"Promise"})," that resolves with the result-set of the query."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const query = myCollection.find();\nconst results = await query.exec();\nconsole.dir(results); // > [RxDocument,RxDocument,RxDocument..]\n"})}),"\n",(0,o.jsx)(n.h2,{id:"query-builder",children:"Query Builder"}),"\n",(0,o.jsxs)(n.p,{children:["To use chained query methods, you can use the ",(0,o.jsx)(n.code,{children:"query-builder"})," plugin."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"\n// add the query builder plugin\nimport { addRxPlugin } from 'rxdb';\nimport { RxDBQueryBuilderPlugin } from 'rxdb/plugins/query-builder';\naddRxPlugin(RxDBQueryBuilderPlugin);\n\n// now you can use chained query methods\n\nconst query = myCollection.find().where('age').gt(18);\n"})}),"\n",(0,o.jsx)(n.h2,{id:"observe-",children:"Observe $"}),"\n",(0,o.jsxs)(n.p,{children:["An ",(0,o.jsx)(n.code,{children:"BehaviorSubject"})," ",(0,o.jsx)(n.a,{href:"https://medium.com/@luukgruijs/understanding-rxjs-behaviorsubject-replaysubject-and-asyncsubject-8cc061f1cfc0",children:"see"})," that always has the current result-set as value.\nThis is extremely helpful when used together with UIs that should always show the same state as what is written in the database."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const query = myCollection.find();\nconst querySub = query.$.subscribe(results => {\n console.log('got results: ' + results.length);\n});\n// > 'got results: 5' // BehaviorSubjects emit on subscription\n\nawait myCollection.insert({/* ... */}); // insert one\n// > 'got results: 6' // $.subscribe() was called again with the new results\n\n// stop watching this query\nquerySub.unsubscribe()\n"})}),"\n",(0,o.jsx)(n.h2,{id:"update",children:"update()"}),"\n",(0,o.jsxs)(n.p,{children:["Runs an ",(0,o.jsx)(n.a,{href:"/rx-document.html#update",children:"update"})," on every RxDocument of the query-result."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"\n// to use the update() method, you need to add the update plugin.\nimport { RxDBUpdatePlugin } from 'rxdb/plugins/update';\naddRxPlugin(RxDBUpdatePlugin);\n\n\nconst query = myCollection.find({\n selector: {\n age: {\n $gt: 18\n }\n }\n});\nawait query.update({\n $inc: {\n age: 1 // increases age of every found document by 1\n }\n});\n"})}),"\n",(0,o.jsx)(n.h2,{id:"remove",children:"remove()"}),"\n",(0,o.jsx)(n.p,{children:"Deletes all found documents. Returns a promise which resolves to the deleted documents."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"// All documents where the age is less than 18\nconst query = myCollection.find({\n selector: {\n age: {\n $lt: 18\n }\n }\n});\n// Remove the documents from the collection\nconst removedDocs = await query.remove();\n"})}),"\n",(0,o.jsx)(n.h2,{id:"doesdocumentdatamatch",children:"doesDocumentDataMatch()"}),"\n",(0,o.jsxs)(n.p,{children:["Returns ",(0,o.jsx)(n.code,{children:"true"})," if the given document data matches the query."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const documentData = {\n id: 'foobar',\n age: 19\n};\n\nmyCollection.find({\n selector: {\n age: {\n $gt: 18\n }\n }\n}).doesDocumentDataMatch(documentData); // > true\n\nmyCollection.find({\n selector: {\n age: {\n $gt: 20\n }\n }\n}).doesDocumentDataMatch(documentData); // > false\n"})}),"\n",(0,o.jsx)(n.h2,{id:"query-examples",children:"Query Examples"}),"\n",(0,o.jsx)(n.p,{children:"Here some examples to fast learn how to write queries without reading the docs."}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.a,{href:"https://github.com/pouchdb/pouchdb/blob/master/packages/node_modules/pouchdb-find/README.md",children:"Pouch-find-docs"})," - learn how to use mango-queries"]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.a,{href:"https://github.com/aheckmann/mquery/blob/master/README.md",children:"mquery-docs"})," - learn how to use chained-queries"]}),"\n"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// directly pass search-object\nmyCollection.find({\n selector: {\n name: { $eq: 'foo' }\n }\n})\n.exec().then(documents => console.dir(documents));\n\n/*\n * find by using sql equivalent '%like%' syntax\n * This example will fe: match 'foo' but also 'fifoo' or 'foofa' or 'fifoofa'\n * Notice that in RxDB queries, a regex is represented as a $regex string with the $options parameter for flags.\n * Using a RegExp instance is not allowed because they are not JSON.stringify()-able and also\n * RegExp instances are mutable which could cause undefined behavior when the RegExp is mutated\n * after the query was parsed.\n */\nmyCollection.find({\n selector: {\n name: { $regex: '.*foo.*' }\n }\n})\n.exec().then(documents => console.dir(documents));\n\n// find using a composite statement eg: $or\n// This example checks where name is either foo or if name is not existent on the document\nmyCollection.find({\n selector: { $or: [ { name: { $eq: 'foo' } }, { name: { $exists: false } }] }\n})\n.exec().then(documents => console.dir(documents));\n\n// do a case insensitive search\n// This example will match 'foo' or 'FOO' or 'FoO' etc...\nmyCollection.find({\n selector: { name: { $regex: '^foo$', $options: 'i' } }\n})\n.exec().then(documents => console.dir(documents));\n\n// chained queries\nmyCollection.find().where('name').eq('foo')\n.exec().then(documents => console.dir(documents));\n"})}),"\n",(0,o.jsx)(n.h2,{id:"setting-a-specific-index",children:"Setting a specific index"}),"\n",(0,o.jsx)(n.p,{children:"By default, the query will be send to the RxStorage, where a query planner will determine which one of the available indexes must be used.\nBut the query planner cannot know everything and sometimes will not pick the most optimal index.\nTo improve query performance, you can specify which index must be used, when running the query."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"const query = myCollection\n .findOne({\n selector: {\n age: {\n $gt: 18\n },\n gender: {\n $eq: 'm'\n }\n },\n /**\n * Because the developer knows that 50% of the documents are 'male',\n * but only 20% are below age 18,\n * it makes sense to enforce using the ['gender', 'age'] index to improve performance.\n * This could not be known by the query planer which might have chosen ['age', 'gender'] instead.\n */\n index: ['gender', 'age']\n });\n"})}),"\n",(0,o.jsx)(n.h2,{id:"count",children:"Count"}),"\n",(0,o.jsxs)(n.p,{children:["When you only need the amount of documents that match a query, but you do not need the document data itself, you can use a count query for ",(0,o.jsx)(n.strong,{children:"better performance"}),".\nThe performance difference compared to a normal query differs depending on which ",(0,o.jsx)(n.a,{href:"/rx-storage.html",children:"RxStorage"})," implementation is used."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"const query = myCollection.count({\n selector: {\n age: {\n $gt: 18\n }\n }\n // 'limit' and 'skip' MUST NOT be set for count queries.\n});\n\n// get the count result once\nconst matchingAmount = await query.exec(); // > number\n\n// observe the result\nquery.$.subscribe(amount => {\n console.log('Currently has ' + amount + ' documents');\n});\n"})}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"IMPORTANT:"})," count queries have a better performance than normal queries because they do not have to fetch the full document data out of the storage. Therefore it is ",(0,o.jsx)(n.strong,{children:"not"})," possible to run a ",(0,o.jsx)(n.code,{children:"count()"})," query with a selector that requires to fetch and compare the document data. So if your query selector ",(0,o.jsx)(n.strong,{children:"does not"})," fully match an index of the schema, it is not allowed to run it. These queries would have no performance benefit compared to normal queries but have the tradeoff of not using the fetched document data for caching."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"/**\n * The following will throw an error because\n * the count operation cannot run on any specific index range\n * because the $regex operator is used.\n */\nconst query = myCollection.count({\n selector: {\n age: {\n $regex: 'foobar'\n }\n }\n});\n\n/**\n * The following will throw an error because\n * the count operation cannot run on any specific index range\n * because there is no ['age' ,'otherNumber'] index\n * defined in the schema.\n */\nconst query = myCollection.count({\n selector: {\n age: {\n $gt: 20\n },\n otherNumber: {\n $gt: 10\n }\n }\n});\n"})}),"\n",(0,o.jsx)(n.p,{children:"If you want to count these kind of queries, you should do a normal query instead and use the length of the result set as counter. This has the same performance as running a non-fully-indexed count which has to fetch all document data from the database and run a query matcher."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"// get count manually once\nconst resultSet = await myCollection.find({\n selector: {\n age: {\n $regex: 'foobar'\n }\n }\n}).exec();\nconst count = resultSet.length;\n\n// observe count manually\nconst count$ = myCollection.find({\n selector: {\n age: {\n $regex: 'foobar'\n }\n }\n}).$.pipe(\n map(result => result.length)\n);\n\n/**\n * To allow non-fully-indexed count queries,\n * you can also specify that by setting allowSlowCount=true\n * when creating the database.\n */\nconst database = await createRxDatabase({\n name: 'mydatabase',\n allowSlowCount: true, // set this to true [default=false]\n /* ... */\n});\n"})}),"\n",(0,o.jsx)(n.h3,{id:"allowslowcount",children:(0,o.jsx)(n.code,{children:"allowSlowCount"})}),"\n",(0,o.jsxs)(n.p,{children:["To allow non-fully-indexed count queries, you can also specify that by setting ",(0,o.jsx)(n.code,{children:"allowSlowCount: true"})," when creating the database.\nDoing this is mostly not wanted, because it would run the counting on the storage without having the document stored in the RxDB document cache.\nThis is only recommended if the RxStorage is running remotely like in a WebWorker and you not always want to send the document-data between the worker and the main thread. In this case you might only need the count-result instead to save performance."]}),"\n",(0,o.jsx)(n.h2,{id:"notice-rxdb-will-always-append-the-primary-key-to-the-sort-parameters",children:"NOTICE: RxDB will always append the primary key to the sort parameters"}),"\n",(0,o.jsxs)(n.p,{children:["For several performance optimizations, like the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/event-reduce",children:"EventReduce algorithm"}),", RxDB expects all queries to return a deterministic sort order that does not depend on the insert order of the documents. To ensure a deterministic ordering, RxDB will always append the primary key as last sort parameter to all queries and to all indexes.\nThis works in contrast to most other databases where a query without sorting would return the documents in the order in which they had been inserted to the database."]}),"\n",(0,o.jsx)(n.h2,{id:"notice-rxquerys-are-immutable",children:"NOTICE: RxQuery's are immutable"}),"\n",(0,o.jsxs)(n.p,{children:["Because RxDB is a reactive database, we can do heavy performance-optimisation on query-results which change over time. To be able to do this, RxQuery's have to be immutable.\nThis means, when you have a ",(0,o.jsx)(n.code,{children:"RxQuery"})," and run a ",(0,o.jsx)(n.code,{children:".where()"})," on it, the original RxQuery-Object is not changed. Instead the where-function returns a new ",(0,o.jsx)(n.code,{children:"RxQuery"}),"-Object with the changed where-field. Keep this in mind if you create RxQuery's and change them afterwards."]}),"\n",(0,o.jsx)(n.p,{children:"Example:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"const queryObject = myCollection.find().where('age').gt(18);\n// Creates a new RxQuery object, does not modify previous one\nqueryObject.sort('name');\nconst results = await queryObject.exec();\nconsole.dir(results); // result-documents are not sorted by name\n\nconst queryObjectSort = queryObject.sort('name');\nconst results = await queryObjectSort.exec();\nconsole.dir(results); // result-documents are now sorted\n"})}),"\n",(0,o.jsx)(n.h3,{id:"isrxquery",children:"isRxQuery"}),"\n",(0,o.jsx)(n.p,{children:"Returns true if the given object is an instance of RxQuery. Returns false if not."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const is = isRxQuery(myObj);\n"})})]})}function u(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>i});var o=t(6540);const s={},r=o.createContext(s);function a(e){const n=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),o.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/39600c95.6284324e.js b/docs/assets/js/39600c95.6284324e.js new file mode 100644 index 00000000000..13f48e1f2eb --- /dev/null +++ b/docs/assets/js/39600c95.6284324e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3148],{755:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>u,frontMatter:()=>r,metadata:()=>i,toc:()=>c});var o=t(4848),s=t(8453);const r={title:"RxQuery",slug:"rx-query.html"},a="RxQuery",i={id:"rx-query",title:"RxQuery",description:"A query allows to find documents in your collection.",source:"@site/docs/rx-query.md",sourceDirName:".",slug:"/rx-query.html",permalink:"/rx-query.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxQuery",slug:"rx-query.html"},sidebar:"tutorialSidebar",previous:{title:"RxDocument",permalink:"/rx-document.html"},next:{title:"Attachments",permalink:"/rx-attachment.html"}},l={},c=[{value:"find()",id:"find",level:2},{value:"findOne()",id:"findone",level:2},{value:"exec()",id:"exec",level:2},{value:"Query Builder",id:"query-builder",level:2},{value:"Observe $",id:"observe-",level:2},{value:"update()",id:"update",level:2},{value:"remove()",id:"remove",level:2},{value:"doesDocumentDataMatch()",id:"doesdocumentdatamatch",level:2},{value:"Query Examples",id:"query-examples",level:2},{value:"Setting a specific index",id:"setting-a-specific-index",level:2},{value:"Count",id:"count",level:2},{value:"allowSlowCount",id:"allowslowcount",level:3},{value:"RxDB will always append the primary key to the sort parameters",id:"rxdb-will-always-append-the-primary-key-to-the-sort-parameters",level:2},{value:"RxQuery's are immutable",id:"rxquerys-are-immutable",level:2},{value:"isRxQuery",id:"isrxquery",level:3}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"rxquery",children:"RxQuery"}),"\n",(0,o.jsxs)(n.p,{children:["A query allows to find documents in your collection.\nLike most other noSQL-Databases, RxDB uses the ",(0,o.jsx)(n.a,{href:"https://github.com/cloudant/mango",children:"mango-query-syntax"}),". It is also possible to use ",(0,o.jsx)(n.a,{href:"https://docs.mongodb.com/manual/reference/method/db.collection.find/#combine-cursor-methods",children:"chained methods"})," with the ",(0,o.jsx)(n.code,{children:"query-builder"})," plugin."]}),"\n",(0,o.jsx)(n.h2,{id:"find",children:"find()"}),"\n",(0,o.jsxs)(n.p,{children:["To create a basic ",(0,o.jsx)(n.code,{children:"RxQuery"}),", call ",(0,o.jsx)(n.code,{children:".find()"})," on a collection and insert selectors. The result-set of normal queries is an array with documents."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find all that are older then 18\nconst query = myCollection\n .find({\n selector: {\n age: {\n $gt: 18\n }\n }\n });\n"})}),"\n",(0,o.jsx)(n.h2,{id:"findone",children:"findOne()"}),"\n",(0,o.jsxs)(n.p,{children:["A findOne-query has only a single ",(0,o.jsx)(n.code,{children:"RxDocument"})," or ",(0,o.jsx)(n.code,{children:"null"})," as result-set."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find alice\nconst query = myCollection\n .findOne({\n selector: {\n name: 'alice'\n }\n });\n"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find the youngest one\nconst query = myCollection\n .findOne({\n selector: {},\n sort: [\n {age: 'asc'}\n ]\n });\n"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find one document by the primary key\nconst query = myCollection.findOne('foobar');\n"})}),"\n",(0,o.jsx)(n.h2,{id:"exec",children:"exec()"}),"\n",(0,o.jsxs)(n.p,{children:["Returns a ",(0,o.jsx)(n.code,{children:"Promise"})," that resolves with the result-set of the query."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const query = myCollection.find();\nconst results = await query.exec();\nconsole.dir(results); // > [RxDocument,RxDocument,RxDocument..]\n"})}),"\n",(0,o.jsx)(n.h2,{id:"query-builder",children:"Query Builder"}),"\n",(0,o.jsxs)(n.p,{children:["To use chained query methods, you can use the ",(0,o.jsx)(n.code,{children:"query-builder"})," plugin."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"\n// add the query builder plugin\nimport { addRxPlugin } from 'rxdb';\nimport { RxDBQueryBuilderPlugin } from 'rxdb/plugins/query-builder';\naddRxPlugin(RxDBQueryBuilderPlugin);\n\n// now you can use chained query methods\n\nconst query = myCollection.find().where('age').gt(18);\n"})}),"\n",(0,o.jsx)(n.h2,{id:"observe-",children:"Observe $"}),"\n",(0,o.jsxs)(n.p,{children:["An ",(0,o.jsx)(n.code,{children:"BehaviorSubject"})," ",(0,o.jsx)(n.a,{href:"https://medium.com/@luukgruijs/understanding-rxjs-behaviorsubject-replaysubject-and-asyncsubject-8cc061f1cfc0",children:"see"})," that always has the current result-set as value.\nThis is extremely helpful when used together with UIs that should always show the same state as what is written in the database."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const query = myCollection.find();\nconst querySub = query.$.subscribe(results => {\n console.log('got results: ' + results.length);\n});\n// > 'got results: 5' // BehaviorSubjects emit on subscription\n\nawait myCollection.insert({/* ... */}); // insert one\n// > 'got results: 6' // $.subscribe() was called again with the new results\n\n// stop watching this query\nquerySub.unsubscribe()\n"})}),"\n",(0,o.jsx)(n.h2,{id:"update",children:"update()"}),"\n",(0,o.jsxs)(n.p,{children:["Runs an ",(0,o.jsx)(n.a,{href:"/rx-document.html#update",children:"update"})," on every RxDocument of the query-result."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"\n// to use the update() method, you need to add the update plugin.\nimport { RxDBUpdatePlugin } from 'rxdb/plugins/update';\naddRxPlugin(RxDBUpdatePlugin);\n\n\nconst query = myCollection.find({\n selector: {\n age: {\n $gt: 18\n }\n }\n});\nawait query.update({\n $inc: {\n age: 1 // increases age of every found document by 1\n }\n});\n"})}),"\n",(0,o.jsx)(n.h2,{id:"remove",children:"remove()"}),"\n",(0,o.jsx)(n.p,{children:"Deletes all found documents. Returns a promise which resolves to the deleted documents."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"// All documents where the age is less than 18\nconst query = myCollection.find({\n selector: {\n age: {\n $lt: 18\n }\n }\n});\n// Remove the documents from the collection\nconst removedDocs = await query.remove();\n"})}),"\n",(0,o.jsx)(n.h2,{id:"doesdocumentdatamatch",children:"doesDocumentDataMatch()"}),"\n",(0,o.jsxs)(n.p,{children:["Returns ",(0,o.jsx)(n.code,{children:"true"})," if the given document data matches the query."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const documentData = {\n id: 'foobar',\n age: 19\n};\n\nmyCollection.find({\n selector: {\n age: {\n $gt: 18\n }\n }\n}).doesDocumentDataMatch(documentData); // > true\n\nmyCollection.find({\n selector: {\n age: {\n $gt: 20\n }\n }\n}).doesDocumentDataMatch(documentData); // > false\n"})}),"\n",(0,o.jsx)(n.h2,{id:"query-examples",children:"Query Examples"}),"\n",(0,o.jsx)(n.p,{children:"Here some examples to fast learn how to write queries without reading the docs."}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.a,{href:"https://github.com/pouchdb/pouchdb/blob/master/packages/node_modules/pouchdb-find/README.md",children:"Pouch-find-docs"})," - learn how to use mango-queries"]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.a,{href:"https://github.com/aheckmann/mquery/blob/master/README.md",children:"mquery-docs"})," - learn how to use chained-queries"]}),"\n"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// directly pass search-object\nmyCollection.find({\n selector: {\n name: { $eq: 'foo' }\n }\n})\n.exec().then(documents => console.dir(documents));\n\n/*\n * find by using sql equivalent '%like%' syntax\n * This example will fe: match 'foo' but also 'fifoo' or 'foofa' or 'fifoofa'\n * Notice that in RxDB queries, a regex is represented as a $regex string with the $options parameter for flags.\n * Using a RegExp instance is not allowed because they are not JSON.stringify()-able and also\n * RegExp instances are mutable which could cause undefined behavior when the RegExp is mutated\n * after the query was parsed.\n */\nmyCollection.find({\n selector: {\n name: { $regex: '.*foo.*' }\n }\n})\n.exec().then(documents => console.dir(documents));\n\n// find using a composite statement eg: $or\n// This example checks where name is either foo or if name is not existent on the document\nmyCollection.find({\n selector: { $or: [ { name: { $eq: 'foo' } }, { name: { $exists: false } }] }\n})\n.exec().then(documents => console.dir(documents));\n\n// do a case insensitive search\n// This example will match 'foo' or 'FOO' or 'FoO' etc...\nmyCollection.find({\n selector: { name: { $regex: '^foo$', $options: 'i' } }\n})\n.exec().then(documents => console.dir(documents));\n\n// chained queries\nmyCollection.find().where('name').eq('foo')\n.exec().then(documents => console.dir(documents));\n"})}),"\n",(0,o.jsx)(n.h2,{id:"setting-a-specific-index",children:"Setting a specific index"}),"\n",(0,o.jsx)(n.p,{children:"By default, the query will be send to the RxStorage, where a query planner will determine which one of the available indexes must be used.\nBut the query planner cannot know everything and sometimes will not pick the most optimal index.\nTo improve query performance, you can specify which index must be used, when running the query."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"const query = myCollection\n .findOne({\n selector: {\n age: {\n $gt: 18\n },\n gender: {\n $eq: 'm'\n }\n },\n /**\n * Because the developer knows that 50% of the documents are 'male',\n * but only 20% are below age 18,\n * it makes sense to enforce using the ['gender', 'age'] index to improve performance.\n * This could not be known by the query planer which might have chosen ['age', 'gender'] instead.\n */\n index: ['gender', 'age']\n });\n"})}),"\n",(0,o.jsx)(n.h2,{id:"count",children:"Count"}),"\n",(0,o.jsxs)(n.p,{children:["When you only need the amount of documents that match a query, but you do not need the document data itself, you can use a count query for ",(0,o.jsx)(n.strong,{children:"better performance"}),".\nThe performance difference compared to a normal query differs depending on which ",(0,o.jsx)(n.a,{href:"/rx-storage.html",children:"RxStorage"})," implementation is used."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"const query = myCollection.count({\n selector: {\n age: {\n $gt: 18\n }\n }\n // 'limit' and 'skip' MUST NOT be set for count queries.\n});\n\n// get the count result once\nconst matchingAmount = await query.exec(); // > number\n\n// observe the result\nquery.$.subscribe(amount => {\n console.log('Currently has ' + amount + ' documents');\n});\n"})}),"\n",(0,o.jsx)(n.admonition,{type:"note",children:(0,o.jsxs)(n.p,{children:["Count queries have a better performance than normal queries because they do not have to fetch the full document data out of the storage. Therefore it is ",(0,o.jsx)(n.strong,{children:"not"})," possible to run a ",(0,o.jsx)(n.code,{children:"count()"})," query with a selector that requires to fetch and compare the document data. So if your query selector ",(0,o.jsx)(n.strong,{children:"does not"})," fully match an index of the schema, it is not allowed to run it. These queries would have no performance benefit compared to normal queries but have the tradeoff of not using the fetched document data for caching."]})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"/**\n * The following will throw an error because\n * the count operation cannot run on any specific index range\n * because the $regex operator is used.\n */\nconst query = myCollection.count({\n selector: {\n age: {\n $regex: 'foobar'\n }\n }\n});\n\n/**\n * The following will throw an error because\n * the count operation cannot run on any specific index range\n * because there is no ['age' ,'otherNumber'] index\n * defined in the schema.\n */\nconst query = myCollection.count({\n selector: {\n age: {\n $gt: 20\n },\n otherNumber: {\n $gt: 10\n }\n }\n});\n"})}),"\n",(0,o.jsx)(n.p,{children:"If you want to count these kind of queries, you should do a normal query instead and use the length of the result set as counter. This has the same performance as running a non-fully-indexed count which has to fetch all document data from the database and run a query matcher."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"// get count manually once\nconst resultSet = await myCollection.find({\n selector: {\n age: {\n $regex: 'foobar'\n }\n }\n}).exec();\nconst count = resultSet.length;\n\n// observe count manually\nconst count$ = myCollection.find({\n selector: {\n age: {\n $regex: 'foobar'\n }\n }\n}).$.pipe(\n map(result => result.length)\n);\n\n/**\n * To allow non-fully-indexed count queries,\n * you can also specify that by setting allowSlowCount=true\n * when creating the database.\n */\nconst database = await createRxDatabase({\n name: 'mydatabase',\n allowSlowCount: true, // set this to true [default=false]\n /* ... */\n});\n"})}),"\n",(0,o.jsx)(n.h3,{id:"allowslowcount",children:(0,o.jsx)(n.code,{children:"allowSlowCount"})}),"\n",(0,o.jsxs)(n.p,{children:["To allow non-fully-indexed count queries, you can also specify that by setting ",(0,o.jsx)(n.code,{children:"allowSlowCount: true"})," when creating the database.\nDoing this is mostly not wanted, because it would run the counting on the storage without having the document stored in the RxDB document cache.\nThis is only recommended if the RxStorage is running remotely like in a WebWorker and you not always want to send the document-data between the worker and the main thread. In this case you might only need the count-result instead to save performance."]}),"\n",(0,o.jsx)(n.h2,{id:"rxdb-will-always-append-the-primary-key-to-the-sort-parameters",children:"RxDB will always append the primary key to the sort parameters"}),"\n",(0,o.jsxs)(n.p,{children:["For several performance optimizations, like the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/event-reduce",children:"EventReduce algorithm"}),", RxDB expects all queries to return a deterministic sort order that does not depend on the insert order of the documents. To ensure a deterministic ordering, RxDB will always append the primary key as last sort parameter to all queries and to all indexes.\nThis works in contrast to most other databases where a query without sorting would return the documents in the order in which they had been inserted to the database.\n:::"]}),"\n",(0,o.jsx)(n.h2,{id:"rxquerys-are-immutable",children:"RxQuery's are immutable"}),"\n",(0,o.jsxs)(n.p,{children:["Because RxDB is a reactive database, we can do heavy performance-optimisation on query-results which change over time. To be able to do this, RxQuery's have to be immutable.\nThis means, when you have a ",(0,o.jsx)(n.code,{children:"RxQuery"})," and run a ",(0,o.jsx)(n.code,{children:".where()"})," on it, the original RxQuery-Object is not changed. Instead the where-function returns a new ",(0,o.jsx)(n.code,{children:"RxQuery"}),"-Object with the changed where-field. Keep this in mind if you create RxQuery's and change them afterwards."]}),"\n",(0,o.jsx)(n.p,{children:"Example:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"const queryObject = myCollection.find().where('age').gt(18);\n// Creates a new RxQuery object, does not modify previous one\nqueryObject.sort('name');\nconst results = await queryObject.exec();\nconsole.dir(results); // result-documents are not sorted by name\n\nconst queryObjectSort = queryObject.sort('name');\nconst results = await queryObjectSort.exec();\nconsole.dir(results); // result-documents are now sorted\n"})}),"\n",(0,o.jsx)(n.h3,{id:"isrxquery",children:"isRxQuery"}),"\n",(0,o.jsx)(n.p,{children:"Returns true if the given object is an instance of RxQuery. Returns false if not."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const is = isRxQuery(myObj);\n"})})]})}function u(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>i});var o=t(6540);const s={},r=o.createContext(s);function a(e){const n=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),o.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/4adf80bb.9e0ea7b4.js b/docs/assets/js/4adf80bb.9e0ea7b4.js new file mode 100644 index 00000000000..e8577a9a2d2 --- /dev/null +++ b/docs/assets/js/4adf80bb.9e0ea7b4.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9548],{4442:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>d,frontMatter:()=>s,metadata:()=>i,toc:()=>h});var t=o(4848),a=o(8453);const s={title:"PouchDB RxStorage",slug:"rx-storage-pouchdb.html"},r="RxStorage PouchDB",i={id:"rx-storage-pouchdb",title:"PouchDB RxStorage",description:"The PouchDB RxStorage is based on the PouchDB database. It is the most battle proven RxStorage and has a big ecosystem of adapters. PouchDB does a lot of overhead to enable CouchDB replication which makes the PouchDB RxStorage one of the slowest.",source:"@site/docs/rx-storage-pouchdb.md",sourceDirName:".",slug:"/rx-storage-pouchdb.html",permalink:"/rx-storage-pouchdb.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"PouchDB RxStorage",slug:"rx-storage-pouchdb.html"}},l={},h=[{value:"Pros",id:"pros",level:2},{value:"Cons",id:"cons",level:2},{value:"Usage",id:"usage",level:2},{value:"Polyfill the global variable",id:"polyfill-the-global-variable",level:2},{value:"Adapters",id:"adapters",level:2},{value:"Using the internal PouchDB Database",id:"using-the-internal-pouchdb-database",level:2}];function c(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"rxstorage-pouchdb",children:"RxStorage PouchDB"}),"\n",(0,t.jsxs)(n.p,{children:["The PouchDB RxStorage is based on the ",(0,t.jsx)(n.a,{href:"https://github.com/pouchdb/pouchdb",children:"PouchDB"})," database. It is the most battle proven RxStorage and has a big ecosystem of adapters. PouchDB does a lot of overhead to enable CouchDB replication which makes the PouchDB RxStorage one of the slowest."]}),"\n",(0,t.jsx)(n.admonition,{type:"warning",children:(0,t.jsxs)(n.p,{children:["The PouchDB RxStorage ",(0,t.jsx)(n.a,{href:"https://rxdb.info/questions-answers.html#why-is-the-pouchdb-rxstorage-deprecated",children:"is removed from RxDB"})," and can no longer be used in new projects. You should switch to a different ",(0,t.jsx)(n.a,{href:"/rx-storage.html",children:"RxStorage"}),"."]})}),"\n",(0,t.jsx)(n.h2,{id:"pros",children:"Pros"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Most battle proven RxStorage"}),"\n",(0,t.jsx)(n.li,{children:"Supports replication with a CouchDB endpoint"}),"\n",(0,t.jsxs)(n.li,{children:["Support storing ",(0,t.jsx)(n.a,{href:"/rx-attachment.html",children:"attachments"})]}),"\n",(0,t.jsx)(n.li,{children:"Big ecosystem of adapters"}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"cons",children:"Cons"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Big bundle size"}),"\n",(0,t.jsx)(n.li,{children:"Slow performance because of revision handling overhead"}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"import { createRxDatabase } from 'rxdb';\nimport { getRxStoragePouch, addPouchPlugin } from 'rxdb/plugins/pouchdb';\n\naddPouchPlugin(require('pouchdb-adapter-idb'));\n\nconst db = await createRxDatabase({\n name: 'exampledb',\n storage: getRxStoragePouch(\n 'idb',\n {\n /**\n * other pouchdb specific options\n * @link https://pouchdb.com/api.html#create_database\n */\n }\n )\n});\n"})}),"\n",(0,t.jsxs)(n.h2,{id:"polyfill-the-global-variable",children:["Polyfill the ",(0,t.jsx)(n.code,{children:"global"})," variable"]}),"\n",(0,t.jsxs)(n.p,{children:["When you use RxDB with ",(0,t.jsx)(n.strong,{children:"angular"})," or other ",(0,t.jsx)(n.strong,{children:"webpack"})," based frameworks, you might get the error:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-html",children:'Uncaught ReferenceError: global is not defined\n'})}),"\n",(0,t.jsxs)(n.p,{children:["This is because pouchdb assumes a nodejs-specific ",(0,t.jsx)(n.code,{children:"global"})," variable that is not added to browser runtimes by some bundlers.\nYou have to add them by your own, like we do ",(0,t.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/blob/master/examples/angular/src/polyfills.ts",children:"here"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"(window as any).global = window;\n(window as any).process = {\n env: { DEBUG: undefined },\n};\n"})}),"\n",(0,t.jsx)(n.h2,{id:"adapters",children:"Adapters"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"/adapters.html",children:"PouchDB has many adapters for all JavaScript runtimes"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"using-the-internal-pouchdb-database",children:"Using the internal PouchDB Database"}),"\n",(0,t.jsx)(n.p,{children:"For custom operations, you can access the internal PouchDB database.\nThis is dangerous because you might do changes that are not compatible with RxDB.\nOnly use this when there is no way to achieve your goals via the RxDB API."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"import {\n getPouchDBOfRxCollection\n} from 'rxdb/plugins/pouchdb';\n\nconst pouch = getPouchDBOfRxCollection(myRxCollection);\n"})})]})}function d(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},8453:(e,n,o)=>{o.d(n,{R:()=>r,x:()=>i});var t=o(6540);const a={},s=t.createContext(a);function r(e){const n=t.useContext(s);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),t.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/4adf80bb.b3bd83e9.js b/docs/assets/js/4adf80bb.b3bd83e9.js deleted file mode 100644 index e5b107750f2..00000000000 --- a/docs/assets/js/4adf80bb.b3bd83e9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9548],{4442:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>h,contentTitle:()=>r,default:()=>d,frontMatter:()=>s,metadata:()=>l,toc:()=>i});var t=o(4848),a=o(8453);const s={title:"PouchDB RxStorage",slug:"rx-storage-pouchdb.html"},r="RxStorage PouchDB",l={id:"rx-storage-pouchdb",title:"PouchDB RxStorage",description:"The PouchDB RxStorage is based on the PouchDB database. It is the most battle proven RxStorage and has a big ecosystem of adapters. PouchDB does a lot of overhead to enable CouchDB replication which makes the PouchDB RxStorage one of the slowest.",source:"@site/docs/rx-storage-pouchdb.md",sourceDirName:".",slug:"/rx-storage-pouchdb.html",permalink:"/rx-storage-pouchdb.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"PouchDB RxStorage",slug:"rx-storage-pouchdb.html"}},h={},i=[{value:"IMPORTANT:",id:"important",level:2},{value:"Pros",id:"pros",level:2},{value:"Cons",id:"cons",level:2},{value:"Usage",id:"usage",level:2},{value:"Polyfill the global variable",id:"polyfill-the-global-variable",level:2},{value:"Adapters",id:"adapters",level:2},{value:"Using the internal PouchDB Database",id:"using-the-internal-pouchdb-database",level:2}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",hr:"hr",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"rxstorage-pouchdb",children:"RxStorage PouchDB"}),"\n",(0,t.jsxs)(n.p,{children:["The PouchDB RxStorage is based on the ",(0,t.jsx)(n.a,{href:"https://github.com/pouchdb/pouchdb",children:"PouchDB"})," database. It is the most battle proven RxStorage and has a big ecosystem of adapters. PouchDB does a lot of overhead to enable CouchDB replication which makes the PouchDB RxStorage one of the slowest."]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"important",children:"IMPORTANT:"}),"\n",(0,t.jsxs)(n.p,{children:["The PouchDB RxStorage ",(0,t.jsx)(n.a,{href:"https://rxdb.info/questions-answers.html#why-is-the-pouchdb-rxstorage-deprecated",children:"is removed from RxDB"})," and can no longer be used in new projects. You should switch to a different ",(0,t.jsx)(n.a,{href:"/rx-storage.html",children:"RxStorage"}),"."]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"pros",children:"Pros"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Most battle proven RxStorage"}),"\n",(0,t.jsx)(n.li,{children:"Supports replication with a CouchDB endpoint"}),"\n",(0,t.jsxs)(n.li,{children:["Support storing ",(0,t.jsx)(n.a,{href:"/rx-attachment.html",children:"attachments"})]}),"\n",(0,t.jsx)(n.li,{children:"Big ecosystem of adapters"}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"cons",children:"Cons"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Big bundle size"}),"\n",(0,t.jsx)(n.li,{children:"Slow performance because of revision handling overhead"}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"import { createRxDatabase } from 'rxdb';\nimport { getRxStoragePouch, addPouchPlugin } from 'rxdb/plugins/pouchdb';\n\naddPouchPlugin(require('pouchdb-adapter-idb'));\n\nconst db = await createRxDatabase({\n name: 'exampledb',\n storage: getRxStoragePouch(\n 'idb',\n {\n /**\n * other pouchdb specific options\n * @link https://pouchdb.com/api.html#create_database\n */\n }\n )\n});\n"})}),"\n",(0,t.jsxs)(n.h2,{id:"polyfill-the-global-variable",children:["Polyfill the ",(0,t.jsx)(n.code,{children:"global"})," variable"]}),"\n",(0,t.jsxs)(n.p,{children:["When you use RxDB with ",(0,t.jsx)(n.strong,{children:"angular"})," or other ",(0,t.jsx)(n.strong,{children:"webpack"})," based frameworks, you might get the error:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-html",children:'Uncaught ReferenceError: global is not defined\n'})}),"\n",(0,t.jsxs)(n.p,{children:["This is because pouchdb assumes a nodejs-specific ",(0,t.jsx)(n.code,{children:"global"})," variable that is not added to browser runtimes by some bundlers.\nYou have to add them by your own, like we do ",(0,t.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/blob/master/examples/angular/src/polyfills.ts",children:"here"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"(window as any).global = window;\n(window as any).process = {\n env: { DEBUG: undefined },\n};\n"})}),"\n",(0,t.jsx)(n.h2,{id:"adapters",children:"Adapters"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"/adapters.html",children:"PouchDB has many adapters for all JavaScript runtimes"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"using-the-internal-pouchdb-database",children:"Using the internal PouchDB Database"}),"\n",(0,t.jsx)(n.p,{children:"For custom operations, you can access the internal PouchDB database.\nThis is dangerous because you might do changes that are not compatible with RxDB.\nOnly use this when there is no way to achieve your goals via the RxDB API."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"import {\n getPouchDBOfRxCollection\n} from 'rxdb/plugins/pouchdb';\n\nconst pouch = getPouchDBOfRxCollection(myRxCollection);\n"})})]})}function d(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},8453:(e,n,o)=>{o.d(n,{R:()=>r,x:()=>l});var t=o(6540);const a={},s=t.createContext(a);function r(e){const n=t.useContext(s);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),t.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/55a5b596.1107da80.js b/docs/assets/js/55a5b596.1107da80.js deleted file mode 100644 index 707c5e3578b..00000000000 --- a/docs/assets/js/55a5b596.1107da80.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6717],{5536:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>o,contentTitle:()=>r,default:()=>c,frontMatter:()=>a,metadata:()=>l,toc:()=>h});var i=t(4848),s=t(8453);const a={title:"RxSchema",slug:"rx-schema.html"},r="RxSchema",l={id:"rx-schema",title:"RxSchema",description:"Schemas define the structure of the documents of a collection. Which field should be used as primary, which fields should be used as indexes and what should be encrypted. Every collection has its own schema. With RxDB, schemas are defined with the jsonschema-standard which you might know from other projects.",source:"@site/docs/rx-schema.md",sourceDirName:".",slug:"/rx-schema.html",permalink:"/rx-schema.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxSchema",slug:"rx-schema.html"},sidebar:"tutorialSidebar",previous:{title:"RxDatabase",permalink:"/rx-database.html"},next:{title:"RxCollection",permalink:"/rx-collection.html"}},o={},h=[{value:"Example",id:"example",level:2},{value:"Create a collection with the schema",id:"create-a-collection-with-the-schema",level:2},{value:"version",id:"version",level:2},{value:"primaryKey",id:"primarykey",level:2},{value:"composite primary key",id:"composite-primary-key",level:3},{value:"Indexes",id:"indexes",level:2},{value:"Index-example",id:"index-example",level:3},{value:"attachments",id:"attachments",level:2},{value:"default",id:"default",level:2},{value:"final",id:"final",level:2},{value:"NOTICE: Not everything within the jsonschema-spec is allowed",id:"notice-not-everything-within-the-jsonschema-spec-is-allowed",level:2}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"rxschema",children:"RxSchema"}),"\n",(0,i.jsxs)(n.p,{children:["Schemas define the structure of the documents of a collection. Which field should be used as primary, which fields should be used as indexes and what should be encrypted. Every collection has its own schema. With RxDB, schemas are defined with the ",(0,i.jsx)(n.a,{href:"http://json-schema.org/",children:"jsonschema"}),"-standard which you might know from other projects."]}),"\n",(0,i.jsx)(n.h2,{id:"example",children:"Example"}),"\n",(0,i.jsx)(n.p,{children:"In this example-schema we define a hero-collection with the following settings:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"the version-number of the schema is 0"}),"\n",(0,i.jsxs)(n.li,{children:["the name-property is the ",(0,i.jsx)(n.strong,{children:"primaryKey"}),". This means its an unique, indexed, required ",(0,i.jsx)(n.code,{children:"string"})," which can be used to definitely find a single document."]}),"\n",(0,i.jsx)(n.li,{children:"the color-field is required for every document"}),"\n",(0,i.jsx)(n.li,{children:"the healthpoints-field must be a number between 0 and 100"}),"\n",(0,i.jsx)(n.li,{children:"the secret-field stores an encrypted value"}),"\n",(0,i.jsx)(n.li,{children:"the birthyear-field is final which means it is required and cannot be changed"}),"\n",(0,i.jsx)(n.li,{children:"the skills-attribute must be an array with objects which contain the name and the damage-attribute. There is a maximum of 5 skills per hero."}),"\n",(0,i.jsx)(n.li,{children:"Allows adding attachments and store them encrypted"}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:' {\n "title": "hero schema",\n "version": 0,\n "description": "describes a simple hero",\n "primaryKey": "name",\n "type": "object",\n "properties": {\n "name": {\n "type": "string",\n "maxLength": 100 // <- the primary key must have set maxLength\n },\n "color": {\n "type": "string"\n },\n "healthpoints": {\n "type": "number",\n "minimum": 0,\n "maximum": 100\n },\n "secret": {\n "type": "string"\n },\n "birthyear": {\n "type": "number",\n "final": true,\n "minimum": 1900,\n "maximum": 2050\n },\n "skills": {\n "type": "array",\n "maxItems": 5,\n "uniqueItems": true,\n "items": {\n "type": "object",\n "properties": {\n "name": {\n "type": "string"\n },\n "damage": {\n "type": "number"\n }\n }\n }\n }\n },\n "required": [\n "name",\n "color"\n ],\n "encrypted": ["secret"],\n "attachments": {\n "encrypted": true\n }\n }\n'})}),"\n",(0,i.jsx)(n.h2,{id:"create-a-collection-with-the-schema",children:"Create a collection with the schema"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"await myDatabase.addCollections({\n heroes: {\n schema: myHeroSchema\n }\n});\nconsole.dir(myDatabase.heroes.name);\n// heroes\n"})}),"\n",(0,i.jsx)(n.h2,{id:"version",children:"version"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"version"})," field is a number, starting with ",(0,i.jsx)(n.code,{children:"0"}),".\nWhen the version is greater than 0, you have to provide the migrationStrategies to create a collection with this schema."]}),"\n",(0,i.jsx)(n.h2,{id:"primarykey",children:"primaryKey"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"primaryKey"})," field contains the fieldname of the property that will be used as primary key for the whole collection.\nThe value of the primary key of the document must be a ",(0,i.jsx)(n.code,{children:"string"}),", unique, final and is required."]}),"\n",(0,i.jsx)(n.h3,{id:"composite-primary-key",children:"composite primary key"}),"\n",(0,i.jsx)(n.p,{children:"You can define a composite primary key which gets composed from multiple properties of the document data."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const mySchema = {\n keyCompression: true, // set this to true, to enable the keyCompression\n version: 0,\n title: 'human schema with composite primary',\n primaryKey: {\n // where should the composed string be stored\n key: 'id',\n // fields that will be used to create the composed key\n fields: [\n 'firstName',\n 'lastName'\n ],\n // separator which is used to concat the fields values.\n separator: '|'\n },\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100 // <- the primary key must have set maxLength\n },\n firstName: {\n type: 'string'\n },\n lastName: {\n type: 'string'\n }\n },\n required: [\n 'id', \n 'firstName',\n 'lastName'\n ]\n};\n"})}),"\n",(0,i.jsx)(n.p,{children:"You can then find a document by using the relevant parts to create the composite primaryKey:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"\n// inserting with composite primary\nawait myRxCollection.insert({\n // id, <- do not set the id, it will be filled by RxDB\n firstName: 'foo',\n lastName: 'bar'\n});\n\n// find by composite primary\nconst id = myRxCollection.schema.getPrimaryOfDocumentData({\n firstName: 'foo',\n lastName: 'bar'\n});\nconst myRxDocument = myRxCollection.findOne(id).exec();\n\n"})}),"\n",(0,i.jsx)(n.h2,{id:"indexes",children:"Indexes"}),"\n",(0,i.jsx)(n.p,{children:"RxDB supports secondary indexes which are defined at the schema-level of the collection."}),"\n",(0,i.jsxs)(n.p,{children:["Index is only allowed on field types ",(0,i.jsx)(n.code,{children:"string"}),", ",(0,i.jsx)(n.code,{children:"integer"})," and ",(0,i.jsx)(n.code,{children:"number"}),". Some RxStorages allow to use ",(0,i.jsx)(n.code,{children:"boolean"})," fields as index."]}),"\n",(0,i.jsxs)(n.p,{children:["Depending on the field type, you must have set some meta attributes like ",(0,i.jsx)(n.code,{children:"maxLength"})," or ",(0,i.jsx)(n.code,{children:"minimum"}),". This is required so that RxDB\nis able to know the maximum string representation length of a field, which is needed to craft custom indexes on several ",(0,i.jsx)(n.code,{children:"RxStorage"})," implementations."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"NOTICE:"})," RxDB will always append the ",(0,i.jsx)(n.code,{children:"primaryKey"})," to all indexes to ensure a deterministic sort order of query results. You do not have to add the ",(0,i.jsx)(n.code,{children:"primaryKey"})," to any index."]}),"\n",(0,i.jsx)(n.h3,{id:"index-example",children:"Index-example"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const schemaWithIndexes = {\n version: 0,\n title: 'human schema with indexes',\n keyCompression: true,\n primaryKey: 'id',\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100 // <- the primary key must have set maxLength\n },\n firstName: {\n type: 'string',\n maxLength: 100 // <- string-fields that are used as an index, must have set maxLength.\n },\n lastName: {\n type: 'string'\n },\n active: {\n type: 'boolean'\n },\n familyName: {\n type: 'string'\n },\n balance: {\n type: 'number',\n\n // number fields that are used in an index, must have set minimum, maximum and multipleOf\n minimum: 0,\n maximum: 100000,\n multipleOf: 0.01\n },\n creditCards: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n cvc: {\n type: 'number'\n }\n }\n } \n }\n },\n required: [\n 'id',\n 'active' // <- boolean fields that are used in an index, must be required. \n ],\n indexes: [\n 'firstName', // <- this will create a simple index for the `firstName` field\n ['active', 'firstName'], // <- this will create a compound-index for these two fields\n 'active'\n ]\n};\n"})}),"\n",(0,i.jsx)(n.h1,{id:"internalindexes",children:"internalIndexes"}),"\n",(0,i.jsxs)(n.p,{children:["When you use RxDB on the server-side, you might want to use internalIndexes to speed up internal queries. ",(0,i.jsx)(n.a,{href:"/rx-server.html#server-only-indexes",children:"Read more"})]}),"\n",(0,i.jsx)(n.h2,{id:"attachments",children:"attachments"}),"\n",(0,i.jsxs)(n.p,{children:["To use attachments in the collection, you have to add the ",(0,i.jsx)(n.code,{children:"attachments"}),"-attribute to the schema. ",(0,i.jsx)(n.a,{href:"/rx-attachment.html",children:"See RxAttachment"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"default",children:"default"}),"\n",(0,i.jsx)(n.p,{children:"Default values can only be defined for first-level fields.\nWhenever you insert a document unset fields will be filled with default-values."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const schemaWithDefaultAge = {\n version: 0,\n primaryKey: 'id',\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100 // <- the primary key must have set maxLength\n },\n firstName: {\n type: 'string'\n },\n lastName: {\n type: 'string'\n },\n age: {\n type: 'integer',\n default: 20 // <- default will be used\n }\n },\n required: ['id']\n};\n"})}),"\n",(0,i.jsx)(n.h2,{id:"final",children:"final"}),"\n",(0,i.jsxs)(n.p,{children:["By setting a field to ",(0,i.jsx)(n.code,{children:"final"}),", you make sure it cannot be modified later. Final fields are always required.\nFinal fields cannot be observed because they will not change."]}),"\n",(0,i.jsx)(n.p,{children:"Advantages:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"With final fields you can ensure that no-one accidentally modifies the data."}),"\n",(0,i.jsxs)(n.li,{children:["When you enable the ",(0,i.jsx)(n.code,{children:"eventReduce"})," algorithm, some performance-improvements are done."]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const schemaWithFinalAge = {\n version: 0,\n primaryKey: 'id',\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100 // <- the primary key must have set maxLength\n },\n firstName: {\n type: 'string'\n },\n lastName: {\n type: 'string'\n },\n age: {\n type: 'integer',\n final: true\n }\n },\n required: ['id']\n};\n"})}),"\n",(0,i.jsx)(n.h2,{id:"notice-not-everything-within-the-jsonschema-spec-is-allowed",children:"NOTICE: Not everything within the jsonschema-spec is allowed"}),"\n",(0,i.jsxs)(n.p,{children:["The schema is not only used to validate objects before they are written into the database, but also used to map getters to observe and populate single fieldnames, keycompression and other things. Therefore you can not use every schema which would be valid for the spec of ",(0,i.jsx)(n.a,{href:"http://json-schema.org/",children:"json-schema.org"}),".\nFor example, fieldnames must match the regex ",(0,i.jsx)(n.code,{children:"^[a-zA-Z][[a-zA-Z0-9_]*]?[a-zA-Z0-9]$"})," and ",(0,i.jsx)(n.code,{children:"additionalProperties"})," is always set to ",(0,i.jsx)(n.code,{children:"false"}),". But don't worry, RxDB will instantly throw an error when you pass an invalid schema into it."]})]})}function c(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>l});var i=t(6540);const s={},a=i.createContext(s);function r(e){const n=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/55a5b596.52ad6e38.js b/docs/assets/js/55a5b596.52ad6e38.js new file mode 100644 index 00000000000..78ee1b0c187 --- /dev/null +++ b/docs/assets/js/55a5b596.52ad6e38.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6717],{5536:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>o,contentTitle:()=>r,default:()=>c,frontMatter:()=>a,metadata:()=>l,toc:()=>h});var i=t(4848),s=t(8453);const a={title:"RxSchema",slug:"rx-schema.html"},r="RxSchema",l={id:"rx-schema",title:"RxSchema",description:"Schemas define the structure of the documents of a collection. Which field should be used as primary, which fields should be used as indexes and what should be encrypted. Every collection has its own schema. With RxDB, schemas are defined with the jsonschema-standard which you might know from other projects.",source:"@site/docs/rx-schema.md",sourceDirName:".",slug:"/rx-schema.html",permalink:"/rx-schema.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxSchema",slug:"rx-schema.html"},sidebar:"tutorialSidebar",previous:{title:"RxDatabase",permalink:"/rx-database.html"},next:{title:"RxCollection",permalink:"/rx-collection.html"}},o={},h=[{value:"Example",id:"example",level:2},{value:"Create a collection with the schema",id:"create-a-collection-with-the-schema",level:2},{value:"version",id:"version",level:2},{value:"primaryKey",id:"primarykey",level:2},{value:"composite primary key",id:"composite-primary-key",level:3},{value:"Indexes",id:"indexes",level:2},{value:"Index-example",id:"index-example",level:3},{value:"attachments",id:"attachments",level:2},{value:"default",id:"default",level:2},{value:"final",id:"final",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"rxschema",children:"RxSchema"}),"\n",(0,i.jsxs)(n.p,{children:["Schemas define the structure of the documents of a collection. Which field should be used as primary, which fields should be used as indexes and what should be encrypted. Every collection has its own schema. With RxDB, schemas are defined with the ",(0,i.jsx)(n.a,{href:"http://json-schema.org/",children:"jsonschema"}),"-standard which you might know from other projects."]}),"\n",(0,i.jsx)(n.h2,{id:"example",children:"Example"}),"\n",(0,i.jsx)(n.p,{children:"In this example-schema we define a hero-collection with the following settings:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"the version-number of the schema is 0"}),"\n",(0,i.jsxs)(n.li,{children:["the name-property is the ",(0,i.jsx)(n.strong,{children:"primaryKey"}),". This means its an unique, indexed, required ",(0,i.jsx)(n.code,{children:"string"})," which can be used to definitely find a single document."]}),"\n",(0,i.jsx)(n.li,{children:"the color-field is required for every document"}),"\n",(0,i.jsx)(n.li,{children:"the healthpoints-field must be a number between 0 and 100"}),"\n",(0,i.jsx)(n.li,{children:"the secret-field stores an encrypted value"}),"\n",(0,i.jsx)(n.li,{children:"the birthyear-field is final which means it is required and cannot be changed"}),"\n",(0,i.jsx)(n.li,{children:"the skills-attribute must be an array with objects which contain the name and the damage-attribute. There is a maximum of 5 skills per hero."}),"\n",(0,i.jsx)(n.li,{children:"Allows adding attachments and store them encrypted"}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:' {\n "title": "hero schema",\n "version": 0,\n "description": "describes a simple hero",\n "primaryKey": "name",\n "type": "object",\n "properties": {\n "name": {\n "type": "string",\n "maxLength": 100 // <- the primary key must have set maxLength\n },\n "color": {\n "type": "string"\n },\n "healthpoints": {\n "type": "number",\n "minimum": 0,\n "maximum": 100\n },\n "secret": {\n "type": "string"\n },\n "birthyear": {\n "type": "number",\n "final": true,\n "minimum": 1900,\n "maximum": 2050\n },\n "skills": {\n "type": "array",\n "maxItems": 5,\n "uniqueItems": true,\n "items": {\n "type": "object",\n "properties": {\n "name": {\n "type": "string"\n },\n "damage": {\n "type": "number"\n }\n }\n }\n }\n },\n "required": [\n "name",\n "color"\n ],\n "encrypted": ["secret"],\n "attachments": {\n "encrypted": true\n }\n }\n'})}),"\n",(0,i.jsx)(n.h2,{id:"create-a-collection-with-the-schema",children:"Create a collection with the schema"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"await myDatabase.addCollections({\n heroes: {\n schema: myHeroSchema\n }\n});\nconsole.dir(myDatabase.heroes.name);\n// heroes\n"})}),"\n",(0,i.jsx)(n.h2,{id:"version",children:"version"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"version"})," field is a number, starting with ",(0,i.jsx)(n.code,{children:"0"}),".\nWhen the version is greater than 0, you have to provide the migrationStrategies to create a collection with this schema."]}),"\n",(0,i.jsx)(n.h2,{id:"primarykey",children:"primaryKey"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"primaryKey"})," field contains the fieldname of the property that will be used as primary key for the whole collection.\nThe value of the primary key of the document must be a ",(0,i.jsx)(n.code,{children:"string"}),", unique, final and is required."]}),"\n",(0,i.jsx)(n.h3,{id:"composite-primary-key",children:"composite primary key"}),"\n",(0,i.jsx)(n.p,{children:"You can define a composite primary key which gets composed from multiple properties of the document data."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const mySchema = {\n keyCompression: true, // set this to true, to enable the keyCompression\n version: 0,\n title: 'human schema with composite primary',\n primaryKey: {\n // where should the composed string be stored\n key: 'id',\n // fields that will be used to create the composed key\n fields: [\n 'firstName',\n 'lastName'\n ],\n // separator which is used to concat the fields values.\n separator: '|'\n },\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100 // <- the primary key must have set maxLength\n },\n firstName: {\n type: 'string'\n },\n lastName: {\n type: 'string'\n }\n },\n required: [\n 'id', \n 'firstName',\n 'lastName'\n ]\n};\n"})}),"\n",(0,i.jsx)(n.p,{children:"You can then find a document by using the relevant parts to create the composite primaryKey:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"\n// inserting with composite primary\nawait myRxCollection.insert({\n // id, <- do not set the id, it will be filled by RxDB\n firstName: 'foo',\n lastName: 'bar'\n});\n\n// find by composite primary\nconst id = myRxCollection.schema.getPrimaryOfDocumentData({\n firstName: 'foo',\n lastName: 'bar'\n});\nconst myRxDocument = myRxCollection.findOne(id).exec();\n\n"})}),"\n",(0,i.jsx)(n.h2,{id:"indexes",children:"Indexes"}),"\n",(0,i.jsx)(n.p,{children:"RxDB supports secondary indexes which are defined at the schema-level of the collection."}),"\n",(0,i.jsxs)(n.p,{children:["Index is only allowed on field types ",(0,i.jsx)(n.code,{children:"string"}),", ",(0,i.jsx)(n.code,{children:"integer"})," and ",(0,i.jsx)(n.code,{children:"number"}),". Some RxStorages allow to use ",(0,i.jsx)(n.code,{children:"boolean"})," fields as index."]}),"\n",(0,i.jsxs)(n.p,{children:["Depending on the field type, you must have set some meta attributes like ",(0,i.jsx)(n.code,{children:"maxLength"})," or ",(0,i.jsx)(n.code,{children:"minimum"}),". This is required so that RxDB\nis able to know the maximum string representation length of a field, which is needed to craft custom indexes on several ",(0,i.jsx)(n.code,{children:"RxStorage"})," implementations."]}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsxs)(n.p,{children:["RxDB will always append the ",(0,i.jsx)(n.code,{children:"primaryKey"})," to all indexes to ensure a deterministic sort order of query results. You do not have to add the ",(0,i.jsx)(n.code,{children:"primaryKey"})," to any index."]})}),"\n",(0,i.jsx)(n.h3,{id:"index-example",children:"Index-example"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const schemaWithIndexes = {\n version: 0,\n title: 'human schema with indexes',\n keyCompression: true,\n primaryKey: 'id',\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100 // <- the primary key must have set maxLength\n },\n firstName: {\n type: 'string',\n maxLength: 100 // <- string-fields that are used as an index, must have set maxLength.\n },\n lastName: {\n type: 'string'\n },\n active: {\n type: 'boolean'\n },\n familyName: {\n type: 'string'\n },\n balance: {\n type: 'number',\n\n // number fields that are used in an index, must have set minimum, maximum and multipleOf\n minimum: 0,\n maximum: 100000,\n multipleOf: 0.01\n },\n creditCards: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n cvc: {\n type: 'number'\n }\n }\n } \n }\n },\n required: [\n 'id',\n 'active' // <- boolean fields that are used in an index, must be required. \n ],\n indexes: [\n 'firstName', // <- this will create a simple index for the `firstName` field\n ['active', 'firstName'], // <- this will create a compound-index for these two fields\n 'active'\n ]\n};\n"})}),"\n",(0,i.jsx)(n.h1,{id:"internalindexes",children:"internalIndexes"}),"\n",(0,i.jsxs)(n.p,{children:["When you use RxDB on the server-side, you might want to use internalIndexes to speed up internal queries. ",(0,i.jsx)(n.a,{href:"/rx-server.html#server-only-indexes",children:"Read more"})]}),"\n",(0,i.jsx)(n.h2,{id:"attachments",children:"attachments"}),"\n",(0,i.jsxs)(n.p,{children:["To use attachments in the collection, you have to add the ",(0,i.jsx)(n.code,{children:"attachments"}),"-attribute to the schema. ",(0,i.jsx)(n.a,{href:"/rx-attachment.html",children:"See RxAttachment"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"default",children:"default"}),"\n",(0,i.jsx)(n.p,{children:"Default values can only be defined for first-level fields.\nWhenever you insert a document unset fields will be filled with default-values."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const schemaWithDefaultAge = {\n version: 0,\n primaryKey: 'id',\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100 // <- the primary key must have set maxLength\n },\n firstName: {\n type: 'string'\n },\n lastName: {\n type: 'string'\n },\n age: {\n type: 'integer',\n default: 20 // <- default will be used\n }\n },\n required: ['id']\n};\n"})}),"\n",(0,i.jsx)(n.h2,{id:"final",children:"final"}),"\n",(0,i.jsxs)(n.p,{children:["By setting a field to ",(0,i.jsx)(n.code,{children:"final"}),", you make sure it cannot be modified later. Final fields are always required.\nFinal fields cannot be observed because they will not change."]}),"\n",(0,i.jsx)(n.p,{children:"Advantages:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"With final fields you can ensure that no-one accidentally modifies the data."}),"\n",(0,i.jsxs)(n.li,{children:["When you enable the ",(0,i.jsx)(n.code,{children:"eventReduce"})," algorithm, some performance-improvements are done."]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const schemaWithFinalAge = {\n version: 0,\n primaryKey: 'id',\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100 // <- the primary key must have set maxLength\n },\n firstName: {\n type: 'string'\n },\n lastName: {\n type: 'string'\n },\n age: {\n type: 'integer',\n final: true\n }\n },\n required: ['id']\n};\n"})}),"\n",(0,i.jsx)(n.admonition,{title:"Not everything within the jsonschema-spec is allowed",type:"note",children:(0,i.jsxs)(n.p,{children:["The schema is not only used to validate objects before they are written into the database, but also used to map getters to observe and populate single fieldnames, keycompression and other things. Therefore you can not use every schema which would be valid for the spec of ",(0,i.jsx)(n.a,{href:"http://json-schema.org/",children:"json-schema.org"}),".\nFor example, fieldnames must match the regex ",(0,i.jsx)(n.code,{children:"^[a-zA-Z][[a-zA-Z0-9_]*]?[a-zA-Z0-9]$"})," and ",(0,i.jsx)(n.code,{children:"additionalProperties"})," is always set to ",(0,i.jsx)(n.code,{children:"false"}),". But don't worry, RxDB will instantly throw an error when you pass an invalid schema into it."]})})]})}function c(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>l});var i=t(6540);const s={},a=i.createContext(s);function r(e){const n=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/6187b59a.1e0edb57.js b/docs/assets/js/6187b59a.1e0edb57.js new file mode 100644 index 00000000000..6b528fe374d --- /dev/null +++ b/docs/assets/js/6187b59a.1e0edb57.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[5866],{7409:(e,r,n)=>{n.r(r),n.d(r,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>i,toc:()=>l});var o=n(4848),t=n(8453);const a={title:"Worker RxStorage \ud83d\udc51",slug:"rx-storage-worker.html"},s="Worker RxStorage",i={id:"rx-storage-worker",title:"Worker RxStorage \ud83d\udc51",description:"With the worker plugin, you can put the RxStorage of your database inside of a WebWorker (in browsers) or a Worker Thread (in node.js). By doing so, you can take CPU load from the main process and move it into the worker's process which can improve the perceived performance of your application. Notice that for browsers, it is recommend to use the SharedWorker instead to get a better performance.",source:"@site/docs/rx-storage-worker.md",sourceDirName:".",slug:"/rx-storage-worker.html",permalink:"/rx-storage-worker.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Worker RxStorage \ud83d\udc51",slug:"rx-storage-worker.html"},sidebar:"tutorialSidebar",previous:{title:"Remote RxStorage",permalink:"/rx-storage-remote.html"},next:{title:"SharedWorker RxStorage \ud83d\udc51",permalink:"/rx-storage-shared-worker.html"}},c={},l=[{value:"On the worker process",id:"on-the-worker-process",level:2},{value:"On the main process",id:"on-the-main-process",level:2},{value:"Pre-build workers",id:"pre-build-workers",level:2},{value:"Building a custom worker",id:"building-a-custom-worker",level:2},{value:"One worker per database",id:"one-worker-per-database",level:2},{value:"Passing in a Worker instance",id:"passing-in-a-worker-instance",level:2}];function d(e){const r={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,t.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(r.h1,{id:"worker-rxstorage",children:"Worker RxStorage"}),"\n",(0,o.jsxs)(r.p,{children:["With the worker plugin, you can put the ",(0,o.jsx)(r.code,{children:"RxStorage"})," of your database inside of a WebWorker (in browsers) or a Worker Thread (in node.js). By doing so, you can take CPU load from the main process and move it into the worker's process which can improve the perceived performance of your application. Notice that for browsers, it is recommend to use the ",(0,o.jsx)(r.a,{href:"/rx-storage-shared-worker.html",children:"SharedWorker"})," instead to get a better performance."]}),"\n",(0,o.jsx)(r.admonition,{title:"Premium",type:"note",children:(0,o.jsxs)(r.p,{children:["This plugin is part of ",(0,o.jsx)(r.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]})}),"\n",(0,o.jsx)(r.h2,{id:"on-the-worker-process",children:"On the worker process"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"// worker.ts\n\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\nimport { getRxStorageLoki } from 'rxdb/plugins/storage-lokijs';\n\nexposeWorkerRxStorage({\n /**\n * You can wrap any implementation of the RxStorage interface\n * into a worker.\n * Here we use the LokiJS RxStorage.\n */\n storage: getRxStorageLoki()\n});\n"})}),"\n",(0,o.jsx)(r.h2,{id:"on-the-main-process",children:"On the main process"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageWorker } from 'rxdb-premium/plugins/storage-worker';\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageWorker(\n {\n /**\n * Contains any value that can be used as parameter\n * to the Worker constructor of thread.js\n * Most likely you want to put the path to the worker.js file in here.\n * \n * @link https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker\n */\n workerInput: 'path/to/worker.js',\n /**\n * (Optional) options\n * for the worker.\n */\n workerOptions: {\n type: 'module',\n credentials: 'omit'\n }\n }\n )\n});\n"})}),"\n",(0,o.jsx)(r.h2,{id:"pre-build-workers",children:"Pre-build workers"}),"\n",(0,o.jsxs)(r.p,{children:["The ",(0,o.jsx)(r.code,{children:"worker.js"})," must be a self containing JavaScript file that contains all dependencies in a bundle.\nTo make it easier for you, RxDB ships with pre-bundles worker files that are ready to use.\nYou can find them in the folder ",(0,o.jsx)(r.code,{children:"node_modules/rxdb-premium/dist/workers"})," after you have installed the ",(0,o.jsx)(r.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51 Plugin"}),". From there you can copy them to a location where it can be served from the webserver and then use their path to create the ",(0,o.jsx)(r.code,{children:"RxDatabase"}),"."]}),"\n",(0,o.jsxs)(r.p,{children:["Any valid ",(0,o.jsx)(r.code,{children:"worker.js"})," JavaScript file can be used both, for normal Workers and SharedWorkers."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageWorker } from 'rxdb-premium/plugins/storage-worker';\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageWorker(\n {\n /**\n * Path to where the copied file from node_modules/rxdb/dist/workers\n * is reachable from the webserver.\n */\n workerInput: '/lokijs-incremental-indexeddb.worker.js'\n }\n )\n});\n"})}),"\n",(0,o.jsx)(r.h2,{id:"building-a-custom-worker",children:"Building a custom worker"}),"\n",(0,o.jsxs)(r.p,{children:["The easiest way to bundle a custom ",(0,o.jsx)(r.code,{children:"worker.js"})," file is by using webpack. Here is the webpack-config that is also used for the prebuild workers:"]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"// webpack.config.js\nconst path = require('path');\nconst TerserPlugin = require('terser-webpack-plugin');\nconst projectRootPath = path.resolve(\n __dirname,\n '../../' // path from webpack-config to the root folder of the repo\n);\nconst babelConfig = require(path.join(projectRootPath, 'babel.config'));\nconst baseDir = './dist/workers/'; // output path\nmodule.exports = {\n target: 'webworker',\n entry: {\n 'my-custom-worker': baseDir + 'my-custom-worker.js',\n },\n output: {\n filename: '[name].js',\n clean: true,\n path: path.resolve(\n projectRootPath,\n 'dist/workers'\n ),\n },\n mode: 'production',\n module: {\n rules: [\n {\n test: /\\.tsx?$/,\n exclude: /(node_modules)/,\n use: {\n loader: 'babel-loader',\n options: babelConfig\n }\n }\n ],\n },\n resolve: {\n extensions: ['.tsx', '.ts', '.js', '.mjs', '.mts']\n },\n optimization: {\n moduleIds: 'deterministic',\n minimize: true,\n minimizer: [new TerserPlugin({\n terserOptions: {\n format: {\n comments: false,\n },\n },\n extractComments: false,\n })],\n }\n};\n"})}),"\n",(0,o.jsx)(r.h2,{id:"one-worker-per-database",children:"One worker per database"}),"\n",(0,o.jsxs)(r.p,{children:["Each call to ",(0,o.jsx)(r.code,{children:"getRxStorageWorker()"})," will create a different worker instance so that when you have more then one ",(0,o.jsx)(r.code,{children:"RxDatabase"}),", each database will have its own JavaScript worker process."]}),"\n",(0,o.jsxs)(r.p,{children:["To reuse the worker instance in more than one ",(0,o.jsx)(r.code,{children:"RxDatabase"}),", you can store the output of ",(0,o.jsx)(r.code,{children:"getRxStorageWorker()"})," into a variable an use that one. Reusing the worker can decrease the initial page load, but you might get slower database operations."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"// Call getRxStorageWorker() exactly once\nconst workerStorage = getRxStorageWorker({\n workerInput: 'path/to/worker.js'\n});\n\n// use the same storage for both databases.\nconst databaseOne = await createRxDatabase({\n name: 'database-one',\n storage: workerStorage\n});\nconst databaseTwo = await createRxDatabase({\n name: 'database-two',\n storage: workerStorage\n});\n\n"})}),"\n",(0,o.jsx)(r.h2,{id:"passing-in-a-worker-instance",children:"Passing in a Worker instance"}),"\n",(0,o.jsxs)(r.p,{children:["Instead of setting an url as ",(0,o.jsx)(r.code,{children:"workerInput"}),", you can also specify a function that returns a new ",(0,o.jsx)(r.code,{children:"Worker"})," instance when called."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"getRxStorageWorker({\n workerInput: () => new Worker('path/to/worker.js')\n})\n"})}),"\n",(0,o.jsxs)(r.p,{children:["This can be helpful for environments where the worker is build dynamically by the bundler. For example in angular you would create a ",(0,o.jsx)(r.code,{children:"my-custom.worker.ts"})," file that contains a custom build worker and then import it."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"const storage = getRxStorageWorker({\n workerInput: () => new Worker(new URL('./my-custom.worker', import.meta.url)),\n});\n"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"//> my-custom.worker.ts\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\nimport { getRxStorageLoki } from 'rxdb/plugins/storage-lokijs';\n\nexposeWorkerRxStorage({\n storage: getRxStorageLoki()\n});\n"})})]})}function h(e={}){const{wrapper:r}={...(0,t.R)(),...e.components};return r?(0,o.jsx)(r,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},8453:(e,r,n)=>{n.d(r,{R:()=>s,x:()=>i});var o=n(6540);const t={},a=o.createContext(t);function s(e){const r=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function i(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),o.createElement(a.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/6187b59a.840fc51d.js b/docs/assets/js/6187b59a.840fc51d.js deleted file mode 100644 index ae7baee2e73..00000000000 --- a/docs/assets/js/6187b59a.840fc51d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[5866],{7409:(e,r,n)=>{n.r(r),n.d(r,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>i,toc:()=>l});var o=n(4848),t=n(8453);const a={title:"Worker RxStorage \ud83d\udc51",slug:"rx-storage-worker.html"},s="Worker RxStorage",i={id:"rx-storage-worker",title:"Worker RxStorage \ud83d\udc51",description:"With the worker plugin, you can put the RxStorage of your database inside of a WebWorker (in browsers) or a Worker Thread (in node.js). By doing so, you can take CPU load from the main process and move it into the worker's process which can improve the perceived performance of your application. Notice that for browsers, it is recommend to use the SharedWorker instead to get a better performance.",source:"@site/docs/rx-storage-worker.md",sourceDirName:".",slug:"/rx-storage-worker.html",permalink:"/rx-storage-worker.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Worker RxStorage \ud83d\udc51",slug:"rx-storage-worker.html"},sidebar:"tutorialSidebar",previous:{title:"Remote RxStorage",permalink:"/rx-storage-remote.html"},next:{title:"SharedWorker RxStorage \ud83d\udc51",permalink:"/rx-storage-shared-worker.html"}},c={},l=[{value:"On the worker process",id:"on-the-worker-process",level:2},{value:"On the main process",id:"on-the-main-process",level:2},{value:"Pre-build workers",id:"pre-build-workers",level:2},{value:"Building a custom worker",id:"building-a-custom-worker",level:2},{value:"One worker per database",id:"one-worker-per-database",level:2},{value:"Passing in a Worker instance",id:"passing-in-a-worker-instance",level:2}];function d(e){const r={a:"a",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,t.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(r.h1,{id:"worker-rxstorage",children:"Worker RxStorage"}),"\n",(0,o.jsxs)(r.p,{children:["With the worker plugin, you can put the ",(0,o.jsx)(r.code,{children:"RxStorage"})," of your database inside of a WebWorker (in browsers) or a Worker Thread (in node.js). By doing so, you can take CPU load from the main process and move it into the worker's process which can improve the perceived performance of your application. Notice that for browsers, it is recommend to use the ",(0,o.jsx)(r.a,{href:"/rx-storage-shared-worker.html",children:"SharedWorker"})," instead to get a better performance."]}),"\n",(0,o.jsxs)(r.p,{children:[(0,o.jsx)(r.strong,{children:"NOTICE:"})," This plugin is part of ",(0,o.jsx)(r.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]}),"\n",(0,o.jsx)(r.h2,{id:"on-the-worker-process",children:"On the worker process"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"// worker.ts\n\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\nimport { getRxStorageLoki } from 'rxdb/plugins/storage-lokijs';\n\nexposeWorkerRxStorage({\n /**\n * You can wrap any implementation of the RxStorage interface\n * into a worker.\n * Here we use the LokiJS RxStorage.\n */\n storage: getRxStorageLoki()\n});\n"})}),"\n",(0,o.jsx)(r.h2,{id:"on-the-main-process",children:"On the main process"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageWorker } from 'rxdb-premium/plugins/storage-worker';\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageWorker(\n {\n /**\n * Contains any value that can be used as parameter\n * to the Worker constructor of thread.js\n * Most likely you want to put the path to the worker.js file in here.\n * \n * @link https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker\n */\n workerInput: 'path/to/worker.js',\n /**\n * (Optional) options\n * for the worker.\n */\n workerOptions: {\n type: 'module',\n credentials: 'omit'\n }\n }\n )\n});\n"})}),"\n",(0,o.jsx)(r.h2,{id:"pre-build-workers",children:"Pre-build workers"}),"\n",(0,o.jsxs)(r.p,{children:["The ",(0,o.jsx)(r.code,{children:"worker.js"})," must be a self containing JavaScript file that contains all dependencies in a bundle.\nTo make it easier for you, RxDB ships with pre-bundles worker files that are ready to use.\nYou can find them in the folder ",(0,o.jsx)(r.code,{children:"node_modules/rxdb-premium/dist/workers"})," after you have installed the ",(0,o.jsx)(r.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51 Plugin"}),". From there you can copy them to a location where it can be served from the webserver and then use their path to create the ",(0,o.jsx)(r.code,{children:"RxDatabase"}),"."]}),"\n",(0,o.jsxs)(r.p,{children:["Any valid ",(0,o.jsx)(r.code,{children:"worker.js"})," JavaScript file can be used both, for normal Workers and SharedWorkers."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageWorker } from 'rxdb-premium/plugins/storage-worker';\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageWorker(\n {\n /**\n * Path to where the copied file from node_modules/rxdb/dist/workers\n * is reachable from the webserver.\n */\n workerInput: '/lokijs-incremental-indexeddb.worker.js'\n }\n )\n});\n"})}),"\n",(0,o.jsx)(r.h2,{id:"building-a-custom-worker",children:"Building a custom worker"}),"\n",(0,o.jsxs)(r.p,{children:["The easiest way to bundle a custom ",(0,o.jsx)(r.code,{children:"worker.js"})," file is by using webpack. Here is the webpack-config that is also used for the prebuild workers:"]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"// webpack.config.js\nconst path = require('path');\nconst TerserPlugin = require('terser-webpack-plugin');\nconst projectRootPath = path.resolve(\n __dirname,\n '../../' // path from webpack-config to the root folder of the repo\n);\nconst babelConfig = require(path.join(projectRootPath, 'babel.config'));\nconst baseDir = './dist/workers/'; // output path\nmodule.exports = {\n target: 'webworker',\n entry: {\n 'my-custom-worker': baseDir + 'my-custom-worker.js',\n },\n output: {\n filename: '[name].js',\n clean: true,\n path: path.resolve(\n projectRootPath,\n 'dist/workers'\n ),\n },\n mode: 'production',\n module: {\n rules: [\n {\n test: /\\.tsx?$/,\n exclude: /(node_modules)/,\n use: {\n loader: 'babel-loader',\n options: babelConfig\n }\n }\n ],\n },\n resolve: {\n extensions: ['.tsx', '.ts', '.js', '.mjs', '.mts']\n },\n optimization: {\n moduleIds: 'deterministic',\n minimize: true,\n minimizer: [new TerserPlugin({\n terserOptions: {\n format: {\n comments: false,\n },\n },\n extractComments: false,\n })],\n }\n};\n"})}),"\n",(0,o.jsx)(r.h2,{id:"one-worker-per-database",children:"One worker per database"}),"\n",(0,o.jsxs)(r.p,{children:["Each call to ",(0,o.jsx)(r.code,{children:"getRxStorageWorker()"})," will create a different worker instance so that when you have more then one ",(0,o.jsx)(r.code,{children:"RxDatabase"}),", each database will have its own JavaScript worker process."]}),"\n",(0,o.jsxs)(r.p,{children:["To reuse the worker instance in more than one ",(0,o.jsx)(r.code,{children:"RxDatabase"}),", you can store the output of ",(0,o.jsx)(r.code,{children:"getRxStorageWorker()"})," into a variable an use that one. Reusing the worker can decrease the initial page load, but you might get slower database operations."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"// Call getRxStorageWorker() exactly once\nconst workerStorage = getRxStorageWorker({\n workerInput: 'path/to/worker.js'\n});\n\n// use the same storage for both databases.\nconst databaseOne = await createRxDatabase({\n name: 'database-one',\n storage: workerStorage\n});\nconst databaseTwo = await createRxDatabase({\n name: 'database-two',\n storage: workerStorage\n});\n\n"})}),"\n",(0,o.jsx)(r.h2,{id:"passing-in-a-worker-instance",children:"Passing in a Worker instance"}),"\n",(0,o.jsxs)(r.p,{children:["Instead of setting an url as ",(0,o.jsx)(r.code,{children:"workerInput"}),", you can also specify a function that returns a new ",(0,o.jsx)(r.code,{children:"Worker"})," instance when called."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"getRxStorageWorker({\n workerInput: () => new Worker('path/to/worker.js')\n})\n"})}),"\n",(0,o.jsxs)(r.p,{children:["This can be helpful for environments where the worker is build dynamically by the bundler. For example in angular you would create a ",(0,o.jsx)(r.code,{children:"my-custom.worker.ts"})," file that contains a custom build worker and then import it."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"const storage = getRxStorageWorker({\n workerInput: () => new Worker(new URL('./my-custom.worker', import.meta.url)),\n});\n"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-ts",children:"//> my-custom.worker.ts\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\nimport { getRxStorageLoki } from 'rxdb/plugins/storage-lokijs';\n\nexposeWorkerRxStorage({\n storage: getRxStorageLoki()\n});\n"})})]})}function h(e={}){const{wrapper:r}={...(0,t.R)(),...e.components};return r?(0,o.jsx)(r,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},8453:(e,r,n)=>{n.d(r,{R:()=>s,x:()=>i});var o=n(6540);const t={},a=o.createContext(t);function s(e){const r=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function i(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),o.createElement(a.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/6ae3580c.3f759a77.js b/docs/assets/js/6ae3580c.3f759a77.js new file mode 100644 index 00000000000..6123b8a094e --- /dev/null +++ b/docs/assets/js/6ae3580c.3f759a77.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[5320],{5376:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>r,contentTitle:()=>l,default:()=>d,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var i=n(4848),a=n(8453);const o={title:"\u2699\ufe0f Replication Protocol",slug:"replication.html"},l="RxDB Database Replication Protocol",s={id:"replication",title:"\u2699\ufe0f Replication Protocol",description:"The RxDB replication protocol provides the ability to replicate the database state in realtime between the clients and the server.",source:"@site/docs/replication.md",sourceDirName:".",slug:"/replication.html",permalink:"/replication.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"\u2699\ufe0f Replication Protocol",slug:"replication.html"},sidebar:"tutorialSidebar",previous:{title:"Electron Plugin",permalink:"/electron.html"},next:{title:"HTTP Replication",permalink:"/replication-http.html"}},r={},c=[{value:"Replication protocol on the document level",id:"replication-protocol-on-the-document-level",level:2},{value:"Replication protocol on the transfer level",id:"replication-protocol-on-the-transfer-level",level:2},{value:"Checkpoint iteration",id:"checkpoint-iteration",level:3},{value:"Event observation",id:"event-observation",level:3},{value:"Data layout on the server",id:"data-layout-on-the-server",level:2},{value:"Conflict handling",id:"conflict-handling",level:2},{value:"replicateRxCollection()",id:"replicaterxcollection",level:2},{value:"Multi Tab support",id:"multi-tab-support",level:2},{value:"Error handling",id:"error-handling",level:2},{value:"Security",id:"security",level:2},{value:"RxReplicationState",id:"rxreplicationstate",level:2},{value:"Observable",id:"observable",level:3},{value:"awaitInitialReplication()",id:"awaitinitialreplication",level:3},{value:"awaitInSync()",id:"awaitinsync",level:3},{value:"awaitInitialReplication() and awaitInSync() should not be used to block the application",id:"awaitinitialreplication-and-awaitinsync-should-not-be-used-to-block-the-application",level:4},{value:"reSync()",id:"resync",level:3},{value:"cancel()",id:"cancel",level:3},{value:"isStopped()",id:"isstopped",level:3},{value:"Setting a custom initialCheckpoint (beta)",id:"setting-a-custom-initialcheckpoint-beta",level:3},{value:"Attachment replication (beta)",id:"attachment-replication-beta",level:3}];function h(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"rxdb-database-replication-protocol",children:"RxDB Database Replication Protocol"}),"\n",(0,i.jsxs)(t.p,{children:["The RxDB replication protocol provides the ability to replicate the database state in ",(0,i.jsx)(t.strong,{children:"realtime"})," between the clients and the server."]}),"\n",(0,i.jsxs)(t.p,{children:["The backend server does not have to be a RxDB instance; you can build a replication with ",(0,i.jsx)(t.strong,{children:"any infrastructure"}),".\nFor example you can replicate with a custom GraphQL endpoint or a ",(0,i.jsx)(t.a,{href:"/replication-http.html",children:"http server"})," on top of a PostgreSQL database."]}),"\n",(0,i.jsxs)(t.p,{children:["The replication is made to support the ",(0,i.jsx)(t.a,{href:"http://offlinefirst.org/",children:"Offline-First"})," paradigm, so that when the client goes offline, the RxDB database can still read and write locally and will continue the replication when the client goes online again."]}),"\n",(0,i.jsx)(t.h2,{id:"replication-protocol-on-the-document-level",children:"Replication protocol on the document level"}),"\n",(0,i.jsx)(t.p,{children:"On the RxDocument level, the replication works like git, where the fork/client contains all new writes and must be merged with the master/server before it can push its new state to the master/server."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"A---B-----------D master/server state\n \\ /\n B---C---D fork/client state\n"})}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["The client pulls the latest state ",(0,i.jsx)(t.code,{children:"B"})," from the master."]}),"\n",(0,i.jsxs)(t.li,{children:["The client does some changes ",(0,i.jsx)(t.code,{children:"C+D"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:["The client pushes these changes to the master by sending the latest known master state ",(0,i.jsx)(t.code,{children:"B"})," and the new client state ",(0,i.jsx)(t.code,{children:"D"})," of the document."]}),"\n",(0,i.jsxs)(t.li,{children:["If the master state is equal to the latest master ",(0,i.jsx)(t.code,{children:"B"})," state of the client, the new client state ",(0,i.jsx)(t.code,{children:"D"})," is set as the latest master state."]}),"\n",(0,i.jsx)(t.li,{children:"If the master also had changes and so the latest master change is different then the one that the client assumes, we have a conflict that has to be resolved on the client."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"replication-protocol-on-the-transfer-level",children:"Replication protocol on the transfer level"}),"\n",(0,i.jsxs)(t.p,{children:["When document states are transferred, all handlers use batches of documents for better performance.\nThe server ",(0,i.jsx)(t.strong,{children:"must"})," implement the following methods to be compatible with the replication:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"pullHandler"})," Get the last checkpoint (or null) as input. Returns all documents that have been written ",(0,i.jsx)(t.strong,{children:"after"})," the given checkpoint. Also returns the checkpoint of the latest written returned document."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"pushHandler"})," a method that can be called by the client to send client side writes to the master. It gets an array with the ",(0,i.jsx)(t.code,{children:"assumedMasterState"})," and the ",(0,i.jsx)(t.code,{children:"newForkState"})," of each document write as input. It must return an array that contains the master document states of all conflicts. If there are no conflicts, it must return an empty array."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"pullStream"})," an observable that emits batches of all master writes and the latest checkpoint of the write batches."]}),"\n"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:" +--------+ +--------+ \n | | pullHandler() | |\n | |---------------------\x3e | | \n | | | | \n | | | | \n | Client | pushHandler() | Server | \n | |---------------------\x3e | | \n | | | |\n | | pullStream$ | | \n | | <-------------------------| | \n +--------+ +--------+\n"})}),"\n",(0,i.jsxs)(t.p,{children:["The replication runs in two ",(0,i.jsx)(t.strong,{children:"different modes"}),":"]}),"\n",(0,i.jsx)(t.h3,{id:"checkpoint-iteration",children:"Checkpoint iteration"}),"\n",(0,i.jsxs)(t.p,{children:["On first initial replication, or when the client comes online again, a checkpoint based iteration is used to catch up with the server state.\nA checkpoint is a subset of the fields of the last pulled document. When the checkpoint is send to the backend via ",(0,i.jsx)(t.code,{children:"pullHandler()"}),", the backend must be able to respond with all documents that have been written ",(0,i.jsx)(t.strong,{children:"after"})," the given checkpoint.\nFor example if your documents contain an ",(0,i.jsx)(t.code,{children:"id"})," and an ",(0,i.jsx)(t.code,{children:"updatedAt"})," field, these two can be used as checkpoint."]}),"\n",(0,i.jsxs)(t.p,{children:["When the checkpoint iteration reaches the last checkpoint, where the backend returns an empty array because there are no newer documents, the replication will automatically switch to the ",(0,i.jsx)(t.code,{children:"event observation"})," mode."]}),"\n",(0,i.jsx)(t.h3,{id:"event-observation",children:"Event observation"}),"\n",(0,i.jsxs)(t.p,{children:["While the client is connected to the backend, the events from the backend are observed via ",(0,i.jsx)(t.code,{children:"pullStream$"})," and persisted to the client."]}),"\n",(0,i.jsxs)(t.p,{children:["If your backend for any reason is not able to provide a full ",(0,i.jsx)(t.code,{children:"pullStream$"})," that contains all events and the checkpoint, you can instead only emit ",(0,i.jsx)(t.code,{children:"RESYNC"})," events that tell RxDB that anything unknown has changed on the server and it should run the pull replication via ",(0,i.jsx)(t.a,{href:"#checkpoint-iteration",children:"checkpoint iteration"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["When the client goes offline and online again, it might happen that the ",(0,i.jsx)(t.code,{children:"pullStream$"})," has missed out some events. Therefore the ",(0,i.jsx)(t.code,{children:"pullStream$"})," should also emit a ",(0,i.jsx)(t.code,{children:"RESYNC"})," event each time the client reconnects, so that the client can become in sync with the backend via the ",(0,i.jsx)(t.a,{href:"#checkpoint-iteration",children:"checkpoint iteration"})," mode."]}),"\n",(0,i.jsx)(t.h2,{id:"data-layout-on-the-server",children:"Data layout on the server"}),"\n",(0,i.jsx)(t.p,{children:"To use the replication you first have to ensure that:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.strong,{children:"documents are deterministic sortable by their last write time"})}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.em,{children:"deterministic"})," means that even if two documents have the same ",(0,i.jsx)(t.em,{children:"last write time"}),", they have a predictable sort order.\nThis is most often ensured by using the ",(0,i.jsx)(t.em,{children:"primaryKey"})," as second sort parameter as part of the checkpoint."]}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsx)(t.p,{children:(0,i.jsxs)(t.strong,{children:["documents are never deleted, instead the ",(0,i.jsx)(t.code,{children:"_deleted"})," field is set to ",(0,i.jsx)(t.code,{children:"true"}),"."]})}),"\n",(0,i.jsx)(t.p,{children:"This is needed so that the deletion state of a document exists in the database and can be replicated with other instances. If your backend uses a different field to mark deleted documents, you have to transform the data in the push/pull handlers or with the modifiers."}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"For example if your documents look like this:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:'const docData = {\n "id": "foobar",\n "name": "Alice",\n "lastName": "Wilson",\n /**\n * Contains the last write timestamp\n * so all documents writes can be sorted by that value\n * when they are fetched from the remote instance.\n */\n "updatedAt": 1564483474,\n /**\n * Instead of physically deleting documents,\n * a deleted document gets replicated.\n */\n "_deleted": false\n}\n'})}),"\n",(0,i.jsxs)(t.p,{children:["Then your data is always sortable by ",(0,i.jsx)(t.code,{children:"updatedAt"}),". This ensures that when RxDB fetches 'new' changes via ",(0,i.jsx)(t.code,{children:"pullHandler()"}),", it can send the latest ",(0,i.jsx)(t.code,{children:"updatedAt+id"})," checkpoint to the remote endpoint and then receive all newer documents."]}),"\n",(0,i.jsxs)(t.p,{children:["By default, the field is ",(0,i.jsx)(t.code,{children:"_deleted"}),". If your remote endpoint uses a different field to mark deleted documents, you can set the ",(0,i.jsx)(t.code,{children:"deletedField"})," in the replication options which will automatically map the field on all pull and push requests."]}),"\n",(0,i.jsx)(t.h2,{id:"conflict-handling",children:"Conflict handling"}),"\n",(0,i.jsx)(t.p,{children:"When multiple clients (or the server) modify the same document at the same time (or when they are offline), it can happen that a conflict arises during the replication."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"A---B1---C1---X master/server state\n \\ /\n B1---C2 fork/client state\n"})}),"\n",(0,i.jsxs)(t.p,{children:["In the case above, the client would tell the master to move the document state from ",(0,i.jsx)(t.code,{children:"B1"})," to ",(0,i.jsx)(t.code,{children:"C2"})," by calling ",(0,i.jsx)(t.code,{children:"pushHandler()"}),". But because the actual master state is ",(0,i.jsx)(t.code,{children:"C1"})," and not ",(0,i.jsx)(t.code,{children:"B1"}),", the master would reject the write by sending back the actual master state ",(0,i.jsx)(t.code,{children:"C1"}),".\n",(0,i.jsx)(t.strong,{children:"RxDB resolves all conflicts on the client"})," so it would call the conflict handler of the ",(0,i.jsx)(t.code,{children:"RxCollection"})," and create a new document state ",(0,i.jsx)(t.code,{children:"D"})," that can then be written to the master."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"A---B1---C1---X---D master/server state\n \\ / \\ /\n B1---C2---D fork/client state\n"})}),"\n",(0,i.jsxs)(t.p,{children:["The default conflict handler will always drop the fork state and use the master state. This ensures that clients that are offline for a very long time, do not accidentally overwrite other peoples changes when they go online again.\nYou can specify a custom conflict handler by setting the property ",(0,i.jsx)(t.code,{children:"conflictHandler"})," when calling ",(0,i.jsx)(t.code,{children:"addCollection()"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["Learn how to create a ",(0,i.jsx)(t.a,{href:"/transactions-conflicts-revisions.html#custom-conflict-handler",children:"custom conflict handler"}),"."]}),"\n",(0,i.jsx)(t.h2,{id:"replicaterxcollection",children:"replicateRxCollection()"}),"\n",(0,i.jsxs)(t.p,{children:["You can start the replication of a single ",(0,i.jsx)(t.code,{children:"RxCollection"})," by calling ",(0,i.jsx)(t.code,{children:"replicateRxCollection()"})," like in the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import { replicateRxCollection } from 'rxdb/plugins/replication';\nimport {\n lastOfArray\n} from 'rxdb';\nconst replicationState = await replicateRxCollection({\n collection: myRxCollection,\n /**\n * An id for the replication to identify it\n * and so that RxDB is able to resume the replication on app reload.\n * If you replicate with a remote server, it is recommended to put the\n * server url into the replicationIdentifier.\n */\n replicationIdentifier: 'my-rest-replication-to-https://example.com/api/sync',\n /**\n * By default it will do an ongoing realtime replication.\n * By settings live: false the replication will run once until the local state\n * is in sync with the remote state, then it will cancel itself.\n * (optional), default is true.\n */\n live: true,\n /**\n * Time in milliseconds after when a failed backend request\n * has to be retried.\n * This time will be skipped if a offline->online switch is detected\n * via navigator.onLine\n * (optional), default is 5 seconds.\n */\n retryTime: 5 * 1000,\n /**\n * When multiInstance is true, like when you use RxDB in multiple browser tabs,\n * the replication should always run in only one of the open browser tabs.\n * If waitForLeadership is true, it will wait until the current instance is leader.\n * If waitForLeadership is false, it will start replicating, even if it is not leader.\n * [default=true]\n */\n waitForLeadership: true,\n /**\n * If this is set to false,\n * the replication will not start automatically\n * but will wait for replicationState.start() being called.\n * (optional), default is true\n */\n autoStart: true,\n\n /**\n * Custom deleted field, the boolean property of the document data that\n * marks a document as being deleted.\n * If your backend uses a different fieldname then '_deleted', set the fieldname here.\n * RxDB will still store the documents internally with '_deleted', setting this field\n * only maps the data on the data layer.\n * \n * If a custom deleted field contains a non-boolean value, the deleted state\n * of the documents depends on if the value is truthy or not. So instead of providing a boolean * * deleted value, you could also work with using a 'deletedAt' timestamp instead.\n * \n * [default='_deleted']\n */\n deletedField: 'deleted',\n\n /**\n * Optional,\n * only needed when you want to replicate local changes to the remote instance.\n */\n push: {\n /**\n * Push handler\n */\n async handler(docs) {\n /**\n * Push the local documents to a remote REST server.\n */\n const rawResponse = await fetch('https://example.com/api/sync/push', {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ docs })\n });\n /**\n * Contains an array with all conflicts that appeared during this push.\n * If there were no conflicts, return an empty array.\n */\n const response = await rawResponse.json();\n return response;\n },\n /**\n * Batch size, optional\n * Defines how many documents will be given to the push handler at once.\n */\n batchSize: 5,\n /**\n * Modifies all documents before they are given to the push handler.\n * Can be used to swap out a custom deleted flag instead of the '_deleted' field.\n * If the push modifier return null, the document will be skipped and not send to the remote.\n * Notice that the modifier can be called multiple times and should not contain any side effects.\n * (optional)\n */\n modifier: d => d\n },\n /**\n * Optional,\n * only needed when you want to replicate remote changes to the local state.\n */\n pull: {\n /**\n * Pull handler\n */\n async handler(lastCheckpoint, batchSize) {\n const minTimestamp = lastCheckpoint ? lastCheckpoint.updatedAt : 0;\n /**\n * In this example we replicate with a remote REST server\n */\n const response = await fetch(\n `https://example.com/api/sync/?minUpdatedAt=${minTimestamp}&limit=${batchSize}`\n );\n const documentsFromRemote = await response.json();\n return {\n /**\n * Contains the pulled documents from the remote.\n * Not that if documentsFromRemote.length < batchSize,\n * then RxDB assumes that there are no more un-replicated documents\n * on the backend, so the replication will switch to 'Event observation' mode.\n */\n documents: documentsFromRemote,\n /**\n * The last checkpoint of the returned documents.\n * On the next call to the pull handler,\n * this checkpoint will be passed as 'lastCheckpoint'\n */\n checkpoint: documentsFromRemote.length === 0 ? lastCheckpoint : {\n id: lastOfArray(documentsFromRemote).id,\n updatedAt: lastOfArray(documentsFromRemote).updatedAt\n }\n };\n },\n batchSize: 10,\n /**\n * Modifies all documents after they have been pulled\n * but before they are used by RxDB.\n * Notice that the modifier can be called multiple times and should not contain any side effects.\n * (optional)\n */\n modifier: d => d,\n /**\n * Stream of the backend document writes.\n * See below.\n * You only need a stream$ when you have set live=true\n */\n stream$: pullStream$.asObservable()\n },\n});\n\n\n/**\n * Creating the pull stream for realtime replication.\n * Here we use a websocket but any other way of sending data to the client can be used,\n * like long polling or server-send events.\n */\nconst pullStream$ = new Subject>();\nlet firstOpen = true;\nfunction connectSocket() {\n const socket = new WebSocket('wss://example.com/api/sync/stream');\n /**\n * When the backend sends a new batch of documents+checkpoint,\n * emit it into the stream$.\n * \n * event.data must look like this\n * {\n * documents: [\n * {\n * id: 'foobar',\n * _deleted: false,\n * updatedAt: 1234\n * }\n * ],\n * checkpoint: {\n * id: 'foobar',\n * updatedAt: 1234\n * }\n * }\n */\n socket.onmessage = event => pullStream$.next(event.data);\n /**\n * Automatically reconnect the socket on close and error.\n */\n socket.onclose = () => connectSocket();\n socket.onerror = () => socket.close();\n\n socket.onopen = () => {\n if(firstOpen) {\n firstOpen = false;\n } else {\n /**\n * When the client is offline and goes online again,\n * it might have missed out events that happened on the server.\n * So we have to emit a RESYNC so that the replication goes\n * into 'Checkpoint iteration' mode until the client is in sync\n * and then it will go back into 'Event observation' mode again.\n */\n pullStream$.next('RESYNC');\n }\n }\n}\n\n"})}),"\n",(0,i.jsx)(t.h2,{id:"multi-tab-support",children:"Multi Tab support"}),"\n",(0,i.jsxs)(t.p,{children:["For better performance, the replication runs only in one instance when RxDB is used in multiple browser tabs or Node.js processes.\nBy setting ",(0,i.jsx)(t.code,{children:"waitForLeadership: false"})," you can enforce that each tab runs its own replication cycles.\nIf used in a multi instance setting, so when at database creation ",(0,i.jsx)(t.code,{children:"multiInstance: false"})," was not set,\nyou need to import the ",(0,i.jsx)(t.a,{href:"/leader-election.html",children:"leader election plugin"})," so that RxDB can know how many instances exist and which browser tab should run the replication."]}),"\n",(0,i.jsx)(t.h2,{id:"error-handling",children:"Error handling"}),"\n",(0,i.jsxs)(t.p,{children:["When sending a document to the remote fails for any reason, RxDB will send it again in a later point in time.\nThis happens for ",(0,i.jsx)(t.strong,{children:"all"})," errors. The document write could have already reached the remote instance and be processed, while only the answering fails.\nThe remote instance must be designed to handle this properly and to not crash on duplicate data transmissions.\nDepending on your use case, it might be ok to just write the duplicate document data again.\nBut for a more resilient error handling you could compare the last write timestamps or add a unique write id field to the document. This field can then be used to detect duplicates and ignore re-send data."]}),"\n",(0,i.jsxs)(t.p,{children:["Also the replication has an ",(0,i.jsx)(t.code,{children:".error$"})," stream that emits all ",(0,i.jsx)(t.code,{children:"RxError"})," objects that arise during replication.\nNotice that these errors contain an inner ",(0,i.jsx)(t.code,{children:".parameters.errors"})," field that contains the original error. Also they contain a ",(0,i.jsx)(t.code,{children:".parameters.direction"})," field that indicates if the error was thrown during ",(0,i.jsx)(t.code,{children:"pull"})," or ",(0,i.jsx)(t.code,{children:"push"}),". You can use these to properly handle errors. For example when the client is outdated, the server might respond with a ",(0,i.jsx)(t.code,{children:"426 Upgrade Required"})," error code that can then be used to force a page reload."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"replicationState.error$.subscribe((error) => {\n if(\n error.parameters.errors &&\n error.parameters.errors[0] &&\n error.parameters.errors[0].code === 426\n ) {\n // client is outdated -> enforce a page reload\n location.reload();\n }\n});\n"})}),"\n",(0,i.jsx)(t.h2,{id:"security",children:"Security"}),"\n",(0,i.jsxs)(t.p,{children:["Be aware that client side clocks can never be trusted. When you have a client-backend replication, the backend should overwrite the ",(0,i.jsx)(t.code,{children:"updatedAt"})," timestamp or use another field, when it receives the change from the client."]}),"\n",(0,i.jsx)(t.h2,{id:"rxreplicationstate",children:"RxReplicationState"}),"\n",(0,i.jsxs)(t.p,{children:["The function ",(0,i.jsx)(t.code,{children:"replicateRxCollection()"})," returns a ",(0,i.jsx)(t.code,{children:"RxReplicationState"})," that can be used to manage and observe the replication."]}),"\n",(0,i.jsx)(t.h3,{id:"observable",children:"Observable"}),"\n",(0,i.jsxs)(t.p,{children:["To observe the replication, the ",(0,i.jsx)(t.code,{children:"RxReplicationState"})," has some ",(0,i.jsx)(t.code,{children:"Observable"})," properties:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"// emits each document that was received from the remote\nmyRxReplicationState.received$.subscribe(doc => console.dir(doc));\n\n// emits each document that was send to the remote\nmyRxReplicationState.sent$.subscribe(doc => console.dir(doc));\n\n// emits all errors that happen when running the push- & pull-handlers.\nmyRxReplicationState.error$.subscribe(error => console.dir(error));\n\n// emits true when the replication was canceled, false when not.\nmyRxReplicationState.canceled$.subscribe(bool => console.dir(bool));\n\n// emits true when a replication cycle is running, false when not.\nmyRxReplicationState.active$.subscribe(bool => console.dir(bool));\n"})}),"\n",(0,i.jsx)(t.h3,{id:"awaitinitialreplication",children:"awaitInitialReplication()"}),"\n",(0,i.jsxs)(t.p,{children:["With ",(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," you can await the initial replication that is done when a full replication cycle was successful finished for the first time. The returned promise will never resolve if you cancel the replication before the initial replication can be done."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"await myRxReplicationState.awaitInitialReplication();\n"})}),"\n",(0,i.jsx)(t.h3,{id:"awaitinsync",children:"awaitInSync()"}),"\n",(0,i.jsxs)(t.p,{children:["Returns a ",(0,i.jsx)(t.code,{children:"Promise"})," that resolves when:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," has emitted."]}),"\n",(0,i.jsx)(t.li,{children:"All local data is replicated with the remote."}),"\n",(0,i.jsx)(t.li,{children:"No replication cycle is running or in retry-state."}),"\n"]}),"\n",(0,i.jsxs)(t.admonition,{type:"warning",children:[(0,i.jsxs)(t.p,{children:["When ",(0,i.jsx)(t.code,{children:"multiInstance: true"})," and ",(0,i.jsx)(t.code,{children:"waitForLeadership: true"})," and another tab is already running the replication, ",(0,i.jsx)(t.code,{children:"awaitInSync()"})," will not resolve until the other tab is closed and the replication starts in this tab."]}),(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"await myRxReplicationState.awaitInSync();\n"})})]}),"\n",(0,i.jsxs)(t.admonition,{type:"warning",children:[(0,i.jsx)(t.mdxAdmonitionTitle,{}),(0,i.jsxs)(t.h4,{id:"awaitinitialreplication-and-awaitinsync-should-not-be-used-to-block-the-application",children:[(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," and ",(0,i.jsx)(t.code,{children:"awaitInSync()"})," should not be used to block the application"]}),(0,i.jsxs)(t.p,{children:["A common mistake in RxDB usage is when developers want to block the app usage until the application is in sync.\nOften they just ",(0,i.jsx)(t.code,{children:"await"})," the promise of ",(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," or ",(0,i.jsx)(t.code,{children:"awaitInSync()"})," and show a loading spinner until they resolve. This is dangerous and should not be done because:"]}),(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["When ",(0,i.jsx)(t.code,{children:"multiInstance: true"})," and ",(0,i.jsx)(t.code,{children:"waitForLeadership: true (default)"})," and another tab is already running the replication, ",(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," will not resolve until the other tab is closed and the replication starts in this tab."]}),"\n",(0,i.jsxs)(t.li,{children:["Your app can no longer be started when the device is offline because there the ",(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," will never resolve and the app cannot be used."]}),"\n"]}),(0,i.jsxs)(t.p,{children:["Instead you should store the last in-sync time in a ",(0,i.jsx)(t.a,{href:"/rx-local-document.html",children:"local document"})," and observe its value on all instances."]}),(0,i.jsx)(t.p,{children:"For example if you want to block clients from using the app if they have not been in sync for the last 24 hours, you could use this code:"}),(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"\n// update last-in-sync-flag each time replication is in sync\nawait myCollection.insertLocal('last-in-sync', { time: 0 }).catch(); // ensure flag exists\nmyReplicationState.active$.pipe(\n mergeMap(async() => {\n await myReplicationState.awaitInSync();\n await myCollection.upsertLocal('last-in-sync', { time: Date.now() })\n })\n);\n\n// observe the flag and toggle loading spinner\nawait showLoadingSpinner();\nconst oneDay = 1000 * 60 * 60 *24;\nawait firstValueFrom(\n myCollection.getLocal$('last-in-sync').pipe(\n filter(d => d.get('time') > (Date.now() - oneDay))\n )\n);\nawait hideLoadingSpinner();\n"})})]}),"\n",(0,i.jsx)(t.h3,{id:"resync",children:"reSync()"}),"\n",(0,i.jsxs)(t.p,{children:["Triggers a ",(0,i.jsx)(t.code,{children:"RESYNC"})," cycle where the replication goes into ",(0,i.jsx)(t.a,{href:"#checkpoint-iteration",children:"checkpoint iteration"})," until the client is in sync with the backend. Used in unit tests or when no proper ",(0,i.jsx)(t.code,{children:"pull.stream$"})," can be implemented so that the client only knows that something has been changed but not what."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"myRxReplicationState.reSync();\n"})}),"\n",(0,i.jsxs)(t.p,{children:["If your backend is not capable of sending events to the client at all, you could run ",(0,i.jsx)(t.code,{children:"reSync()"})," in an interval so that the client will automatically fetch server changes after some time at least."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"// trigger RESYNC each 10 seconds.\nsetInterval(() => myRxReplicationState.reSync(), 10 * 1000);\n"})}),"\n",(0,i.jsx)(t.h3,{id:"cancel",children:"cancel()"}),"\n",(0,i.jsx)(t.p,{children:"Cancels the replication. Returns a promise that resolved when everything has been cleaned up."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"await myRxReplicationState.cancel()\n"})}),"\n",(0,i.jsx)(t.h3,{id:"isstopped",children:"isStopped()"}),"\n",(0,i.jsxs)(t.p,{children:["Returns ",(0,i.jsx)(t.code,{children:"true"})," if the replication is stopped. This can be if a non-live replication is finished or a replication got canceled."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-js",children:"replicationState.isStopped(); // true/false\n"})}),"\n",(0,i.jsx)(t.h3,{id:"setting-a-custom-initialcheckpoint-beta",children:"Setting a custom initialCheckpoint (beta)"}),"\n",(0,i.jsxs)(t.p,{children:["By default, the push replication will start from the beginning of time and push all documents from there to the remote.\nBy setting a custom ",(0,i.jsx)(t.code,{children:"push.initialCheckpoint"}),", you can tell the replication to only push writes that are newer than the given checkpoint."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"// store the latest checkpoint of a collection\nlet lastLocalCheckpoint: any;\nmyCollection.checkpoint$.subscribe(checkpoint => lastLocalCheckpoint = checkpoint);\n\n// start the replication but only push documents that are newer than the lastLocalCheckpoint\nconst replicationState = replicateRxCollection({\n collection: myCollection,\n replicationIdentifier: 'my-custom-replication-with-init-checkpoint',\n /* ... */\n push: {\n handler: /* ... */,\n initialCheckpoint: lastLocalCheckpoint\n }\n});\n"})}),"\n",(0,i.jsxs)(t.p,{children:["The same can be done for the other direction by setting a ",(0,i.jsx)(t.code,{children:"pull.initialCheckpoint"}),". Notice that here we need the remote checkpoint from the backend instead of the one from the RxDB storage."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"// get the last pull checkpoint from the server\nconst lastRemoteCheckpoint = await (await fetch('http://example.com/pull-checkpoint')).json();\n\n// start the replication but only pull documents that are newer than the lastRemoteCheckpoint\nconst replicationState = replicateRxCollection({\n collection: myCollection,\n replicationIdentifier: 'my-custom-replication-with-init-checkpoint',\n /* ... */\n pull: {\n handler: /* ... */,\n initialCheckpoint: lastRemoteCheckpoint\n }\n});\n"})}),"\n",(0,i.jsx)(t.h3,{id:"attachment-replication-beta",children:"Attachment replication (beta)"}),"\n",(0,i.jsxs)(t.p,{children:["Attachment replication is supported in the RxDB replication protocol itself. However not all replication plugins support it.\nIf you start the replication with a collection which has ",(0,i.jsx)(t.a,{href:"/rx-attachment.html",children:"enabled RxAttachments"})," attachments data will be added to all push- and write data."]}),"\n",(0,i.jsxs)(t.p,{children:["The pushed documents will contain an ",(0,i.jsx)(t.code,{children:"_attachments"})," object which contains:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"The attachment meta data (id, length, digest) of all non-attachments"}),"\n",(0,i.jsx)(t.li,{children:"The full attachment data of all attachments that have been updated/added from the client."}),"\n",(0,i.jsx)(t.li,{children:"Deleted attachments are spared out in the pushed document."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"With this data, the backend can decide onto which attachments must be deleted, added or overwritten."}),"\n",(0,i.jsx)(t.p,{children:"Accordingly, the pulled document must contain the same data, if the backend has a new document state with updated attachments."})]})}function d(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>l,x:()=>s});var i=n(6540);const a={},o=i.createContext(a);function l(e){const t=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:l(e.components),i.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/6ae3580c.9d39383d.js b/docs/assets/js/6ae3580c.9d39383d.js deleted file mode 100644 index f80f927ef3c..00000000000 --- a/docs/assets/js/6ae3580c.9d39383d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[5320],{5376:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>r,contentTitle:()=>l,default:()=>d,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var i=n(4848),a=n(8453);const o={title:"\u2699\ufe0f Replication Protocol",slug:"replication.html"},l="RxDB Database Replication Protocol",s={id:"replication",title:"\u2699\ufe0f Replication Protocol",description:"The RxDB replication protocol provides the ability to replicate the database state in realtime between the clients and the server.",source:"@site/docs/replication.md",sourceDirName:".",slug:"/replication.html",permalink:"/replication.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"\u2699\ufe0f Replication Protocol",slug:"replication.html"},sidebar:"tutorialSidebar",previous:{title:"Electron Plugin",permalink:"/electron.html"},next:{title:"HTTP Replication",permalink:"/replication-http.html"}},r={},c=[{value:"Replication protocol on the document level",id:"replication-protocol-on-the-document-level",level:2},{value:"Replication protocol on the transfer level",id:"replication-protocol-on-the-transfer-level",level:2},{value:"Checkpoint iteration",id:"checkpoint-iteration",level:3},{value:"Event observation",id:"event-observation",level:3},{value:"Data layout on the server",id:"data-layout-on-the-server",level:2},{value:"Conflict handling",id:"conflict-handling",level:2},{value:"replicateRxCollection()",id:"replicaterxcollection",level:2},{value:"Multi Tab support",id:"multi-tab-support",level:2},{value:"Error handling",id:"error-handling",level:2},{value:"Security",id:"security",level:2},{value:"RxReplicationState",id:"rxreplicationstate",level:2},{value:"Observable",id:"observable",level:3},{value:"awaitInitialReplication()",id:"awaitinitialreplication",level:3},{value:"awaitInSync()",id:"awaitinsync",level:3},{value:"Warning: awaitInitialReplication() and awaitInSync() should not be used to block the application",id:"warning-awaitinitialreplication-and-awaitinsync-should-not-be-used-to-block-the-application",level:3},{value:"reSync()",id:"resync",level:3},{value:"cancel()",id:"cancel",level:3},{value:"isStopped()",id:"isstopped",level:3},{value:"Setting a custom initialCheckpoint (beta)",id:"setting-a-custom-initialcheckpoint-beta",level:3},{value:"Attachment replication (beta)",id:"attachment-replication-beta",level:3}];function h(e){const t={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"rxdb-database-replication-protocol",children:"RxDB Database Replication Protocol"}),"\n",(0,i.jsxs)(t.p,{children:["The RxDB replication protocol provides the ability to replicate the database state in ",(0,i.jsx)(t.strong,{children:"realtime"})," between the clients and the server."]}),"\n",(0,i.jsxs)(t.p,{children:["The backend server does not have to be a RxDB instance; you can build a replication with ",(0,i.jsx)(t.strong,{children:"any infrastructure"}),".\nFor example you can replicate with a custom GraphQL endpoint or a ",(0,i.jsx)(t.a,{href:"/replication-http.html",children:"http server"})," on top of a PostgreSQL database."]}),"\n",(0,i.jsxs)(t.p,{children:["The replication is made to support the ",(0,i.jsx)(t.a,{href:"http://offlinefirst.org/",children:"Offline-First"})," paradigm, so that when the client goes offline, the RxDB database can still read and write locally and will continue the replication when the client goes online again."]}),"\n",(0,i.jsx)(t.h2,{id:"replication-protocol-on-the-document-level",children:"Replication protocol on the document level"}),"\n",(0,i.jsx)(t.p,{children:"On the RxDocument level, the replication works like git, where the fork/client contains all new writes and must be merged with the master/server before it can push its new state to the master/server."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"A---B-----------D master/server state\n \\ /\n B---C---D fork/client state\n"})}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["The client pulls the latest state ",(0,i.jsx)(t.code,{children:"B"})," from the master."]}),"\n",(0,i.jsxs)(t.li,{children:["The client does some changes ",(0,i.jsx)(t.code,{children:"C+D"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:["The client pushes these changes to the master by sending the latest known master state ",(0,i.jsx)(t.code,{children:"B"})," and the new client state ",(0,i.jsx)(t.code,{children:"D"})," of the document."]}),"\n",(0,i.jsxs)(t.li,{children:["If the master state is equal to the latest master ",(0,i.jsx)(t.code,{children:"B"})," state of the client, the new client state ",(0,i.jsx)(t.code,{children:"D"})," is set as the latest master state."]}),"\n",(0,i.jsx)(t.li,{children:"If the master also had changes and so the latest master change is different then the one that the client assumes, we have a conflict that has to be resolved on the client."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"replication-protocol-on-the-transfer-level",children:"Replication protocol on the transfer level"}),"\n",(0,i.jsxs)(t.p,{children:["When document states are transferred, all handlers use batches of documents for better performance.\nThe server ",(0,i.jsx)(t.strong,{children:"must"})," implement the following methods to be compatible with the replication:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"pullHandler"})," Get the last checkpoint (or null) as input. Returns all documents that have been written ",(0,i.jsx)(t.strong,{children:"after"})," the given checkpoint. Also returns the checkpoint of the latest written returned document."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"pushHandler"})," a method that can be called by the client to send client side writes to the master. It gets an array with the ",(0,i.jsx)(t.code,{children:"assumedMasterState"})," and the ",(0,i.jsx)(t.code,{children:"newForkState"})," of each document write as input. It must return an array that contains the master document states of all conflicts. If there are no conflicts, it must return an empty array."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"pullStream"})," an observable that emits batches of all master writes and the latest checkpoint of the write batches."]}),"\n"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:" +--------+ +--------+ \n | | pullHandler() | |\n | |---------------------\x3e | | \n | | | | \n | | | | \n | Client | pushHandler() | Server | \n | |---------------------\x3e | | \n | | | |\n | | pullStream$ | | \n | | <-------------------------| | \n +--------+ +--------+\n"})}),"\n",(0,i.jsxs)(t.p,{children:["The replication runs in two ",(0,i.jsx)(t.strong,{children:"different modes"}),":"]}),"\n",(0,i.jsx)(t.h3,{id:"checkpoint-iteration",children:"Checkpoint iteration"}),"\n",(0,i.jsxs)(t.p,{children:["On first initial replication, or when the client comes online again, a checkpoint based iteration is used to catch up with the server state.\nA checkpoint is a subset of the fields of the last pulled document. When the checkpoint is send to the backend via ",(0,i.jsx)(t.code,{children:"pullHandler()"}),", the backend must be able to respond with all documents that have been written ",(0,i.jsx)(t.strong,{children:"after"})," the given checkpoint.\nFor example if your documents contain an ",(0,i.jsx)(t.code,{children:"id"})," and an ",(0,i.jsx)(t.code,{children:"updatedAt"})," field, these two can be used as checkpoint."]}),"\n",(0,i.jsxs)(t.p,{children:["When the checkpoint iteration reaches the last checkpoint, where the backend returns an empty array because there are no newer documents, the replication will automatically switch to the ",(0,i.jsx)(t.code,{children:"event observation"})," mode."]}),"\n",(0,i.jsx)(t.h3,{id:"event-observation",children:"Event observation"}),"\n",(0,i.jsxs)(t.p,{children:["While the client is connected to the backend, the events from the backend are observed via ",(0,i.jsx)(t.code,{children:"pullStream$"})," and persisted to the client."]}),"\n",(0,i.jsxs)(t.p,{children:["If your backend for any reason is not able to provide a full ",(0,i.jsx)(t.code,{children:"pullStream$"})," that contains all events and the checkpoint, you can instead only emit ",(0,i.jsx)(t.code,{children:"RESYNC"})," events that tell RxDB that anything unknown has changed on the server and it should run the pull replication via ",(0,i.jsx)(t.a,{href:"#checkpoint-iteration",children:"checkpoint iteration"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["When the client goes offline and online again, it might happen that the ",(0,i.jsx)(t.code,{children:"pullStream$"})," has missed out some events. Therefore the ",(0,i.jsx)(t.code,{children:"pullStream$"})," should also emit a ",(0,i.jsx)(t.code,{children:"RESYNC"})," event each time the client reconnects, so that the client can become in sync with the backend via the ",(0,i.jsx)(t.a,{href:"#checkpoint-iteration",children:"checkpoint iteration"})," mode."]}),"\n",(0,i.jsx)(t.h2,{id:"data-layout-on-the-server",children:"Data layout on the server"}),"\n",(0,i.jsx)(t.p,{children:"To use the replication you first have to ensure that:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.strong,{children:"documents are deterministic sortable by their last write time"})}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.em,{children:"deterministic"})," means that even if two documents have the same ",(0,i.jsx)(t.em,{children:"last write time"}),", they have a predictable sort order.\nThis is most often ensured by using the ",(0,i.jsx)(t.em,{children:"primaryKey"})," as second sort parameter as part of the checkpoint."]}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsx)(t.p,{children:(0,i.jsxs)(t.strong,{children:["documents are never deleted, instead the ",(0,i.jsx)(t.code,{children:"_deleted"})," field is set to ",(0,i.jsx)(t.code,{children:"true"}),"."]})}),"\n",(0,i.jsx)(t.p,{children:"This is needed so that the deletion state of a document exists in the database and can be replicated with other instances. If your backend uses a different field to mark deleted documents, you have to transform the data in the push/pull handlers or with the modifiers."}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"For example if your documents look like this:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:'const docData = {\n "id": "foobar",\n "name": "Alice",\n "lastName": "Wilson",\n /**\n * Contains the last write timestamp\n * so all documents writes can be sorted by that value\n * when they are fetched from the remote instance.\n */\n "updatedAt": 1564483474,\n /**\n * Instead of physically deleting documents,\n * a deleted document gets replicated.\n */\n "_deleted": false\n}\n'})}),"\n",(0,i.jsxs)(t.p,{children:["Then your data is always sortable by ",(0,i.jsx)(t.code,{children:"updatedAt"}),". This ensures that when RxDB fetches 'new' changes via ",(0,i.jsx)(t.code,{children:"pullHandler()"}),", it can send the latest ",(0,i.jsx)(t.code,{children:"updatedAt+id"})," checkpoint to the remote endpoint and then receive all newer documents."]}),"\n",(0,i.jsxs)(t.p,{children:["By default, the field is ",(0,i.jsx)(t.code,{children:"_deleted"}),". If your remote endpoint uses a different field to mark deleted documents, you can set the ",(0,i.jsx)(t.code,{children:"deletedField"})," in the replication options which will automatically map the field on all pull and push requests."]}),"\n",(0,i.jsx)(t.h2,{id:"conflict-handling",children:"Conflict handling"}),"\n",(0,i.jsx)(t.p,{children:"When multiple clients (or the server) modify the same document at the same time (or when they are offline), it can happen that a conflict arises during the replication."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"A---B1---C1---X master/server state\n \\ /\n B1---C2 fork/client state\n"})}),"\n",(0,i.jsxs)(t.p,{children:["In the case above, the client would tell the master to move the document state from ",(0,i.jsx)(t.code,{children:"B1"})," to ",(0,i.jsx)(t.code,{children:"C2"})," by calling ",(0,i.jsx)(t.code,{children:"pushHandler()"}),". But because the actual master state is ",(0,i.jsx)(t.code,{children:"C1"})," and not ",(0,i.jsx)(t.code,{children:"B1"}),", the master would reject the write by sending back the actual master state ",(0,i.jsx)(t.code,{children:"C1"}),".\n",(0,i.jsx)(t.strong,{children:"RxDB resolves all conflicts on the client"})," so it would call the conflict handler of the ",(0,i.jsx)(t.code,{children:"RxCollection"})," and create a new document state ",(0,i.jsx)(t.code,{children:"D"})," that can then be written to the master."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"A---B1---C1---X---D master/server state\n \\ / \\ /\n B1---C2---D fork/client state\n"})}),"\n",(0,i.jsxs)(t.p,{children:["The default conflict handler will always drop the fork state and use the master state. This ensures that clients that are offline for a very long time, do not accidentally overwrite other peoples changes when they go online again.\nYou can specify a custom conflict handler by setting the property ",(0,i.jsx)(t.code,{children:"conflictHandler"})," when calling ",(0,i.jsx)(t.code,{children:"addCollection()"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["Learn how to create a ",(0,i.jsx)(t.a,{href:"/transactions-conflicts-revisions.html#custom-conflict-handler",children:"custom conflict handler"}),"."]}),"\n",(0,i.jsx)(t.h2,{id:"replicaterxcollection",children:"replicateRxCollection()"}),"\n",(0,i.jsxs)(t.p,{children:["You can start the replication of a single ",(0,i.jsx)(t.code,{children:"RxCollection"})," by calling ",(0,i.jsx)(t.code,{children:"replicateRxCollection()"})," like in the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import { replicateRxCollection } from 'rxdb/plugins/replication';\nimport {\n lastOfArray\n} from 'rxdb';\nconst replicationState = await replicateRxCollection({\n collection: myRxCollection,\n /**\n * An id for the replication to identify it\n * and so that RxDB is able to resume the replication on app reload.\n * If you replicate with a remote server, it is recommended to put the\n * server url into the replicationIdentifier.\n */\n replicationIdentifier: 'my-rest-replication-to-https://example.com/api/sync',\n /**\n * By default it will do an ongoing realtime replication.\n * By settings live: false the replication will run once until the local state\n * is in sync with the remote state, then it will cancel itself.\n * (optional), default is true.\n */\n live: true,\n /**\n * Time in milliseconds after when a failed backend request\n * has to be retried.\n * This time will be skipped if a offline->online switch is detected\n * via navigator.onLine\n * (optional), default is 5 seconds.\n */\n retryTime: 5 * 1000,\n /**\n * When multiInstance is true, like when you use RxDB in multiple browser tabs,\n * the replication should always run in only one of the open browser tabs.\n * If waitForLeadership is true, it will wait until the current instance is leader.\n * If waitForLeadership is false, it will start replicating, even if it is not leader.\n * [default=true]\n */\n waitForLeadership: true,\n /**\n * If this is set to false,\n * the replication will not start automatically\n * but will wait for replicationState.start() being called.\n * (optional), default is true\n */\n autoStart: true,\n\n /**\n * Custom deleted field, the boolean property of the document data that\n * marks a document as being deleted.\n * If your backend uses a different fieldname then '_deleted', set the fieldname here.\n * RxDB will still store the documents internally with '_deleted', setting this field\n * only maps the data on the data layer.\n * \n * If a custom deleted field contains a non-boolean value, the deleted state\n * of the documents depends on if the value is truthy or not. So instead of providing a boolean * * deleted value, you could also work with using a 'deletedAt' timestamp instead.\n * \n * [default='_deleted']\n */\n deletedField: 'deleted',\n\n /**\n * Optional,\n * only needed when you want to replicate local changes to the remote instance.\n */\n push: {\n /**\n * Push handler\n */\n async handler(docs) {\n /**\n * Push the local documents to a remote REST server.\n */\n const rawResponse = await fetch('https://example.com/api/sync/push', {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ docs })\n });\n /**\n * Contains an array with all conflicts that appeared during this push.\n * If there were no conflicts, return an empty array.\n */\n const response = await rawResponse.json();\n return response;\n },\n /**\n * Batch size, optional\n * Defines how many documents will be given to the push handler at once.\n */\n batchSize: 5,\n /**\n * Modifies all documents before they are given to the push handler.\n * Can be used to swap out a custom deleted flag instead of the '_deleted' field.\n * If the push modifier return null, the document will be skipped and not send to the remote.\n * Notice that the modifier can be called multiple times and should not contain any side effects.\n * (optional)\n */\n modifier: d => d\n },\n /**\n * Optional,\n * only needed when you want to replicate remote changes to the local state.\n */\n pull: {\n /**\n * Pull handler\n */\n async handler(lastCheckpoint, batchSize) {\n const minTimestamp = lastCheckpoint ? lastCheckpoint.updatedAt : 0;\n /**\n * In this example we replicate with a remote REST server\n */\n const response = await fetch(\n `https://example.com/api/sync/?minUpdatedAt=${minTimestamp}&limit=${batchSize}`\n );\n const documentsFromRemote = await response.json();\n return {\n /**\n * Contains the pulled documents from the remote.\n * Notice: If documentsFromRemote.length < batchSize,\n * then RxDB assumes that there are no more un-replicated documents\n * on the backend, so the replication will switch to 'Event observation' mode.\n */\n documents: documentsFromRemote,\n /**\n * The last checkpoint of the returned documents.\n * On the next call to the pull handler,\n * this checkpoint will be passed as 'lastCheckpoint'\n */\n checkpoint: documentsFromRemote.length === 0 ? lastCheckpoint : {\n id: lastOfArray(documentsFromRemote).id,\n updatedAt: lastOfArray(documentsFromRemote).updatedAt\n }\n };\n },\n batchSize: 10,\n /**\n * Modifies all documents after they have been pulled\n * but before they are used by RxDB.\n * Notice that the modifier can be called multiple times and should not contain any side effects.\n * (optional)\n */\n modifier: d => d,\n /**\n * Stream of the backend document writes.\n * See below.\n * You only need a stream$ when you have set live=true\n */\n stream$: pullStream$.asObservable()\n },\n});\n\n\n/**\n * Creating the pull stream for realtime replication.\n * Here we use a websocket but any other way of sending data to the client can be used,\n * like long polling or server-send events.\n */\nconst pullStream$ = new Subject>();\nlet firstOpen = true;\nfunction connectSocket() {\n const socket = new WebSocket('wss://example.com/api/sync/stream');\n /**\n * When the backend sends a new batch of documents+checkpoint,\n * emit it into the stream$.\n * \n * event.data must look like this\n * {\n * documents: [\n * {\n * id: 'foobar',\n * _deleted: false,\n * updatedAt: 1234\n * }\n * ],\n * checkpoint: {\n * id: 'foobar',\n * updatedAt: 1234\n * }\n * }\n */\n socket.onmessage = event => pullStream$.next(event.data);\n /**\n * Automatically reconnect the socket on close and error.\n */\n socket.onclose = () => connectSocket();\n socket.onerror = () => socket.close();\n\n socket.onopen = () => {\n if(firstOpen) {\n firstOpen = false;\n } else {\n /**\n * When the client is offline and goes online again,\n * it might have missed out events that happened on the server.\n * So we have to emit a RESYNC so that the replication goes\n * into 'Checkpoint iteration' mode until the client is in sync\n * and then it will go back into 'Event observation' mode again.\n */\n pullStream$.next('RESYNC');\n }\n }\n}\n\n"})}),"\n",(0,i.jsx)(t.h2,{id:"multi-tab-support",children:"Multi Tab support"}),"\n",(0,i.jsxs)(t.p,{children:["For better performance, the replication runs only in one instance when RxDB is used in multiple browser tabs or Node.js processes.\nBy setting ",(0,i.jsx)(t.code,{children:"waitForLeadership: false"})," you can enforce that each tab runs its own replication cycles.\nIf used in a multi instance setting, so when at database creation ",(0,i.jsx)(t.code,{children:"multiInstance: false"})," was not set,\nyou need to import the ",(0,i.jsx)(t.a,{href:"/leader-election.html",children:"leader election plugin"})," so that RxDB can know how many instances exist and which browser tab should run the replication."]}),"\n",(0,i.jsx)(t.h2,{id:"error-handling",children:"Error handling"}),"\n",(0,i.jsxs)(t.p,{children:["When sending a document to the remote fails for any reason, RxDB will send it again in a later point in time.\nThis happens for ",(0,i.jsx)(t.strong,{children:"all"})," errors. The document write could have already reached the remote instance and be processed, while only the answering fails.\nThe remote instance must be designed to handle this properly and to not crash on duplicate data transmissions.\nDepending on your use case, it might be ok to just write the duplicate document data again.\nBut for a more resilient error handling you could compare the last write timestamps or add a unique write id field to the document. This field can then be used to detect duplicates and ignore re-send data."]}),"\n",(0,i.jsxs)(t.p,{children:["Also the replication has an ",(0,i.jsx)(t.code,{children:".error$"})," stream that emits all ",(0,i.jsx)(t.code,{children:"RxError"})," objects that arise during replication.\nNotice that these errors contain an inner ",(0,i.jsx)(t.code,{children:".parameters.errors"})," field that contains the original error. Also they contain a ",(0,i.jsx)(t.code,{children:".parameters.direction"})," field that indicates if the error was thrown during ",(0,i.jsx)(t.code,{children:"pull"})," or ",(0,i.jsx)(t.code,{children:"push"}),". You can use these to properly handle errors. For example when the client is outdated, the server might respond with a ",(0,i.jsx)(t.code,{children:"426 Upgrade Required"})," error code that can then be used to force a page reload."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"replicationState.error$.subscribe((error) => {\n if(\n error.parameters.errors &&\n error.parameters.errors[0] &&\n error.parameters.errors[0].code === 426\n ) {\n // client is outdated -> enforce a page reload\n location.reload();\n }\n});\n"})}),"\n",(0,i.jsx)(t.h2,{id:"security",children:"Security"}),"\n",(0,i.jsxs)(t.p,{children:["Be aware that client side clocks can never be trusted. When you have a client-backend replication, the backend should overwrite the ",(0,i.jsx)(t.code,{children:"updatedAt"})," timestamp or use another field, when it receives the change from the client."]}),"\n",(0,i.jsx)(t.h2,{id:"rxreplicationstate",children:"RxReplicationState"}),"\n",(0,i.jsxs)(t.p,{children:["The function ",(0,i.jsx)(t.code,{children:"replicateRxCollection()"})," returns a ",(0,i.jsx)(t.code,{children:"RxReplicationState"})," that can be used to manage and observe the replication."]}),"\n",(0,i.jsx)(t.h3,{id:"observable",children:"Observable"}),"\n",(0,i.jsxs)(t.p,{children:["To observe the replication, the ",(0,i.jsx)(t.code,{children:"RxReplicationState"})," has some ",(0,i.jsx)(t.code,{children:"Observable"})," properties:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"// emits each document that was received from the remote\nmyRxReplicationState.received$.subscribe(doc => console.dir(doc));\n\n// emits each document that was send to the remote\nmyRxReplicationState.sent$.subscribe(doc => console.dir(doc));\n\n// emits all errors that happen when running the push- & pull-handlers.\nmyRxReplicationState.error$.subscribe(error => console.dir(error));\n\n// emits true when the replication was canceled, false when not.\nmyRxReplicationState.canceled$.subscribe(bool => console.dir(bool));\n\n// emits true when a replication cycle is running, false when not.\nmyRxReplicationState.active$.subscribe(bool => console.dir(bool));\n"})}),"\n",(0,i.jsx)(t.h3,{id:"awaitinitialreplication",children:"awaitInitialReplication()"}),"\n",(0,i.jsxs)(t.p,{children:["With ",(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," you can await the initial replication that is done when a full replication cycle was successful finished for the first time. The returned promise will never resolve if you cancel the replication before the initial replication can be done."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"await myRxReplicationState.awaitInitialReplication();\n"})}),"\n",(0,i.jsx)(t.h3,{id:"awaitinsync",children:"awaitInSync()"}),"\n",(0,i.jsxs)(t.p,{children:["Returns a ",(0,i.jsx)(t.code,{children:"Promise"})," that resolves when:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," has emitted."]}),"\n",(0,i.jsx)(t.li,{children:"All local data is replicated with the remote."}),"\n",(0,i.jsx)(t.li,{children:"No replication cycle is running or in retry-state."}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"WARNING:"})," When ",(0,i.jsx)(t.code,{children:"multiInstance: true"})," and ",(0,i.jsx)(t.code,{children:"waitForLeadership: true"})," and another tab is already running the replication, ",(0,i.jsx)(t.code,{children:"awaitInSync()"})," will not resolve until the other tab is closed and the replication starts in this tab."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"await myRxReplicationState.awaitInSync();\n"})}),"\n",(0,i.jsxs)(t.h3,{id:"warning-awaitinitialreplication-and-awaitinsync-should-not-be-used-to-block-the-application",children:["Warning: ",(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," and ",(0,i.jsx)(t.code,{children:"awaitInSync()"})," should not be used to block the application"]}),"\n",(0,i.jsxs)(t.p,{children:["A common mistake in RxDB usage is when developers want to block the app usage until the application is in sync.\nOften they just ",(0,i.jsx)(t.code,{children:"await"})," the promise of ",(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," or ",(0,i.jsx)(t.code,{children:"awaitInSync()"})," and show a loading spinner until they resolve. This is dangerous and should not be done because:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["When ",(0,i.jsx)(t.code,{children:"multiInstance: true"})," and ",(0,i.jsx)(t.code,{children:"waitForLeadership: true (default)"})," and another tab is already running the replication, ",(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," will not resolve until the other tab is closed and the replication starts in this tab."]}),"\n",(0,i.jsxs)(t.li,{children:["Your app can no longer be started when the device is offline because there the ",(0,i.jsx)(t.code,{children:"awaitInitialReplication()"})," will never resolve and the app cannot be used."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["Instead you should store the last in-sync time in a ",(0,i.jsx)(t.a,{href:"/rx-local-document.html",children:"local document"})," and observe its value on all instances."]}),"\n",(0,i.jsx)(t.p,{children:"For example if you want to block clients from using the app if they have not been in sync for the last 24 hours, you could use this code:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"\n// update last-in-sync-flag each time replication is in sync\nawait myCollection.insertLocal('last-in-sync', { time: 0 }).catch(); // ensure flag exists\nmyReplicationState.active$.pipe(\n mergeMap(async() => {\n await myReplicationState.awaitInSync();\n await myCollection.upsertLocal('last-in-sync', { time: Date.now() })\n })\n);\n\n// observe the flag and toggle loading spinner\nawait showLoadingSpinner();\nconst oneDay = 1000 * 60 * 60 *24;\nawait firstValueFrom(\n myCollection.getLocal$('last-in-sync').pipe(\n filter(d => d.get('time') > (Date.now() - oneDay))\n )\n);\nawait hideLoadingSpinner();\n"})}),"\n",(0,i.jsx)(t.h3,{id:"resync",children:"reSync()"}),"\n",(0,i.jsxs)(t.p,{children:["Triggers a ",(0,i.jsx)(t.code,{children:"RESYNC"})," cycle where the replication goes into ",(0,i.jsx)(t.a,{href:"#checkpoint-iteration",children:"checkpoint iteration"})," until the client is in sync with the backend. Used in unit tests or when no proper ",(0,i.jsx)(t.code,{children:"pull.stream$"})," can be implemented so that the client only knows that something has been changed but not what."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"myRxReplicationState.reSync();\n"})}),"\n",(0,i.jsxs)(t.p,{children:["If your backend is not capable of sending events to the client at all, you could run ",(0,i.jsx)(t.code,{children:"reSync()"})," in an interval so that the client will automatically fetch server changes after some time at least."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"// trigger RESYNC each 10 seconds.\nsetInterval(() => myRxReplicationState.reSync(), 10 * 1000);\n"})}),"\n",(0,i.jsx)(t.h3,{id:"cancel",children:"cancel()"}),"\n",(0,i.jsx)(t.p,{children:"Cancels the replication. Returns a promise that resolved when everything has been cleaned up."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"await myRxReplicationState.cancel()\n"})}),"\n",(0,i.jsx)(t.h3,{id:"isstopped",children:"isStopped()"}),"\n",(0,i.jsxs)(t.p,{children:["Returns ",(0,i.jsx)(t.code,{children:"true"})," if the replication is stopped. This can be if a non-live replication is finished or a replication got canceled."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-js",children:"replicationState.isStopped(); // true/false\n"})}),"\n",(0,i.jsx)(t.h3,{id:"setting-a-custom-initialcheckpoint-beta",children:"Setting a custom initialCheckpoint (beta)"}),"\n",(0,i.jsxs)(t.p,{children:["By default, the push replication will start from the beginning of time and push all documents from there to the remote.\nBy setting a custom ",(0,i.jsx)(t.code,{children:"push.initialCheckpoint"}),", you can tell the replication to only push writes that are newer than the given checkpoint."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"// store the latest checkpoint of a collection\nlet lastLocalCheckpoint: any;\nmyCollection.checkpoint$.subscribe(checkpoint => lastLocalCheckpoint = checkpoint);\n\n// start the replication but only push documents that are newer than the lastLocalCheckpoint\nconst replicationState = replicateRxCollection({\n collection: myCollection,\n replicationIdentifier: 'my-custom-replication-with-init-checkpoint',\n /* ... */\n push: {\n handler: /* ... */,\n initialCheckpoint: lastLocalCheckpoint\n }\n});\n"})}),"\n",(0,i.jsxs)(t.p,{children:["The same can be done for the other direction by setting a ",(0,i.jsx)(t.code,{children:"pull.initialCheckpoint"}),". Notice that here we need the remote checkpoint from the backend instead of the one from the RxDB storage."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"// get the last pull checkpoint from the server\nconst lastRemoteCheckpoint = await (await fetch('http://example.com/pull-checkpoint')).json();\n\n// start the replication but only pull documents that are newer than the lastRemoteCheckpoint\nconst replicationState = replicateRxCollection({\n collection: myCollection,\n replicationIdentifier: 'my-custom-replication-with-init-checkpoint',\n /* ... */\n pull: {\n handler: /* ... */,\n initialCheckpoint: lastRemoteCheckpoint\n }\n});\n"})}),"\n",(0,i.jsx)(t.h3,{id:"attachment-replication-beta",children:"Attachment replication (beta)"}),"\n",(0,i.jsxs)(t.p,{children:["Attachment replication is supported in the RxDB replication protocol itself. However not all replication plugins support it.\nIf you start the replication with a collection which has ",(0,i.jsx)(t.a,{href:"/rx-attachment.html",children:"enabled RxAttachments"})," attachments data will be added to all push- and write data."]}),"\n",(0,i.jsxs)(t.p,{children:["The pushed documents will contain an ",(0,i.jsx)(t.code,{children:"_attachments"})," object which contains:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"The attachment meta data (id, length, digest) of all non-attachments"}),"\n",(0,i.jsx)(t.li,{children:"The full attachment data of all attachments that have been updated/added from the client."}),"\n",(0,i.jsx)(t.li,{children:"Deleted attachments are spared out in the pushed document."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"With this data, the backend can decide onto which attachments must be deleted, added or overwritten."}),"\n",(0,i.jsx)(t.p,{children:"Accordingly, the pulled document must contain the same data, if the backend has a new document state with updated attachments."})]})}function d(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>l,x:()=>s});var i=n(6540);const a={},o=i.createContext(a);function l(e){const t=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:l(e.components),i.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/6bfb0089.2da9b579.js b/docs/assets/js/6bfb0089.2da9b579.js deleted file mode 100644 index 9e98b1a8c5d..00000000000 --- a/docs/assets/js/6bfb0089.2da9b579.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6422],{300:(e,a,n)=>{n.r(a),n.d(a,{assets:()=>i,contentTitle:()=>r,default:()=>h,frontMatter:()=>d,metadata:()=>o,toc:()=>l});var t=n(4848),s=n(8453);const d={title:"PouchDB Adapters",slug:"adapters.html"},r="PouchDB Adapters",o={id:"adapters",title:"PouchDB Adapters",description:"When you use PouchDB RxStorage, there are many adapters that define where the data has to be stored.",source:"@site/docs/adapters.md",sourceDirName:".",slug:"/adapters.html",permalink:"/adapters.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"PouchDB Adapters",slug:"adapters.html"}},i={},l=[{value:"IMPORTANT:",id:"important",level:2},{value:"Memory",id:"memory",level:2},{value:"Memdown",id:"memdown",level:2},{value:"IndexedDB",id:"indexeddb",level:2},{value:"IndexedDB",id:"indexeddb-1",level:2},{value:"Websql",id:"websql",level:2},{value:"leveldown",id:"leveldown",level:2},{value:"Node-Websql",id:"node-websql",level:2},{value:"react-native-sqlite",id:"react-native-sqlite",level:2},{value:"asyncstorage",id:"asyncstorage",level:2},{value:"asyncstorage-down",id:"asyncstorage-down",level:2},{value:"cordova-sqlite",id:"cordova-sqlite",level:2}];function c(e){const a={a:"a",code:"code",h1:"h1",h2:"h2",hr:"hr",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(a.h1,{id:"pouchdb-adapters",children:"PouchDB Adapters"}),"\n",(0,t.jsxs)(a.p,{children:["When you use PouchDB ",(0,t.jsx)(a.code,{children:"RxStorage"}),", there are many adapters that define where the data has to be stored.\nDepending on which environment you work in, you can choose between different adapters. For example, in the browser you want to store the data inside of IndexedDB but on NodeJS you want to store the data on the filesystem."]}),"\n",(0,t.jsx)(a.p,{children:"This page is an overview over the different adapters with recommendations on what to use where."}),"\n",(0,t.jsx)(a.hr,{}),"\n",(0,t.jsx)(a.h2,{id:"important",children:"IMPORTANT:"}),"\n",(0,t.jsxs)(a.p,{children:["The PouchDB RxStorage ",(0,t.jsx)(a.a,{href:"https://rxdb.info/questions-answers.html#why-is-the-pouchdb-rxstorage-deprecated",children:"is removed from RxDB"})," and can no longer be used in new projects. You should switch to a different ",(0,t.jsx)(a.a,{href:"/rx-storage.html",children:"RxStorage"}),"."]}),"\n",(0,t.jsx)(a.hr,{}),"\n",(0,t.jsxs)(a.p,{children:["Please always ensure that your pouchdb adapter-version is the same as ",(0,t.jsx)(a.code,{children:"pouchdb-core"})," in the ",(0,t.jsx)(a.a,{href:"https://github.com/pubkey/rxdb/blob/master/package.json",children:"rxdb package.json"}),". Otherwise, you might have strange problems."]}),"\n",(0,t.jsx)(a.h1,{id:"any-environment",children:"Any environment"}),"\n",(0,t.jsx)(a.h2,{id:"memory",children:"Memory"}),"\n",(0,t.jsx)(a.p,{children:"In any environment, you can use the memory-adapter. It stores the data in the javascript runtime memory. This means it is not persistent and the data is lost when the process terminates."}),"\n",(0,t.jsx)(a.p,{children:"Use this adapter when:"}),"\n",(0,t.jsxs)(a.ul,{children:["\n",(0,t.jsx)(a.li,{children:"You want to have really good performance"}),"\n",(0,t.jsx)(a.li,{children:"You do not want persistent state, for example in your test suite"}),"\n"]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"import {\n createRxDatabase\n} from 'rxdb'\nimport {\n getRxStoragePouch\n} from 'rxdb/plugins/pouchdb';\n// npm install pouchdb-adapter-memory --save\naddPouchPlugin(require('pouchdb-adapter-memory'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('memory')\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"memdown",children:"Memdown"}),"\n",(0,t.jsxs)(a.p,{children:["With RxDB you can also use adapters that implement ",(0,t.jsx)(a.a,{href:"https://github.com/Level/abstract-leveldown",children:"abstract-leveldown"})," like the memdown-adapter."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install memdown --save\n// npm install pouchdb-adapter-leveldb --save\naddPouchPlugin(require('pouchdb-adapter-leveldb')); // leveldown adapters need the leveldb plugin to work\n\nconst memdown = require('memdown');\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch(memdown) // the full leveldown-module\n});\n"})}),"\n",(0,t.jsx)(a.h1,{id:"browser",children:"Browser"}),"\n",(0,t.jsx)(a.h2,{id:"indexeddb",children:"IndexedDB"}),"\n",(0,t.jsxs)(a.p,{children:["The IndexedDB adapter stores the data inside of ",(0,t.jsx)(a.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API",children:"IndexedDB"})," use this in browsers environments as default."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-idb --save\naddPouchPlugin(require('pouchdb-adapter-idb'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('idb')\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"indexeddb-1",children:"IndexedDB"}),"\n",(0,t.jsxs)(a.p,{children:["A reimplementation of the indexeddb adapter which uses native secondary indexes. Should have a much better performance but can behave ",(0,t.jsx)(a.a,{href:"https://github.com/pouchdb/pouchdb/tree/master/packages/node_modules/pouchdb-adapter-indexeddb#differences-between-couchdb-and-pouchdbs-find-implementations-under-indexeddb",children:"different on some edge cases"}),"."]}),"\n",(0,t.jsxs)(a.p,{children:[(0,t.jsx)(a.strong,{children:"Notice"}),": Multiple users have reported problems with this adapter. It is ",(0,t.jsx)(a.strong,{children:"not"})," recommended to use this adapter."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-indexeddb --save\naddPouchPlugin(require('pouchdb-adapter-indexeddb'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('indexeddb')\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"websql",children:"Websql"}),"\n",(0,t.jsxs)(a.p,{children:["This adapter stores the data inside of websql. It has a different performance behavior. ",(0,t.jsx)(a.a,{href:"https://softwareengineering.stackexchange.com/questions/220254/why-is-web-sql-database-deprecated",children:"Websql is deprecated"}),". You should not use the websql adapter unless you have a really good reason."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-websql --save\naddPouchPlugin(require('pouchdb-adapter-websql'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('websql')\n});\n"})}),"\n",(0,t.jsx)(a.h1,{id:"nodejs",children:"NodeJS"}),"\n",(0,t.jsx)(a.h2,{id:"leveldown",children:"leveldown"}),"\n",(0,t.jsxs)(a.p,{children:["This adapter uses a ",(0,t.jsx)(a.a,{href:"https://github.com/Level/leveldown",children:"LevelDB C++ binding"})," to store that data on the filesystem. It has the best performance compared to other filesystem adapters. This adapter can ",(0,t.jsx)(a.strong,{children:"not"})," be used when multiple nodejs-processes access the same filesystem folders for storage."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install leveldown --save\n// npm install pouchdb-adapter-leveldb --save\naddPouchPlugin(require('pouchdb-adapter-leveldb')); // leveldown adapters need the leveldb plugin to work\nconst leveldown = require('leveldown');\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch(leveldown) // the full leveldown-module\n});\n\n// or use a specific folder to store the data\nconst database = await createRxDatabase({\n name: '/root/user/project/mydatabase',\n storage: getRxStoragePouch(leveldown) // the full leveldown-module\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"node-websql",children:"Node-Websql"}),"\n",(0,t.jsxs)(a.p,{children:["This adapter uses the ",(0,t.jsx)(a.a,{href:"https://github.com/nolanlawson/node-websql",children:"node-websql"}),"-shim to store data on the filesystem. Its advantages are that it does not need a leveldb build and it can be used when multiple nodejs-processes use the same database-files."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-node-websql --save\naddPouchPlugin(require('pouchdb-adapter-node-websql'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('websql') // the name of your adapter\n});\n\n// or use a specific folder to store the data\nconst database = await createRxDatabase({\n name: '/root/user/project/mydatabase',\n storage: getRxStoragePouch('websql') // the name of your adapter\n});\n"})}),"\n",(0,t.jsx)(a.h1,{id:"react-native",children:"React-Native"}),"\n",(0,t.jsx)(a.h2,{id:"react-native-sqlite",children:"react-native-sqlite"}),"\n",(0,t.jsxs)(a.p,{children:["Uses ReactNative SQLite as storage. Claims to be much faster than the asyncstorage adapter.\nTo use it, you have to do some steps from ",(0,t.jsx)(a.a,{href:"https://dev.to/craftzdog/hacking-pouchdb-to-use-on-react-native-1gjh",children:"this tutorial"}),"."]}),"\n",(0,t.jsxs)(a.p,{children:["First install ",(0,t.jsx)(a.code,{children:"pouchdb-adapter-react-native-sqlite"})," and ",(0,t.jsx)(a.code,{children:"react-native-sqlite-2"}),"."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-bash",children:"npm install pouchdb-adapter-react-native-sqlite react-native-sqlite-2\n"})}),"\n",(0,t.jsxs)(a.p,{children:["Then you have to ",(0,t.jsx)(a.a,{href:"https://facebook.github.io/react-native/docs/linking-libraries-ios",children:"link"})," the library."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-bash",children:"react-native link react-native-sqlite-2\n"})}),"\n",(0,t.jsx)(a.p,{children:"You also have to add some polyfills which are need but not included in react-native."}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-bash",children:"npm install base-64 events\n"})}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"import { decode, encode } from 'base-64'\n\nif (!global.btoa) {\n global.btoa = encode;\n}\n\nif (!global.atob) {\n global.atob = decode;\n}\n\n// Avoid using node dependent modules\nprocess.browser = true;\n"})}),"\n",(0,t.jsx)(a.p,{children:"Then you can use it inside of your code."}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"import { createRxDatabase } from 'rxdb';\nimport { addPouchPlugin, getRxStoragePouch } from 'rxdb/plugins/pouchdb';\nimport SQLite from 'react-native-sqlite-2'\nimport SQLiteAdapterFactory from 'pouchdb-adapter-react-native-sqlite'\n\nconst SQLiteAdapter = SQLiteAdapterFactory(SQLite)\n\naddPouchPlugin(SQLiteAdapter);\naddPouchPlugin(require('pouchdb-adapter-http'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('react-native-sqlite') // the name of your adapter\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"asyncstorage",children:"asyncstorage"}),"\n",(0,t.jsxs)(a.p,{children:["Uses react-native's ",(0,t.jsx)(a.a,{href:"https://facebook.github.io/react-native/docs/asyncstorage",children:"asyncstorage"}),"."]}),"\n",(0,t.jsxs)(a.p,{children:[(0,t.jsx)(a.strong,{children:"Notice"}),": There are ",(0,t.jsx)(a.a,{href:"https://github.com/pubkey/rxdb/issues/2286",children:"known problems"})," with this adapter and it is ",(0,t.jsx)(a.strong,{children:"not"})," recommended to use it."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-asyncstorage --save\naddPouchPlugin(require('pouchdb-adapter-asyncstorage'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('node-asyncstorage') // the name of your adapter\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"asyncstorage-down",children:"asyncstorage-down"}),"\n",(0,t.jsx)(a.p,{children:"A leveldown adapter that stores on asyncstorage."}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-asyncstorage-down --save\naddPouchPlugin(require('pouchdb-adapter-leveldb')); // leveldown adapters need the leveldb plugin to work\n\nconst asyncstorageDown = require('asyncstorage-down');\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch(asyncstorageDown) // the full leveldown-module\n});\n"})}),"\n",(0,t.jsx)(a.h1,{id:"cordova--phonegap--capacitor",children:"Cordova / Phonegap / Capacitor"}),"\n",(0,t.jsx)(a.h2,{id:"cordova-sqlite",children:"cordova-sqlite"}),"\n",(0,t.jsxs)(a.p,{children:["Uses cordova's global ",(0,t.jsx)(a.code,{children:"cordova.sqlitePlugin"}),". It can be used with cordova and capacitor."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-cordova-sqlite --save\naddPouchPlugin(require('pouchdb-adapter-cordova-sqlite'));\n\n/**\n * In capacitor/cordova you have to wait until all plugins are loaded and 'window.sqlitePlugin'\n * can be accessed.\n * This function waits until document deviceready is called which ensures that everything is loaded.\n * @link https://cordova.apache.org/docs/de/latest/cordova/events/events.deviceready.html\n */\nexport function awaitCapacitorDeviceReady(): Promise {\n return new Promise(res => {\n document.addEventListener('deviceready', () => {\n res();\n });\n });\n}\n\nasync function getDatabase(){\n\n // first wait until the deviceready event is fired\n await awaitCapacitorDeviceReady();\n\n const database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch(\n 'cordova-sqlite',\n // pouch settings are passed as second parameter\n {\n // for ios devices, the cordova-sqlite adapter needs to know where to save the data.\n iosDatabaseLocation: 'Library'\n }\n )\n });\n}\n"})})]})}function h(e={}){const{wrapper:a}={...(0,s.R)(),...e.components};return a?(0,t.jsx)(a,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},8453:(e,a,n)=>{n.d(a,{R:()=>r,x:()=>o});var t=n(6540);const s={},d=t.createContext(s);function r(e){const a=t.useContext(d);return t.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function o(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),t.createElement(d.Provider,{value:a},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/6bfb0089.34cd7c55.js b/docs/assets/js/6bfb0089.34cd7c55.js new file mode 100644 index 00000000000..8193ff811da --- /dev/null +++ b/docs/assets/js/6bfb0089.34cd7c55.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6422],{300:(e,a,n)=>{n.r(a),n.d(a,{assets:()=>i,contentTitle:()=>o,default:()=>h,frontMatter:()=>d,metadata:()=>r,toc:()=>l});var t=n(4848),s=n(8453);const d={title:"PouchDB Adapters",slug:"adapters.html"},o="PouchDB Adapters",r={id:"adapters",title:"PouchDB Adapters",description:"When you use PouchDB RxStorage, there are many adapters that define where the data has to be stored.",source:"@site/docs/adapters.md",sourceDirName:".",slug:"/adapters.html",permalink:"/adapters.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"PouchDB Adapters",slug:"adapters.html"}},i={},l=[{value:"Memory",id:"memory",level:2},{value:"Memdown",id:"memdown",level:2},{value:"IndexedDB",id:"indexeddb",level:2},{value:"IndexedDB",id:"indexeddb-1",level:2},{value:"Websql",id:"websql",level:2},{value:"leveldown",id:"leveldown",level:2},{value:"Node-Websql",id:"node-websql",level:2},{value:"react-native-sqlite",id:"react-native-sqlite",level:2},{value:"asyncstorage",id:"asyncstorage",level:2},{value:"asyncstorage-down",id:"asyncstorage-down",level:2},{value:"cordova-sqlite",id:"cordova-sqlite",level:2}];function c(e){const a={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",hr:"hr",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(a.h1,{id:"pouchdb-adapters",children:"PouchDB Adapters"}),"\n",(0,t.jsxs)(a.p,{children:["When you use PouchDB ",(0,t.jsx)(a.code,{children:"RxStorage"}),", there are many adapters that define where the data has to be stored.\nDepending on which environment you work in, you can choose between different adapters. For example, in the browser you want to store the data inside of IndexedDB but on NodeJS you want to store the data on the filesystem."]}),"\n",(0,t.jsx)(a.p,{children:"This page is an overview over the different adapters with recommendations on what to use where."}),"\n",(0,t.jsx)(a.hr,{}),"\n",(0,t.jsx)(a.admonition,{type:"warning",children:(0,t.jsxs)(a.p,{children:["The PouchDB RxStorage ",(0,t.jsx)(a.a,{href:"https://rxdb.info/questions-answers.html#why-is-the-pouchdb-rxstorage-deprecated",children:"is removed from RxDB"})," and can no longer be used in new projects. You should switch to a different ",(0,t.jsx)(a.a,{href:"/rx-storage.html",children:"RxStorage"}),"."]})}),"\n",(0,t.jsx)(a.hr,{}),"\n",(0,t.jsxs)(a.p,{children:["Please always ensure that your pouchdb adapter-version is the same as ",(0,t.jsx)(a.code,{children:"pouchdb-core"})," in the ",(0,t.jsx)(a.a,{href:"https://github.com/pubkey/rxdb/blob/master/package.json",children:"rxdb package.json"}),". Otherwise, you might have strange problems."]}),"\n",(0,t.jsx)(a.h1,{id:"any-environment",children:"Any environment"}),"\n",(0,t.jsx)(a.h2,{id:"memory",children:"Memory"}),"\n",(0,t.jsx)(a.p,{children:"In any environment, you can use the memory-adapter. It stores the data in the javascript runtime memory. This means it is not persistent and the data is lost when the process terminates."}),"\n",(0,t.jsx)(a.p,{children:"Use this adapter when:"}),"\n",(0,t.jsxs)(a.ul,{children:["\n",(0,t.jsx)(a.li,{children:"You want to have really good performance"}),"\n",(0,t.jsx)(a.li,{children:"You do not want persistent state, for example in your test suite"}),"\n"]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"import {\n createRxDatabase\n} from 'rxdb'\nimport {\n getRxStoragePouch\n} from 'rxdb/plugins/pouchdb';\n// npm install pouchdb-adapter-memory --save\naddPouchPlugin(require('pouchdb-adapter-memory'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('memory')\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"memdown",children:"Memdown"}),"\n",(0,t.jsxs)(a.p,{children:["With RxDB you can also use adapters that implement ",(0,t.jsx)(a.a,{href:"https://github.com/Level/abstract-leveldown",children:"abstract-leveldown"})," like the memdown-adapter."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install memdown --save\n// npm install pouchdb-adapter-leveldb --save\naddPouchPlugin(require('pouchdb-adapter-leveldb')); // leveldown adapters need the leveldb plugin to work\n\nconst memdown = require('memdown');\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch(memdown) // the full leveldown-module\n});\n"})}),"\n",(0,t.jsx)(a.h1,{id:"browser",children:"Browser"}),"\n",(0,t.jsx)(a.h2,{id:"indexeddb",children:"IndexedDB"}),"\n",(0,t.jsxs)(a.p,{children:["The IndexedDB adapter stores the data inside of ",(0,t.jsx)(a.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API",children:"IndexedDB"})," use this in browsers environments as default."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-idb --save\naddPouchPlugin(require('pouchdb-adapter-idb'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('idb')\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"indexeddb-1",children:"IndexedDB"}),"\n",(0,t.jsxs)(a.p,{children:["A reimplementation of the indexeddb adapter which uses native secondary indexes. Should have a much better performance but can behave ",(0,t.jsx)(a.a,{href:"https://github.com/pouchdb/pouchdb/tree/master/packages/node_modules/pouchdb-adapter-indexeddb#differences-between-couchdb-and-pouchdbs-find-implementations-under-indexeddb",children:"different on some edge cases"}),"."]}),"\n",(0,t.jsx)(a.admonition,{type:"note",children:(0,t.jsxs)(a.p,{children:["Multiple users have reported problems with this adapter. It is ",(0,t.jsx)(a.strong,{children:"not"})," recommended to use this adapter."]})}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-indexeddb --save\naddPouchPlugin(require('pouchdb-adapter-indexeddb'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('indexeddb')\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"websql",children:"Websql"}),"\n",(0,t.jsxs)(a.p,{children:["This adapter stores the data inside of websql. It has a different performance behavior. ",(0,t.jsx)(a.a,{href:"https://softwareengineering.stackexchange.com/questions/220254/why-is-web-sql-database-deprecated",children:"Websql is deprecated"}),". You should not use the websql adapter unless you have a really good reason."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-websql --save\naddPouchPlugin(require('pouchdb-adapter-websql'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('websql')\n});\n"})}),"\n",(0,t.jsx)(a.h1,{id:"nodejs",children:"NodeJS"}),"\n",(0,t.jsx)(a.h2,{id:"leveldown",children:"leveldown"}),"\n",(0,t.jsxs)(a.p,{children:["This adapter uses a ",(0,t.jsx)(a.a,{href:"https://github.com/Level/leveldown",children:"LevelDB C++ binding"})," to store that data on the filesystem. It has the best performance compared to other filesystem adapters. This adapter can ",(0,t.jsx)(a.strong,{children:"not"})," be used when multiple nodejs-processes access the same filesystem folders for storage."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install leveldown --save\n// npm install pouchdb-adapter-leveldb --save\naddPouchPlugin(require('pouchdb-adapter-leveldb')); // leveldown adapters need the leveldb plugin to work\nconst leveldown = require('leveldown');\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch(leveldown) // the full leveldown-module\n});\n\n// or use a specific folder to store the data\nconst database = await createRxDatabase({\n name: '/root/user/project/mydatabase',\n storage: getRxStoragePouch(leveldown) // the full leveldown-module\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"node-websql",children:"Node-Websql"}),"\n",(0,t.jsxs)(a.p,{children:["This adapter uses the ",(0,t.jsx)(a.a,{href:"https://github.com/nolanlawson/node-websql",children:"node-websql"}),"-shim to store data on the filesystem. Its advantages are that it does not need a leveldb build and it can be used when multiple nodejs-processes use the same database-files."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-node-websql --save\naddPouchPlugin(require('pouchdb-adapter-node-websql'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('websql') // the name of your adapter\n});\n\n// or use a specific folder to store the data\nconst database = await createRxDatabase({\n name: '/root/user/project/mydatabase',\n storage: getRxStoragePouch('websql') // the name of your adapter\n});\n"})}),"\n",(0,t.jsx)(a.h1,{id:"react-native",children:"React-Native"}),"\n",(0,t.jsx)(a.h2,{id:"react-native-sqlite",children:"react-native-sqlite"}),"\n",(0,t.jsxs)(a.p,{children:["Uses ReactNative SQLite as storage. Claims to be much faster than the asyncstorage adapter.\nTo use it, you have to do some steps from ",(0,t.jsx)(a.a,{href:"https://dev.to/craftzdog/hacking-pouchdb-to-use-on-react-native-1gjh",children:"this tutorial"}),"."]}),"\n",(0,t.jsxs)(a.p,{children:["First install ",(0,t.jsx)(a.code,{children:"pouchdb-adapter-react-native-sqlite"})," and ",(0,t.jsx)(a.code,{children:"react-native-sqlite-2"}),"."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-bash",children:"npm install pouchdb-adapter-react-native-sqlite react-native-sqlite-2\n"})}),"\n",(0,t.jsxs)(a.p,{children:["Then you have to ",(0,t.jsx)(a.a,{href:"https://facebook.github.io/react-native/docs/linking-libraries-ios",children:"link"})," the library."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-bash",children:"react-native link react-native-sqlite-2\n"})}),"\n",(0,t.jsx)(a.p,{children:"You also have to add some polyfills which are need but not included in react-native."}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-bash",children:"npm install base-64 events\n"})}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"import { decode, encode } from 'base-64'\n\nif (!global.btoa) {\n global.btoa = encode;\n}\n\nif (!global.atob) {\n global.atob = decode;\n}\n\n// Avoid using node dependent modules\nprocess.browser = true;\n"})}),"\n",(0,t.jsx)(a.p,{children:"Then you can use it inside of your code."}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"import { createRxDatabase } from 'rxdb';\nimport { addPouchPlugin, getRxStoragePouch } from 'rxdb/plugins/pouchdb';\nimport SQLite from 'react-native-sqlite-2'\nimport SQLiteAdapterFactory from 'pouchdb-adapter-react-native-sqlite'\n\nconst SQLiteAdapter = SQLiteAdapterFactory(SQLite)\n\naddPouchPlugin(SQLiteAdapter);\naddPouchPlugin(require('pouchdb-adapter-http'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('react-native-sqlite') // the name of your adapter\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"asyncstorage",children:"asyncstorage"}),"\n",(0,t.jsxs)(a.p,{children:["Uses react-native's ",(0,t.jsx)(a.a,{href:"https://facebook.github.io/react-native/docs/asyncstorage",children:"asyncstorage"}),"."]}),"\n",(0,t.jsx)(a.admonition,{type:"note",children:(0,t.jsxs)(a.p,{children:["There are ",(0,t.jsx)(a.a,{href:"https://github.com/pubkey/rxdb/issues/2286",children:"known problems"})," with this adapter and it is ",(0,t.jsx)(a.strong,{children:"not"})," recommended to use it."]})}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-asyncstorage --save\naddPouchPlugin(require('pouchdb-adapter-asyncstorage'));\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('node-asyncstorage') // the name of your adapter\n});\n"})}),"\n",(0,t.jsx)(a.h2,{id:"asyncstorage-down",children:"asyncstorage-down"}),"\n",(0,t.jsx)(a.p,{children:"A leveldown adapter that stores on asyncstorage."}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-asyncstorage-down --save\naddPouchPlugin(require('pouchdb-adapter-leveldb')); // leveldown adapters need the leveldb plugin to work\n\nconst asyncstorageDown = require('asyncstorage-down');\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch(asyncstorageDown) // the full leveldown-module\n});\n"})}),"\n",(0,t.jsx)(a.h1,{id:"cordova--phonegap--capacitor",children:"Cordova / Phonegap / Capacitor"}),"\n",(0,t.jsx)(a.h2,{id:"cordova-sqlite",children:"cordova-sqlite"}),"\n",(0,t.jsxs)(a.p,{children:["Uses cordova's global ",(0,t.jsx)(a.code,{children:"cordova.sqlitePlugin"}),". It can be used with cordova and capacitor."]}),"\n",(0,t.jsx)(a.pre,{children:(0,t.jsx)(a.code,{className:"language-js",children:"// npm install pouchdb-adapter-cordova-sqlite --save\naddPouchPlugin(require('pouchdb-adapter-cordova-sqlite'));\n\n/**\n * In capacitor/cordova you have to wait until all plugins are loaded and 'window.sqlitePlugin'\n * can be accessed.\n * This function waits until document deviceready is called which ensures that everything is loaded.\n * @link https://cordova.apache.org/docs/de/latest/cordova/events/events.deviceready.html\n */\nexport function awaitCapacitorDeviceReady(): Promise {\n return new Promise(res => {\n document.addEventListener('deviceready', () => {\n res();\n });\n });\n}\n\nasync function getDatabase(){\n\n // first wait until the deviceready event is fired\n await awaitCapacitorDeviceReady();\n\n const database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch(\n 'cordova-sqlite',\n // pouch settings are passed as second parameter\n {\n // for ios devices, the cordova-sqlite adapter needs to know where to save the data.\n iosDatabaseLocation: 'Library'\n }\n )\n });\n}\n"})})]})}function h(e={}){const{wrapper:a}={...(0,s.R)(),...e.components};return a?(0,t.jsx)(a,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},8453:(e,a,n)=>{n.d(a,{R:()=>o,x:()=>r});var t=n(6540);const s={},d=t.createContext(s);function o(e){const a=t.useContext(d);return t.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function r(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),t.createElement(d.Provider,{value:a},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/6cbff7c2.648148ea.js b/docs/assets/js/6cbff7c2.648148ea.js deleted file mode 100644 index c5c9135aa9f..00000000000 --- a/docs/assets/js/6cbff7c2.648148ea.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[7408],{5943:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>n,metadata:()=>o,toc:()=>c});var r=s(4848),i=s(8453);const n={title:"OPFS RxStorage \ud83d\udc51",slug:"rx-storage-opfs.html",description:"Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage"},a="Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage",o={id:"rx-storage-opfs",title:"OPFS RxStorage \ud83d\udc51",description:"Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage",source:"@site/docs/rx-storage-opfs.md",sourceDirName:".",slug:"/rx-storage-opfs.html",permalink:"/rx-storage-opfs.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"OPFS RxStorage \ud83d\udc51",slug:"rx-storage-opfs.html",description:"Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage"},sidebar:"tutorialSidebar",previous:{title:"Dexie.js RxStorage",permalink:"/rx-storage-dexie.html"},next:{title:"Node.js Filesystem RxStorage \ud83d\udc51",permalink:"/rx-storage-filesystem-node.html"}},l={},c=[{value:"What is OPFS",id:"what-is-opfs",level:2},{value:"OPFS limitations",id:"opfs-limitations",level:3},{value:"How the OPFS API works",id:"how-the-opfs-api-works",level:2},{value:"OPFS performance",id:"opfs-performance",level:2},{value:"Using OPFS as RxStorage in RxDB",id:"using-opfs-as-rxstorage-in-rxdb",level:2},{value:"Using OPFS in the main thread instead of a worker",id:"using-opfs-in-the-main-thread-instead-of-a-worker",level:2},{value:"Building a custom worker.js",id:"building-a-custom-workerjs",level:2},{value:"Setting usesRxDatabaseInWorker when a RxDatabase is also used inside of the worker",id:"setting-usesrxdatabaseinworker-when-a-rxdatabase-is-also-used-inside-of-the-worker",level:2},{value:"Setting jsonPositionSize to increase the maximum database size.",id:"setting-jsonpositionsize-to-increase-the-maximum-database-size",level:2},{value:"OPFS in Electron, React-Native or Capacitor.js",id:"opfs-in-electron-react-native-or-capacitorjs",level:2},{value:"Difference between File System Access API and Origin Private File System (OPFS)",id:"difference-between-file-system-access-api-and-origin-private-file-system-opfs",level:2},{value:"Learn more about OPFS:",id:"learn-more-about-opfs",level:2}];function d(e){const t={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"origin-private-file-system-opfs-database-with-the-rxdb-opfs-rxstorage",children:"Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage"}),"\n",(0,r.jsxs)(t.p,{children:["With the ",(0,r.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," OPFS storage you can build a fully featured database on top of the ",(0,r.jsx)(t.a,{href:"https://web.dev/opfs",children:"Origin Private File System"})," (OPFS) browser API. Compared to other storage solutions, it has a way better performance."]}),"\n",(0,r.jsx)(t.h2,{id:"what-is-opfs",children:"What is OPFS"}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.strong,{children:"Origin Private File System (OPFS)"})," is a native browser storage API that allows web applications to manage files in a private, sandboxed, ",(0,r.jsx)(t.strong,{children:"origin-specific virtual filesystem"}),". Unlike ",(0,r.jsx)(t.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB"})," and ",(0,r.jsx)(t.a,{href:"/articles/localstorage.html",children:"LocalStorage"}),", which are optimized as object/key-value storage, OPFS provides more granular control for file operations, enabling byte-by-byte access, file streaming, and even low-level manipulations.\nOPFS is ideal for applications requiring ",(0,r.jsx)(t.strong,{children:"high-performance"})," file operations (",(0,r.jsx)(t.strong,{children:"3x-4x faster compared to IndexedDB"}),") inside of a client-side application, offering advantages like improved speed, more efficient use of resources, and enhanced security and privacy features."]}),"\n",(0,r.jsx)(t.h3,{id:"opfs-limitations",children:"OPFS limitations"}),"\n",(0,r.jsxs)(t.p,{children:["From the beginning of 2023, the Origin Private File System API is supported by ",(0,r.jsx)(t.a,{href:"https://caniuse.com/native-filesystem-api",children:"all modern browsers"})," like Safari, Chrome, Edge and Firefox. Only Internet Explorer is not supported and likely will never get support."]}),"\n",(0,r.jsxs)(t.p,{children:["It is important to know that the most performant synchronous methods like ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/read",children:(0,r.jsx)(t.code,{children:"read()"})})," and ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/write",children:(0,r.jsx)(t.code,{children:"write()"})})," of the OPFS API are ",(0,r.jsxs)(t.strong,{children:["only available inside of a ",(0,r.jsx)(t.a,{href:"/rx-storage-worker.html",children:"WebWorker"})]}),".\nThey cannot be used in the main thread, an iFrame or even a ",(0,r.jsx)(t.a,{href:"/rx-storage-shared-worker.html",children:"SharedWorker"}),".\nThe OPFS ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle",children:(0,r.jsx)(t.code,{children:"createSyncAccessHandle()"})})," method that gives you access to the synchronous methods is not exposed in the main thread, only in a Worker."]}),"\n",(0,r.jsxs)(t.p,{children:["While there is no concrete ",(0,r.jsx)(t.strong,{children:"data size limit"})," defined by the API, browsers will refuse to store more data at some point.\nIf no more data can be written, a ",(0,r.jsx)(t.code,{children:"QuotaExceededError"})," is thrown which should be handled by the application, like showing an error message to the user."]}),"\n",(0,r.jsx)(t.h2,{id:"how-the-opfs-api-works",children:"How the OPFS API works"}),"\n",(0,r.jsxs)(t.p,{children:["The OPFS API is pretty straightforward to use. First you get the root filesystem. Then you can create files and directories on that. Notice that whenever you ",(0,r.jsx)(t.em,{children:"synchronously"})," write to, or read from a file, an ",(0,r.jsx)(t.code,{children:"ArrayBuffer"})," must be used that contains the data. It is not possible to synchronously write plain strings or objects into the file. Therefore the ",(0,r.jsx)(t.code,{children:"TextEncoder"})," and ",(0,r.jsx)(t.code,{children:"TextDecoder"})," API must be used."]}),"\n",(0,r.jsxs)(t.p,{children:["Also notice that some of the methods of ",(0,r.jsx)(t.code,{children:"FileSystemSyncAccessHandle"})," ",(0,r.jsx)(t.a,{href:"https://developer.chrome.com/blog/sync-methods-for-accesshandles",children:"have been asynchronous"})," in the past, but are synchronous since Chromium 108. To make it less confusing, we just use ",(0,r.jsx)(t.code,{children:"await"})," in front of them, so it will work in both cases."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"// Access the root directory of the origin's private file system.\nconst root = await navigator.storage.getDirectory();\n\n// Create a subdirectory.\nconst diaryDirectory = await root.getDirectoryHandle('subfolder', {\n create: true,\n}); \n\n// Create a new file named 'example.txt'.\nconst fileHandle = await diaryDirectory.getFileHandle('example.txt', {\n create: true,\n});\n\n// Create a FileSystemSyncAccessHandle on the file.\nconst accessHandle = await fileHandle.createSyncAccessHandle();\n\n// Write a sentence to the file.\nlet writeBuffer = new TextEncoder().encode('Hello from RxDB');\nconst writeSize = accessHandle.write(writeBuffer);\n\n// Read file and transform data to string.\nconst readBuffer = new Uint8Array(writeSize);\nconst readSize = accessHandle.read(readBuffer, { at: 0 }); \nconst contentAsString = new TextDecoder().decode(readBuffer);\n\n// Write an exclamation mark to the end of the file.\nwriteBuffer = new TextEncoder().encode('!');\naccessHandle.write(writeBuffer, { at: readSize });\n\n// Truncate file to 10 bytes.\nawait accessHandle.truncate(10);\n\n// Get the new size of the file.\nconst fileSize = await accessHandle.getSize();\n\n// Persist changes to disk.\nawait accessHandle.flush();\n\n// Always close FileSystemSyncAccessHandle if done, so others can open the file again.\nawait accessHandle.close();\n"})}),"\n",(0,r.jsxs)(t.p,{children:["A more detailed description of the OPFS API can be found ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system",children:"on MDN"}),"."]}),"\n",(0,r.jsx)(t.h2,{id:"opfs-performance",children:"OPFS performance"}),"\n",(0,r.jsxs)(t.p,{children:["Because the Origin Private File System API provides low-level access to binary files, it is much faster compared to ",(0,r.jsx)(t.a,{href:"/slow-indexeddb.html",children:"IndexedDB"})," or ",(0,r.jsx)(t.a,{href:"/articles/localstorage.html",children:"localStorage"}),". According to the ",(0,r.jsx)(t.a,{href:"https://pubkey.github.io/client-side-databases/database-comparison/index.html",children:"storage performance test"}),", OPFS is up to 2x times faster on plain inserts when a new file is created on each write. Reads are even faster."]}),"\n",(0,r.jsxs)(t.p,{children:["A good comparison about real world scenarios, are the ",(0,r.jsx)(t.a,{href:"/rx-storage-performance.html",children:"performance results"})," of the various RxDB storages. Here it shows that reads are up to 4x faster compared to IndexedDB, even with complex queries:"]}),"\n",(0,r.jsx)("p",{align:"center",children:(0,r.jsx)("img",{src:"./files/rx-storage-performance-browser.png",alt:"RxStorage performance - browser",width:"700"})}),"\n",(0,r.jsx)(t.h2,{id:"using-opfs-as-rxstorage-in-rxdb",children:"Using OPFS as RxStorage in RxDB"}),"\n",(0,r.jsxs)(t.p,{children:["The OPFS ",(0,r.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"})," itself must run inside a WebWorker. Therefore we use the ",(0,r.jsx)(t.a,{href:"/rx-storage-worker.html",children:"Worker RxStorage"})," and let it point to the prebuild ",(0,r.jsx)(t.code,{children:"opfs.worker.js"})," file that comes shipped with RxDB Premium \ud83d\udc51."]}),"\n",(0,r.jsxs)(t.p,{children:["Notice that the OPFS RxStorage is part of the ",(0,r.jsx)(t.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"})," plugin that must be purchased."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageWorker } from 'rxdb-premium/plugins/storage-worker';\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageWorker(\n {\n /**\n * This file must be statically served from a webserver.\n * You might want to first copy it somewhere outside of\n * your node_modules folder.\n */\n workerInput: 'node_modules/rxdb-premium/dist/workers/opfs.worker.js'\n }\n )\n});\n"})}),"\n",(0,r.jsx)(t.h2,{id:"using-opfs-in-the-main-thread-instead-of-a-worker",children:"Using OPFS in the main thread instead of a worker"}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"createSyncAccessHandle"})," method from the Filesystem API is only available inside of a Webworker. Therefore you cannot use ",(0,r.jsx)(t.code,{children:"getRxStorageOPFS()"})," in the main thread. But there is a slightly slower way to access the virtual filesystem from the main thread. RxDB support the ",(0,r.jsx)(t.code,{children:"getRxStorageOPFSMainThread()"})," for that. Notice that this uses the ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createWritable",children:"createWritable"})," function which is not supported in safari."]}),"\n",(0,r.jsx)(t.p,{children:"Using OPFS from the main thread can have benefits because not having to cross the worker bridge can reduce latence in reads and writes."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"import { createRxDatabase } from 'rxdb';\nimport { getRxStorageOPFSMainThread } from 'rxdb-premium/plugins/storage-worker';\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageOPFSMainThread()\n});\n"})}),"\n",(0,r.jsxs)(t.h2,{id:"building-a-custom-workerjs",children:["Building a custom ",(0,r.jsx)(t.code,{children:"worker.js"})]}),"\n",(0,r.jsxs)(t.p,{children:["When you want to run additional plugins like storage wrappers or replication ",(0,r.jsx)(t.strong,{children:"inside"})," of the worker, you have to build your own ",(0,r.jsx)(t.code,{children:"worker.js"})," file. You can do that similar to other workers by calling ",(0,r.jsx)(t.code,{children:"exposeWorkerRxStorage"})," like described in the ",(0,r.jsx)(t.a,{href:"/rx-storage-worker.html",children:"worker storage plugin"}),"."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"// inside of the worker.js file\nimport { getRxStorageOPFS } from 'rxdb-premium/plugins/storage-opfs';\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\n\nconst storage = getRxStorageOPFS();\nexposeWorkerRxStorage({\n storage\n});\n"})}),"\n",(0,r.jsxs)(t.h2,{id:"setting-usesrxdatabaseinworker-when-a-rxdatabase-is-also-used-inside-of-the-worker",children:["Setting ",(0,r.jsx)(t.code,{children:"usesRxDatabaseInWorker"})," when a RxDatabase is also used inside of the worker"]}),"\n",(0,r.jsxs)(t.p,{children:["When you use the OPFS inside of a worker, it will internally use strings to represent operation results. This has the benefit that transferring strings from the worker to the main thread, is way faster compared to complex json objects. The ",(0,r.jsx)(t.code,{children:"getRxStorageWorker()"})," will automatically decode these strings on the main thread so that the data can be used by the RxDatabase."]}),"\n",(0,r.jsxs)(t.p,{children:["But using a RxDatabase ",(0,r.jsx)(t.strong,{children:"inside"})," of your worker can make sense for example when you want to move the ",(0,r.jsx)(t.a,{href:"/replication.html",children:"replication"})," with a server. To enable this, you have to set ",(0,r.jsx)(t.code,{children:"usesRxDatabaseInWorker"})," to ",(0,r.jsx)(t.code,{children:"true"}),":"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"// inside of the worker.js file\nimport { getRxStorageOPFS } from 'rxdb-premium/plugins/storage-opfs';\nconst storage = getRxStorageOPFS({\n usesRxDatabaseInWorker: true\n});\n"})}),"\n",(0,r.jsxs)(t.h2,{id:"setting-jsonpositionsize-to-increase-the-maximum-database-size",children:["Setting ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," to increase the maximum database size."]}),"\n",(0,r.jsxs)(t.p,{children:["By default the ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," value is set to ",(0,r.jsx)(t.code,{children:"8"})," which allows the database to get up to 100 megabytes in size (per collection).\nThis is ok for most use cases but you might want to just increase ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," to ",(0,r.jsx)(t.code,{children:"14"}),".\nIn the next major RxDB version the default will be set to ",(0,r.jsx)(t.code,{children:"14"}),", but this was not possible without introducing a breaking change."]}),"\n",(0,r.jsxs)(t.p,{children:["NOTICE: If you have already stored data, you cannot just change the ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," value because your stored binary data will not be compatible anymore."]}),"\n",(0,r.jsxs)(t.p,{children:["Also there is a ",(0,r.jsx)(t.code,{children:"opfs-big.worker.js"})," file that has ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," set to ",(0,r.jsx)(t.code,{children:"14"})," already."]}),"\n",(0,r.jsx)(t.h2,{id:"opfs-in-electron-react-native-or-capacitorjs",children:"OPFS in Electron, React-Native or Capacitor.js"}),"\n",(0,r.jsx)(t.p,{children:"Origin Private File System is a browser API that is only accessible in browsers. Other JavaScript like React-Native or Node.js, do not support it."}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Electron"})," has two JavaScript contexts: the browser (chromium) context and the Node.js context. While you could use the OPFS API in the browser context, it is not recommended. Instead you should use the Filesystem API of Node.js and then only transfer the relevant data with the ",(0,r.jsx)(t.a,{href:"https://www.electronjs.org/de/docs/latest/api/ipc-renderer",children:"ipcRenderer"}),". With RxDB that is pretty easy to configure:"]}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:["In the ",(0,r.jsx)(t.code,{children:"main.js"}),", expose the ",(0,r.jsx)(t.a,{href:"/rx-storage-filesystem-node.html",children:"Node Filesystem"})," storage with the ",(0,r.jsx)(t.code,{children:"exposeIpcMainRxStorage()"})," that comes with the ",(0,r.jsx)(t.a,{href:"/electron.html",children:"electron plugin"})]}),"\n",(0,r.jsxs)(t.li,{children:["In the browser context, access the main storage with the ",(0,r.jsx)(t.code,{children:"getRxStorageIpcRenderer()"})," method."]}),"\n"]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"React Native"})," (and Expo) does not have an OPFS API. You could use the ReactNative Filesystem to directly write data. But to get a fully featured database like RxDB it is easier to use the ",(0,r.jsx)(t.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"})," which starts an SQLite database inside of the ReactNative app and uses that to do the database operations."]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Capacitor.js"})," is able to access the OPFS API."]}),"\n",(0,r.jsxs)(t.h2,{id:"difference-between-file-system-access-api-and-origin-private-file-system-opfs",children:["Difference between ",(0,r.jsx)(t.code,{children:"File System Access API"})," and ",(0,r.jsx)(t.code,{children:"Origin Private File System (OPFS)"})]}),"\n",(0,r.jsxs)(t.p,{children:["Often developers are confused with the differences between the ",(0,r.jsx)(t.code,{children:"File System Access API"})," and the ",(0,r.jsx)(t.code,{children:"Origin Private File System (OPFS)"}),"."]}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:["The ",(0,r.jsx)(t.code,{children:"File System Access API"})," provides access to the files on the device file system, like the ones shown in the file explorer of the operating system. To use the File System API, the user has to actively select the files from a filepicker."]}),"\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.code,{children:"Origin Private File System (OPFS)"})," is a sub-part of the ",(0,r.jsx)(t.code,{children:"File System Standard"})," and it only describes the things you can do with the filesystem root from ",(0,r.jsx)(t.code,{children:"navigator.storage.getDirectory()"}),". OPFS writes to a ",(0,r.jsx)(t.strong,{children:"sandboxed"})," filesystem, not visible to the user. Therefore the user does not have to actively select or allow the data access."]}),"\n"]}),"\n",(0,r.jsx)(t.h2,{id:"learn-more-about-opfs",children:"Learn more about OPFS:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsx)(t.li,{children:(0,r.jsx)(t.a,{href:"https://webkit.org/blog/12257/the-file-system-access-api-with-origin-private-file-system/",children:"WebKit: The File System API with Origin Private File System"})}),"\n",(0,r.jsx)(t.li,{children:(0,r.jsx)(t.a,{href:"https://caniuse.com/native-filesystem-api",children:"Browser Support"})}),"\n",(0,r.jsx)(t.li,{children:(0,r.jsx)(t.a,{href:"https://pubkey.github.io/client-side-databases/database-comparison/index.html",children:"Performance Test Tool"})}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},8453:(e,t,s)=>{s.d(t,{R:()=>a,x:()=>o});var r=s(6540);const i={},n=r.createContext(i);function a(e){const t=r.useContext(n);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),r.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/6cbff7c2.993fe833.js b/docs/assets/js/6cbff7c2.993fe833.js new file mode 100644 index 00000000000..4135087b2f7 --- /dev/null +++ b/docs/assets/js/6cbff7c2.993fe833.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[7408],{5943:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>n,metadata:()=>o,toc:()=>d});var r=s(4848),i=s(8453);const n={title:"OPFS RxStorage \ud83d\udc51",slug:"rx-storage-opfs.html",description:"Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage"},a="Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage",o={id:"rx-storage-opfs",title:"OPFS RxStorage \ud83d\udc51",description:"Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage",source:"@site/docs/rx-storage-opfs.md",sourceDirName:".",slug:"/rx-storage-opfs.html",permalink:"/rx-storage-opfs.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"OPFS RxStorage \ud83d\udc51",slug:"rx-storage-opfs.html",description:"Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage"},sidebar:"tutorialSidebar",previous:{title:"Dexie.js RxStorage",permalink:"/rx-storage-dexie.html"},next:{title:"Node.js Filesystem RxStorage \ud83d\udc51",permalink:"/rx-storage-filesystem-node.html"}},l={},d=[{value:"What is OPFS",id:"what-is-opfs",level:2},{value:"OPFS limitations",id:"opfs-limitations",level:3},{value:"How the OPFS API works",id:"how-the-opfs-api-works",level:2},{value:"OPFS performance",id:"opfs-performance",level:2},{value:"Using OPFS as RxStorage in RxDB",id:"using-opfs-as-rxstorage-in-rxdb",level:2},{value:"Using OPFS in the main thread instead of a worker",id:"using-opfs-in-the-main-thread-instead-of-a-worker",level:2},{value:"Building a custom worker.js",id:"building-a-custom-workerjs",level:2},{value:"Setting usesRxDatabaseInWorker when a RxDatabase is also used inside of the worker",id:"setting-usesrxdatabaseinworker-when-a-rxdatabase-is-also-used-inside-of-the-worker",level:2},{value:"Setting jsonPositionSize to increase the maximum database size.",id:"setting-jsonpositionsize-to-increase-the-maximum-database-size",level:2},{value:"OPFS in Electron, React-Native or Capacitor.js",id:"opfs-in-electron-react-native-or-capacitorjs",level:2},{value:"Difference between File System Access API and Origin Private File System (OPFS)",id:"difference-between-file-system-access-api-and-origin-private-file-system-opfs",level:2},{value:"Learn more about OPFS:",id:"learn-more-about-opfs",level:2}];function c(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"origin-private-file-system-opfs-database-with-the-rxdb-opfs-rxstorage",children:"Origin Private File System (OPFS) Database with the RxDB OPFS-RxStorage"}),"\n",(0,r.jsxs)(t.p,{children:["With the ",(0,r.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," OPFS storage you can build a fully featured database on top of the ",(0,r.jsx)(t.a,{href:"https://web.dev/opfs",children:"Origin Private File System"})," (OPFS) browser API. Compared to other storage solutions, it has a way better performance."]}),"\n",(0,r.jsx)(t.h2,{id:"what-is-opfs",children:"What is OPFS"}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.strong,{children:"Origin Private File System (OPFS)"})," is a native browser storage API that allows web applications to manage files in a private, sandboxed, ",(0,r.jsx)(t.strong,{children:"origin-specific virtual filesystem"}),". Unlike ",(0,r.jsx)(t.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB"})," and ",(0,r.jsx)(t.a,{href:"/articles/localstorage.html",children:"LocalStorage"}),", which are optimized as object/key-value storage, OPFS provides more granular control for file operations, enabling byte-by-byte access, file streaming, and even low-level manipulations.\nOPFS is ideal for applications requiring ",(0,r.jsx)(t.strong,{children:"high-performance"})," file operations (",(0,r.jsx)(t.strong,{children:"3x-4x faster compared to IndexedDB"}),") inside of a client-side application, offering advantages like improved speed, more efficient use of resources, and enhanced security and privacy features."]}),"\n",(0,r.jsx)(t.h3,{id:"opfs-limitations",children:"OPFS limitations"}),"\n",(0,r.jsxs)(t.p,{children:["From the beginning of 2023, the Origin Private File System API is supported by ",(0,r.jsx)(t.a,{href:"https://caniuse.com/native-filesystem-api",children:"all modern browsers"})," like Safari, Chrome, Edge and Firefox. Only Internet Explorer is not supported and likely will never get support."]}),"\n",(0,r.jsxs)(t.p,{children:["It is important to know that the most performant synchronous methods like ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/read",children:(0,r.jsx)(t.code,{children:"read()"})})," and ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/write",children:(0,r.jsx)(t.code,{children:"write()"})})," of the OPFS API are ",(0,r.jsxs)(t.strong,{children:["only available inside of a ",(0,r.jsx)(t.a,{href:"/rx-storage-worker.html",children:"WebWorker"})]}),".\nThey cannot be used in the main thread, an iFrame or even a ",(0,r.jsx)(t.a,{href:"/rx-storage-shared-worker.html",children:"SharedWorker"}),".\nThe OPFS ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle",children:(0,r.jsx)(t.code,{children:"createSyncAccessHandle()"})})," method that gives you access to the synchronous methods is not exposed in the main thread, only in a Worker."]}),"\n",(0,r.jsxs)(t.p,{children:["While there is no concrete ",(0,r.jsx)(t.strong,{children:"data size limit"})," defined by the API, browsers will refuse to store more data at some point.\nIf no more data can be written, a ",(0,r.jsx)(t.code,{children:"QuotaExceededError"})," is thrown which should be handled by the application, like showing an error message to the user."]}),"\n",(0,r.jsx)(t.h2,{id:"how-the-opfs-api-works",children:"How the OPFS API works"}),"\n",(0,r.jsxs)(t.p,{children:["The OPFS API is pretty straightforward to use. First you get the root filesystem. Then you can create files and directories on that. Notice that whenever you ",(0,r.jsx)(t.em,{children:"synchronously"})," write to, or read from a file, an ",(0,r.jsx)(t.code,{children:"ArrayBuffer"})," must be used that contains the data. It is not possible to synchronously write plain strings or objects into the file. Therefore the ",(0,r.jsx)(t.code,{children:"TextEncoder"})," and ",(0,r.jsx)(t.code,{children:"TextDecoder"})," API must be used."]}),"\n",(0,r.jsxs)(t.p,{children:["Also notice that some of the methods of ",(0,r.jsx)(t.code,{children:"FileSystemSyncAccessHandle"})," ",(0,r.jsx)(t.a,{href:"https://developer.chrome.com/blog/sync-methods-for-accesshandles",children:"have been asynchronous"})," in the past, but are synchronous since Chromium 108. To make it less confusing, we just use ",(0,r.jsx)(t.code,{children:"await"})," in front of them, so it will work in both cases."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"// Access the root directory of the origin's private file system.\nconst root = await navigator.storage.getDirectory();\n\n// Create a subdirectory.\nconst diaryDirectory = await root.getDirectoryHandle('subfolder', {\n create: true,\n}); \n\n// Create a new file named 'example.txt'.\nconst fileHandle = await diaryDirectory.getFileHandle('example.txt', {\n create: true,\n});\n\n// Create a FileSystemSyncAccessHandle on the file.\nconst accessHandle = await fileHandle.createSyncAccessHandle();\n\n// Write a sentence to the file.\nlet writeBuffer = new TextEncoder().encode('Hello from RxDB');\nconst writeSize = accessHandle.write(writeBuffer);\n\n// Read file and transform data to string.\nconst readBuffer = new Uint8Array(writeSize);\nconst readSize = accessHandle.read(readBuffer, { at: 0 }); \nconst contentAsString = new TextDecoder().decode(readBuffer);\n\n// Write an exclamation mark to the end of the file.\nwriteBuffer = new TextEncoder().encode('!');\naccessHandle.write(writeBuffer, { at: readSize });\n\n// Truncate file to 10 bytes.\nawait accessHandle.truncate(10);\n\n// Get the new size of the file.\nconst fileSize = await accessHandle.getSize();\n\n// Persist changes to disk.\nawait accessHandle.flush();\n\n// Always close FileSystemSyncAccessHandle if done, so others can open the file again.\nawait accessHandle.close();\n"})}),"\n",(0,r.jsxs)(t.p,{children:["A more detailed description of the OPFS API can be found ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system",children:"on MDN"}),"."]}),"\n",(0,r.jsx)(t.h2,{id:"opfs-performance",children:"OPFS performance"}),"\n",(0,r.jsxs)(t.p,{children:["Because the Origin Private File System API provides low-level access to binary files, it is much faster compared to ",(0,r.jsx)(t.a,{href:"/slow-indexeddb.html",children:"IndexedDB"})," or ",(0,r.jsx)(t.a,{href:"/articles/localstorage.html",children:"localStorage"}),". According to the ",(0,r.jsx)(t.a,{href:"https://pubkey.github.io/client-side-databases/database-comparison/index.html",children:"storage performance test"}),", OPFS is up to 2x times faster on plain inserts when a new file is created on each write. Reads are even faster."]}),"\n",(0,r.jsxs)(t.p,{children:["A good comparison about real world scenarios, are the ",(0,r.jsx)(t.a,{href:"/rx-storage-performance.html",children:"performance results"})," of the various RxDB storages. Here it shows that reads are up to 4x faster compared to IndexedDB, even with complex queries:"]}),"\n",(0,r.jsx)("p",{align:"center",children:(0,r.jsx)("img",{src:"./files/rx-storage-performance-browser.png",alt:"RxStorage performance - browser",width:"700"})}),"\n",(0,r.jsx)(t.h2,{id:"using-opfs-as-rxstorage-in-rxdb",children:"Using OPFS as RxStorage in RxDB"}),"\n",(0,r.jsxs)(t.p,{children:["The OPFS ",(0,r.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"})," itself must run inside a WebWorker. Therefore we use the ",(0,r.jsx)(t.a,{href:"/rx-storage-worker.html",children:"Worker RxStorage"})," and let it point to the prebuild ",(0,r.jsx)(t.code,{children:"opfs.worker.js"})," file that comes shipped with RxDB Premium \ud83d\udc51."]}),"\n",(0,r.jsxs)(t.p,{children:["Notice that the OPFS RxStorage is part of the ",(0,r.jsx)(t.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"})," plugin that must be purchased."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport { getRxStorageWorker } from 'rxdb-premium/plugins/storage-worker';\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageWorker(\n {\n /**\n * This file must be statically served from a webserver.\n * You might want to first copy it somewhere outside of\n * your node_modules folder.\n */\n workerInput: 'node_modules/rxdb-premium/dist/workers/opfs.worker.js'\n }\n )\n});\n"})}),"\n",(0,r.jsx)(t.h2,{id:"using-opfs-in-the-main-thread-instead-of-a-worker",children:"Using OPFS in the main thread instead of a worker"}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"createSyncAccessHandle"})," method from the Filesystem API is only available inside of a Webworker. Therefore you cannot use ",(0,r.jsx)(t.code,{children:"getRxStorageOPFS()"})," in the main thread. But there is a slightly slower way to access the virtual filesystem from the main thread. RxDB support the ",(0,r.jsx)(t.code,{children:"getRxStorageOPFSMainThread()"})," for that. Notice that this uses the ",(0,r.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createWritable",children:"createWritable"})," function which is not supported in safari."]}),"\n",(0,r.jsx)(t.p,{children:"Using OPFS from the main thread can have benefits because not having to cross the worker bridge can reduce latence in reads and writes."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"import { createRxDatabase } from 'rxdb';\nimport { getRxStorageOPFSMainThread } from 'rxdb-premium/plugins/storage-worker';\n\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageOPFSMainThread()\n});\n"})}),"\n",(0,r.jsxs)(t.h2,{id:"building-a-custom-workerjs",children:["Building a custom ",(0,r.jsx)(t.code,{children:"worker.js"})]}),"\n",(0,r.jsxs)(t.p,{children:["When you want to run additional plugins like storage wrappers or replication ",(0,r.jsx)(t.strong,{children:"inside"})," of the worker, you have to build your own ",(0,r.jsx)(t.code,{children:"worker.js"})," file. You can do that similar to other workers by calling ",(0,r.jsx)(t.code,{children:"exposeWorkerRxStorage"})," like described in the ",(0,r.jsx)(t.a,{href:"/rx-storage-worker.html",children:"worker storage plugin"}),"."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"// inside of the worker.js file\nimport { getRxStorageOPFS } from 'rxdb-premium/plugins/storage-opfs';\nimport { exposeWorkerRxStorage } from 'rxdb-premium/plugins/storage-worker';\n\nconst storage = getRxStorageOPFS();\nexposeWorkerRxStorage({\n storage\n});\n"})}),"\n",(0,r.jsxs)(t.h2,{id:"setting-usesrxdatabaseinworker-when-a-rxdatabase-is-also-used-inside-of-the-worker",children:["Setting ",(0,r.jsx)(t.code,{children:"usesRxDatabaseInWorker"})," when a RxDatabase is also used inside of the worker"]}),"\n",(0,r.jsxs)(t.p,{children:["When you use the OPFS inside of a worker, it will internally use strings to represent operation results. This has the benefit that transferring strings from the worker to the main thread, is way faster compared to complex json objects. The ",(0,r.jsx)(t.code,{children:"getRxStorageWorker()"})," will automatically decode these strings on the main thread so that the data can be used by the RxDatabase."]}),"\n",(0,r.jsxs)(t.p,{children:["But using a RxDatabase ",(0,r.jsx)(t.strong,{children:"inside"})," of your worker can make sense for example when you want to move the ",(0,r.jsx)(t.a,{href:"/replication.html",children:"replication"})," with a server. To enable this, you have to set ",(0,r.jsx)(t.code,{children:"usesRxDatabaseInWorker"})," to ",(0,r.jsx)(t.code,{children:"true"}),":"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-ts",children:"// inside of the worker.js file\nimport { getRxStorageOPFS } from 'rxdb-premium/plugins/storage-opfs';\nconst storage = getRxStorageOPFS({\n usesRxDatabaseInWorker: true\n});\n"})}),"\n",(0,r.jsxs)(t.h2,{id:"setting-jsonpositionsize-to-increase-the-maximum-database-size",children:["Setting ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," to increase the maximum database size."]}),"\n",(0,r.jsxs)(t.p,{children:["By default the ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," value is set to ",(0,r.jsx)(t.code,{children:"8"})," which allows the database to get up to 100 megabytes in size (per collection).\nThis is ok for most use cases but you might want to just increase ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," to ",(0,r.jsx)(t.code,{children:"14"}),".\nIn the next major RxDB version the default will be set to ",(0,r.jsx)(t.code,{children:"14"}),", but this was not possible without introducing a breaking change."]}),"\n",(0,r.jsxs)(t.admonition,{type:"note",children:[(0,r.jsxs)(t.p,{children:["If you have already stored data, you cannot just change the ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," value because your stored binary data will not be compatible anymore."]}),(0,r.jsxs)(t.p,{children:["Also there is a ",(0,r.jsx)(t.code,{children:"opfs-big.worker.js"})," file that has ",(0,r.jsx)(t.code,{children:"jsonPositionSize"})," set to ",(0,r.jsx)(t.code,{children:"14"})," already."]})]}),"\n",(0,r.jsx)(t.h2,{id:"opfs-in-electron-react-native-or-capacitorjs",children:"OPFS in Electron, React-Native or Capacitor.js"}),"\n",(0,r.jsx)(t.p,{children:"Origin Private File System is a browser API that is only accessible in browsers. Other JavaScript like React-Native or Node.js, do not support it."}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Electron"})," has two JavaScript contexts: the browser (chromium) context and the Node.js context. While you could use the OPFS API in the browser context, it is not recommended. Instead you should use the Filesystem API of Node.js and then only transfer the relevant data with the ",(0,r.jsx)(t.a,{href:"https://www.electronjs.org/de/docs/latest/api/ipc-renderer",children:"ipcRenderer"}),". With RxDB that is pretty easy to configure:"]}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:["In the ",(0,r.jsx)(t.code,{children:"main.js"}),", expose the ",(0,r.jsx)(t.a,{href:"/rx-storage-filesystem-node.html",children:"Node Filesystem"})," storage with the ",(0,r.jsx)(t.code,{children:"exposeIpcMainRxStorage()"})," that comes with the ",(0,r.jsx)(t.a,{href:"/electron.html",children:"electron plugin"})]}),"\n",(0,r.jsxs)(t.li,{children:["In the browser context, access the main storage with the ",(0,r.jsx)(t.code,{children:"getRxStorageIpcRenderer()"})," method."]}),"\n"]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"React Native"})," (and Expo) does not have an OPFS API. You could use the ReactNative Filesystem to directly write data. But to get a fully featured database like RxDB it is easier to use the ",(0,r.jsx)(t.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"})," which starts an SQLite database inside of the ReactNative app and uses that to do the database operations."]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Capacitor.js"})," is able to access the OPFS API."]}),"\n",(0,r.jsxs)(t.h2,{id:"difference-between-file-system-access-api-and-origin-private-file-system-opfs",children:["Difference between ",(0,r.jsx)(t.code,{children:"File System Access API"})," and ",(0,r.jsx)(t.code,{children:"Origin Private File System (OPFS)"})]}),"\n",(0,r.jsxs)(t.p,{children:["Often developers are confused with the differences between the ",(0,r.jsx)(t.code,{children:"File System Access API"})," and the ",(0,r.jsx)(t.code,{children:"Origin Private File System (OPFS)"}),"."]}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:["The ",(0,r.jsx)(t.code,{children:"File System Access API"})," provides access to the files on the device file system, like the ones shown in the file explorer of the operating system. To use the File System API, the user has to actively select the files from a filepicker."]}),"\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.code,{children:"Origin Private File System (OPFS)"})," is a sub-part of the ",(0,r.jsx)(t.code,{children:"File System Standard"})," and it only describes the things you can do with the filesystem root from ",(0,r.jsx)(t.code,{children:"navigator.storage.getDirectory()"}),". OPFS writes to a ",(0,r.jsx)(t.strong,{children:"sandboxed"})," filesystem, not visible to the user. Therefore the user does not have to actively select or allow the data access."]}),"\n"]}),"\n",(0,r.jsx)(t.h2,{id:"learn-more-about-opfs",children:"Learn more about OPFS:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsx)(t.li,{children:(0,r.jsx)(t.a,{href:"https://webkit.org/blog/12257/the-file-system-access-api-with-origin-private-file-system/",children:"WebKit: The File System API with Origin Private File System"})}),"\n",(0,r.jsx)(t.li,{children:(0,r.jsx)(t.a,{href:"https://caniuse.com/native-filesystem-api",children:"Browser Support"})}),"\n",(0,r.jsx)(t.li,{children:(0,r.jsx)(t.a,{href:"https://pubkey.github.io/client-side-databases/database-comparison/index.html",children:"Performance Test Tool"})}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},8453:(e,t,s)=>{s.d(t,{R:()=>a,x:()=>o});var r=s(6540);const i={},n=r.createContext(i);function a(e){const t=r.useContext(n);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),r.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/714575d7.05a79107.js b/docs/assets/js/714575d7.05a79107.js new file mode 100644 index 00000000000..2f9ed9ec9bc --- /dev/null +++ b/docs/assets/js/714575d7.05a79107.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3185],{4880:(e,o,n)=>{n.r(o),n.d(o,{assets:()=>i,contentTitle:()=>l,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>r});var c=n(4848),t=n(8453);const a={title:"Local Documents",slug:"rx-local-document.html"},l="Local Documents",s={id:"rx-local-document",title:"Local Documents",description:"Local documents are a special class of documents which are used to store local metadata.",source:"@site/docs/rx-local-document.md",sourceDirName:".",slug:"/rx-local-document.html",permalink:"/rx-local-document.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Local Documents",slug:"rx-local-document.html"},sidebar:"tutorialSidebar",previous:{title:"Key Compression",permalink:"/key-compression.html"},next:{title:"Leader Election",permalink:"/leader-election.html"}},i={},r=[{value:"Add the local documents plugin",id:"add-the-local-documents-plugin",level:2},{value:"Activate the plugin for a RxDatabase or RxCollection",id:"activate-the-plugin-for-a-rxdatabase-or-rxcollection",level:2},{value:"insertLocal()",id:"insertlocal",level:2},{value:"upsertLocal()",id:"upsertlocal",level:2},{value:"getLocal()",id:"getlocal",level:2},{value:"getLocal$()",id:"getlocal-1",level:2},{value:"RxLocalDocument",id:"rxlocaldocument",level:2}];function d(e){const o={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(o.h1,{id:"local-documents",children:"Local Documents"}),"\n",(0,c.jsx)(o.p,{children:"Local documents are a special class of documents which are used to store local metadata.\nThey come in handy when you want to store settings or additional data next to your documents."}),"\n",(0,c.jsxs)(o.ul,{children:["\n",(0,c.jsxs)(o.li,{children:["Local Documents can exist on ",(0,c.jsx)(o.code,{children:"RxDatabase"})," or ",(0,c.jsx)(o.code,{children:"RxCollection"}),"."]}),"\n",(0,c.jsx)(o.li,{children:"Local Document do not have to match the collections schema."}),"\n",(0,c.jsx)(o.li,{children:"Local Documents do not get replicated."}),"\n",(0,c.jsx)(o.li,{children:"Local Documents will not be found on queries."}),"\n",(0,c.jsx)(o.li,{children:"Local Documents can not have attachments."}),"\n",(0,c.jsxs)(o.li,{children:["Local Documents will not get handled by the ",(0,c.jsx)(o.a,{href:"/migration-schema.html",children:"migration-schema"}),"."]}),"\n",(0,c.jsxs)(o.li,{children:["The id of a local document has the ",(0,c.jsx)(o.code,{children:"maxLength"})," of ",(0,c.jsx)(o.code,{children:"128"})," characters."]}),"\n"]}),"\n",(0,c.jsx)(o.h2,{id:"add-the-local-documents-plugin",children:"Add the local documents plugin"}),"\n",(0,c.jsxs)(o.p,{children:["To enable the local documents, you have to add the ",(0,c.jsx)(o.code,{children:"local-documents"})," plugin."]}),"\n",(0,c.jsx)(o.pre,{children:(0,c.jsx)(o.code,{className:"language-ts",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBLocalDocumentsPlugin } from 'rxdb/plugins/local-documents';\naddRxPlugin(RxDBLocalDocumentsPlugin);\n"})}),"\n",(0,c.jsx)(o.h2,{id:"activate-the-plugin-for-a-rxdatabase-or-rxcollection",children:"Activate the plugin for a RxDatabase or RxCollection"}),"\n",(0,c.jsxs)(o.p,{children:["For better performance, the local document plugin does not create a storage for every database or collection that is created.\nInstead you have to set ",(0,c.jsx)(o.code,{children:"localDocuments: true"})," when you want to store local documents in the instance."]}),"\n",(0,c.jsx)(o.pre,{children:(0,c.jsx)(o.code,{className:"language-js",children:"// activate local documents on a RxDatabase\nconst myDatabase = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageDexie(),\n localDocuments: true // <- activate this to store local documents in the database\n});\n\nmyDatabase.addCollections({\n messages: {\n schema: messageSchema,\n localDocuments: true // <- activate this to store local documents in the collection\n }\n});\n"})}),"\n",(0,c.jsx)(o.admonition,{type:"note",children:(0,c.jsxs)(o.p,{children:["If you want to store local documents in a ",(0,c.jsx)(o.code,{children:"RxCollection"})," but ",(0,c.jsx)(o.strong,{children:"NOT"})," in the ",(0,c.jsx)(o.code,{children:"RxDatabase"}),", you ",(0,c.jsx)(o.strong,{children:"MUST NOT"})," set ",(0,c.jsx)(o.code,{children:"localDocuments: true"})," in the ",(0,c.jsx)(o.code,{children:"RxDatabase"})," because it will only slow down the initial database creation."]})}),"\n",(0,c.jsx)(o.h2,{id:"insertlocal",children:"insertLocal()"}),"\n",(0,c.jsxs)(o.p,{children:["Creates a local document for the database or collection. Throws if a local document with the same id already exists. Returns a Promise which resolves the new ",(0,c.jsx)(o.code,{children:"RxLocalDocument"}),"."]}),"\n",(0,c.jsx)(o.pre,{children:(0,c.jsx)(o.code,{className:"language-javascript",children:"const localDoc = await myCollection.insertLocal(\n 'foobar', // id\n { // data\n foo: 'bar'\n }\n);\n\n// you can also use local-documents on a database\nconst localDoc = await myDatabase.insertLocal(\n 'foobar', // id\n { // data\n foo: 'bar'\n }\n);\n"})}),"\n",(0,c.jsx)(o.h2,{id:"upsertlocal",children:"upsertLocal()"}),"\n",(0,c.jsxs)(o.p,{children:["Creates a local document for the database or collection if not exists. Overwrites the if exists. Returns a Promise which resolves the ",(0,c.jsx)(o.code,{children:"RxLocalDocument"}),"."]}),"\n",(0,c.jsx)(o.pre,{children:(0,c.jsx)(o.code,{className:"language-javascript",children:"const localDoc = await myCollection.upsertLocal(\n 'foobar', // id\n { // data\n foo: 'bar'\n }\n);\n"})}),"\n",(0,c.jsx)(o.h2,{id:"getlocal",children:"getLocal()"}),"\n",(0,c.jsxs)(o.p,{children:["Find a ",(0,c.jsx)(o.code,{children:"RxLocalDocument"})," by its id. Returns a Promise which resolves the ",(0,c.jsx)(o.code,{children:"RxLocalDocument"})," or ",(0,c.jsx)(o.code,{children:"null"})," if not exists."]}),"\n",(0,c.jsx)(o.pre,{children:(0,c.jsx)(o.code,{className:"language-javascript",children:"const localDoc = await myCollection.getLocal('foobar');\n"})}),"\n",(0,c.jsx)(o.h2,{id:"getlocal-1",children:"getLocal$()"}),"\n",(0,c.jsxs)(o.p,{children:["Like ",(0,c.jsx)(o.code,{children:"getLocal()"})," but returns an ",(0,c.jsx)(o.code,{children:"Observable"})," that emits the document or ",(0,c.jsx)(o.code,{children:"null"})," if not exists."]}),"\n",(0,c.jsx)(o.pre,{children:(0,c.jsx)(o.code,{className:"language-javascript",children:"const subscription = myCollection.getLocal$('foobar').subscribe(documentOrNull => {\n console.dir(documentOrNull); // > RxLocalDocument or null\n});\n"})}),"\n",(0,c.jsx)(o.h2,{id:"rxlocaldocument",children:"RxLocalDocument"}),"\n",(0,c.jsxs)(o.p,{children:["A ",(0,c.jsx)(o.code,{children:"RxLocalDocument"})," behaves like a normal ",(0,c.jsx)(o.code,{children:"RxDocument"}),"."]}),"\n",(0,c.jsx)(o.pre,{children:(0,c.jsx)(o.code,{className:"language-javascript",children:"const localDoc = await myCollection.getLocal('foobar');\n\n// access data\nconst foo = localDoc.get('foo');\n\n// change data\nlocalDoc.set('foo', 'bar2');\nawait localDoc.save();\n\n// observe data\nlocalDoc.get$('foo').subscribe(value => { /* .. */ });\n\n// remove it\nawait localDoc.remove();\n"})}),"\n",(0,c.jsx)(o.admonition,{type:"note",children:(0,c.jsx)(o.p,{children:"Because the local document does not have a schema, accessing the documents data-fields via pseudo-proxy will not work."})}),"\n",(0,c.jsx)(o.pre,{children:(0,c.jsx)(o.code,{className:"language-javascript",children:"const foo = localDoc.foo; // undefined\nconst foo = localDoc.get('foo'); // works!\n\nlocalDoc.foo = 'bar'; // does not work!\nlocalDoc.set('foo', 'bar'); // works\n"})}),"\n",(0,c.jsxs)(o.p,{children:["For the usage with typescript, you can have access to the typed data of the document over ",(0,c.jsx)(o.code,{children:"toJSON()"})]}),"\n",(0,c.jsx)(o.pre,{children:(0,c.jsx)(o.code,{className:"language-ts",children:"declare type MyLocalDocumentType = {\n foo: string\n}\nconst localDoc = await myCollection.upsertLocal(\n 'foobar', // id\n { // data\n foo: 'bar'\n }\n);\n\n// typescript will know that foo is a string\nconst foo: string = localDoc.toJSON().foo;\n"})})]})}function h(e={}){const{wrapper:o}={...(0,t.R)(),...e.components};return o?(0,c.jsx)(o,{...e,children:(0,c.jsx)(d,{...e})}):d(e)}},8453:(e,o,n)=>{n.d(o,{R:()=>l,x:()=>s});var c=n(6540);const t={},a=c.createContext(t);function l(e){const o=c.useContext(a);return c.useMemo((function(){return"function"==typeof e?e(o):{...o,...e}}),[o,e])}function s(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:l(e.components),c.createElement(a.Provider,{value:o},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/714575d7.0c21cd05.js b/docs/assets/js/714575d7.0c21cd05.js deleted file mode 100644 index 6b1f68b852b..00000000000 --- a/docs/assets/js/714575d7.0c21cd05.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3185],{4880:(e,o,n)=>{n.r(o),n.d(o,{assets:()=>i,contentTitle:()=>l,default:()=>u,frontMatter:()=>t,metadata:()=>s,toc:()=>r});var a=n(4848),c=n(8453);const t={title:"Local Documents",slug:"rx-local-document.html"},l="Local Documents",s={id:"rx-local-document",title:"Local Documents",description:"Local documents are a special class of documents which are used to store local metadata.",source:"@site/docs/rx-local-document.md",sourceDirName:".",slug:"/rx-local-document.html",permalink:"/rx-local-document.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Local Documents",slug:"rx-local-document.html"},sidebar:"tutorialSidebar",previous:{title:"Key Compression",permalink:"/key-compression.html"},next:{title:"Leader Election",permalink:"/leader-election.html"}},i={},r=[{value:"Add the local documents plugin",id:"add-the-local-documents-plugin",level:2},{value:"Activate the plugin for a RxDatabase or RxCollection",id:"activate-the-plugin-for-a-rxdatabase-or-rxcollection",level:2},{value:"insertLocal()",id:"insertlocal",level:2},{value:"upsertLocal()",id:"upsertlocal",level:2},{value:"getLocal()",id:"getlocal",level:2},{value:"getLocal$()",id:"getlocal-1",level:2},{value:"RxLocalDocument",id:"rxlocaldocument",level:2},{value:"NOTICE: Because the local document does not have a schema, accessing the documents data-fields via pseudo-proxy will not work.",id:"notice-because-the-local-document-does-not-have-a-schema-accessing-the-documents-data-fields-via-pseudo-proxy-will-not-work",level:2}];function d(e){const o={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,c.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(o.h1,{id:"local-documents",children:"Local Documents"}),"\n",(0,a.jsx)(o.p,{children:"Local documents are a special class of documents which are used to store local metadata.\nThey come in handy when you want to store settings or additional data next to your documents."}),"\n",(0,a.jsxs)(o.ul,{children:["\n",(0,a.jsxs)(o.li,{children:["Local Documents can exist on ",(0,a.jsx)(o.code,{children:"RxDatabase"})," or ",(0,a.jsx)(o.code,{children:"RxCollection"}),"."]}),"\n",(0,a.jsx)(o.li,{children:"Local Document do not have to match the collections schema."}),"\n",(0,a.jsx)(o.li,{children:"Local Documents do not get replicated."}),"\n",(0,a.jsx)(o.li,{children:"Local Documents will not be found on queries."}),"\n",(0,a.jsx)(o.li,{children:"Local Documents can not have attachments."}),"\n",(0,a.jsxs)(o.li,{children:["Local Documents will not get handled by the ",(0,a.jsx)(o.a,{href:"/migration-schema.html",children:"migration-schema"}),"."]}),"\n",(0,a.jsxs)(o.li,{children:["The id of a local document has the ",(0,a.jsx)(o.code,{children:"maxLength"})," of ",(0,a.jsx)(o.code,{children:"128"})," characters."]}),"\n"]}),"\n",(0,a.jsx)(o.h2,{id:"add-the-local-documents-plugin",children:"Add the local documents plugin"}),"\n",(0,a.jsxs)(o.p,{children:["To enable the local documents, you have to add the ",(0,a.jsx)(o.code,{children:"local-documents"})," plugin."]}),"\n",(0,a.jsx)(o.pre,{children:(0,a.jsx)(o.code,{className:"language-ts",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBLocalDocumentsPlugin } from 'rxdb/plugins/local-documents';\naddRxPlugin(RxDBLocalDocumentsPlugin);\n"})}),"\n",(0,a.jsx)(o.h2,{id:"activate-the-plugin-for-a-rxdatabase-or-rxcollection",children:"Activate the plugin for a RxDatabase or RxCollection"}),"\n",(0,a.jsxs)(o.p,{children:["For better performance, the local document plugin does not create a storage for every database or collection that is created.\nInstead you have to set ",(0,a.jsx)(o.code,{children:"localDocuments: true"})," when you want to store local documents in the instance."]}),"\n",(0,a.jsx)(o.pre,{children:(0,a.jsx)(o.code,{className:"language-js",children:"// activate local documents on a RxDatabase\nconst myDatabase = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStorageDexie(),\n localDocuments: true // <- activate this to store local documents in the database\n});\n\nmyDatabase.addCollections({\n messages: {\n schema: messageSchema,\n localDocuments: true // <- activate this to store local documents in the collection\n }\n});\n"})}),"\n",(0,a.jsxs)(o.p,{children:[(0,a.jsx)(o.strong,{children:"NOTICE:"})," If you want to store local documents in a ",(0,a.jsx)(o.code,{children:"RxCollection"})," but ",(0,a.jsx)(o.strong,{children:"NOT"})," in the ",(0,a.jsx)(o.code,{children:"RxDatabase"}),", you ",(0,a.jsx)(o.strong,{children:"MUST NOT"})," set ",(0,a.jsx)(o.code,{children:"localDocuments: true"})," in the ",(0,a.jsx)(o.code,{children:"RxDatabase"})," because it will only slow down the initial database creation."]}),"\n",(0,a.jsx)(o.h2,{id:"insertlocal",children:"insertLocal()"}),"\n",(0,a.jsxs)(o.p,{children:["Creates a local document for the database or collection. Throws if a local document with the same id already exists. Returns a Promise which resolves the new ",(0,a.jsx)(o.code,{children:"RxLocalDocument"}),"."]}),"\n",(0,a.jsx)(o.pre,{children:(0,a.jsx)(o.code,{className:"language-javascript",children:"const localDoc = await myCollection.insertLocal(\n 'foobar', // id\n { // data\n foo: 'bar'\n }\n);\n\n// you can also use local-documents on a database\nconst localDoc = await myDatabase.insertLocal(\n 'foobar', // id\n { // data\n foo: 'bar'\n }\n);\n"})}),"\n",(0,a.jsx)(o.h2,{id:"upsertlocal",children:"upsertLocal()"}),"\n",(0,a.jsxs)(o.p,{children:["Creates a local document for the database or collection if not exists. Overwrites the if exists. Returns a Promise which resolves the ",(0,a.jsx)(o.code,{children:"RxLocalDocument"}),"."]}),"\n",(0,a.jsx)(o.pre,{children:(0,a.jsx)(o.code,{className:"language-javascript",children:"const localDoc = await myCollection.upsertLocal(\n 'foobar', // id\n { // data\n foo: 'bar'\n }\n);\n"})}),"\n",(0,a.jsx)(o.h2,{id:"getlocal",children:"getLocal()"}),"\n",(0,a.jsxs)(o.p,{children:["Find a ",(0,a.jsx)(o.code,{children:"RxLocalDocument"})," by its id. Returns a Promise which resolves the ",(0,a.jsx)(o.code,{children:"RxLocalDocument"})," or ",(0,a.jsx)(o.code,{children:"null"})," if not exists."]}),"\n",(0,a.jsx)(o.pre,{children:(0,a.jsx)(o.code,{className:"language-javascript",children:"const localDoc = await myCollection.getLocal('foobar');\n"})}),"\n",(0,a.jsx)(o.h2,{id:"getlocal-1",children:"getLocal$()"}),"\n",(0,a.jsxs)(o.p,{children:["Like ",(0,a.jsx)(o.code,{children:"getLocal()"})," but returns an ",(0,a.jsx)(o.code,{children:"Observable"})," that emits the document or ",(0,a.jsx)(o.code,{children:"null"})," if not exists."]}),"\n",(0,a.jsx)(o.pre,{children:(0,a.jsx)(o.code,{className:"language-javascript",children:"const subscription = myCollection.getLocal$('foobar').subscribe(documentOrNull => {\n console.dir(documentOrNull); // > RxLocalDocument or null\n});\n"})}),"\n",(0,a.jsx)(o.h2,{id:"rxlocaldocument",children:"RxLocalDocument"}),"\n",(0,a.jsxs)(o.p,{children:["A ",(0,a.jsx)(o.code,{children:"RxLocalDocument"})," behaves like a normal ",(0,a.jsx)(o.code,{children:"RxDocument"}),"."]}),"\n",(0,a.jsx)(o.pre,{children:(0,a.jsx)(o.code,{className:"language-javascript",children:"const localDoc = await myCollection.getLocal('foobar');\n\n// access data\nconst foo = localDoc.get('foo');\n\n// change data\nlocalDoc.set('foo', 'bar2');\nawait localDoc.save();\n\n// observe data\nlocalDoc.get$('foo').subscribe(value => { /* .. */ });\n\n// remove it\nawait localDoc.remove();\n"})}),"\n",(0,a.jsx)(o.h2,{id:"notice-because-the-local-document-does-not-have-a-schema-accessing-the-documents-data-fields-via-pseudo-proxy-will-not-work",children:"NOTICE: Because the local document does not have a schema, accessing the documents data-fields via pseudo-proxy will not work."}),"\n",(0,a.jsx)(o.pre,{children:(0,a.jsx)(o.code,{className:"language-javascript",children:"const foo = localDoc.foo; // undefined\nconst foo = localDoc.get('foo'); // works!\n\nlocalDoc.foo = 'bar'; // does not work!\nlocalDoc.set('foo', 'bar'); // works\n"})}),"\n",(0,a.jsxs)(o.p,{children:["For the usage with typescript, you can have access to the typed data of the document over ",(0,a.jsx)(o.code,{children:"toJSON()"})]}),"\n",(0,a.jsx)(o.pre,{children:(0,a.jsx)(o.code,{className:"language-ts",children:"declare type MyLocalDocumentType = {\n foo: string\n}\nconst localDoc = await myCollection.upsertLocal(\n 'foobar', // id\n { // data\n foo: 'bar'\n }\n);\n\n// typescript will know that foo is a string\nconst foo: string = localDoc.toJSON().foo;\n"})})]})}function u(e={}){const{wrapper:o}={...(0,c.R)(),...e.components};return o?(0,a.jsx)(o,{...e,children:(0,a.jsx)(d,{...e})}):d(e)}},8453:(e,o,n)=>{n.d(o,{R:()=>l,x:()=>s});var a=n(6540);const c={},t=a.createContext(c);function l(e){const o=a.useContext(t);return a.useMemo((function(){return"function"==typeof e?e(o):{...o,...e}}),[o,e])}function s(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(c):e.components||c:l(e.components),a.createElement(t.Provider,{value:o},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/7f02c700.0bda96b7.js b/docs/assets/js/7f02c700.f33748e9.js similarity index 58% rename from docs/assets/js/7f02c700.0bda96b7.js rename to docs/assets/js/7f02c700.f33748e9.js index e8564e96ef3..0e6e02012f8 100644 --- a/docs/assets/js/7f02c700.0bda96b7.js +++ b/docs/assets/js/7f02c700.f33748e9.js @@ -1 +1 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9592],{9640:(e,t,i)=>{i.r(t),i.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>a,toc:()=>c});var n=i(4848),r=i(8453);const o={title:"Firestore Replication",slug:"replication-firestore.html"},s="Replication with Firestore from Firebase",a={id:"replication-firestore",title:"Firestore Replication",description:"With the replication-firestore plugin you can do a two-way realtime replication",source:"@site/docs/replication-firestore.md",sourceDirName:".",slug:"/replication-firestore.html",permalink:"/replication-firestore.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Firestore Replication",slug:"replication-firestore.html"},sidebar:"tutorialSidebar",previous:{title:"WebRTC Replication",permalink:"/replication-webrtc.html"},next:{title:"NATS Replication",permalink:"/replication-nats.html"}},l={},c=[{value:"Usage",id:"usage",level:2},{value:"Handling deletes",id:"handling-deletes",level:2},{value:"Do not set enableIndexedDbPersistence()",id:"do-not-set-enableindexeddbpersistence",level:2},{value:"Using the replication with an already existing Firestore Database State",id:"using-the-replication-with-an-already-existing-firestore-database-state",level:2},{value:"Filtered Replication",id:"filtered-replication",level:2}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,r.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"replication-with-firestore-from-firebase",children:"Replication with Firestore from Firebase"}),"\n",(0,n.jsxs)(t.p,{children:["With the ",(0,n.jsx)(t.code,{children:"replication-firestore"})," plugin you can do a two-way realtime replication\nbetween your client side ",(0,n.jsx)(t.a,{href:"./",children:"RxDB"})," Database and a ",(0,n.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore",children:"Cloud Firestore"})," database that is hosted on the Firebase platform. It will use the ",(0,n.jsx)(t.a,{href:"/replication.html",children:"RxDB Replication Protocol"})," to manage the replication streams, error- and conflict handling."]}),"\n",(0,n.jsx)("p",{align:"center",children:(0,n.jsx)("img",{src:"./files/alternatives/firebase.svg",alt:"Firebase",height:"40"})}),"\n",(0,n.jsx)(t.p,{children:"Replicating your Firestore state to RxDB can bring multiple benefits compared to using the Firestore directly:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"It can reduce your cloud fees because your queries run against the local state of the documents without touching a server and writes can be batched up locally and send to the backend in bulks. This is mostly the case for read heavy applications."}),"\n",(0,n.jsxs)(t.li,{children:["You can run complex NoSQL queries on your documents because you are not bound to the ",(0,n.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore/query-data/queries",children:"Firestore Query"})," handling. You can also use local indexes, ",(0,n.jsx)(t.a,{href:"/key-compression.html",children:"compression"})," and ",(0,n.jsx)(t.a,{href:"/encryption.html",children:"encryption"})," and do things like fulltext search, fully locally."]}),"\n",(0,n.jsxs)(t.li,{children:["Your application can be truly ",(0,n.jsx)(t.a,{href:"/offline-first.html",children:"Offline-First"})," because your data is stored in a client side database. In contrast Firestore by itself only provides options to support ",(0,n.jsx)(t.a,{href:"https://cloud.google.com/firestore/docs/manage-data/enable-offline",children:"offline also"})," which more works like a cache and requires the user to be online at application start to run authentication."]}),"\n",(0,n.jsxs)(t.li,{children:["It reduces the vendor lock in because you can switch out the backend server afterwards without having to rebuild big parts of the application. RxDB supports replication plugins with multiple technologies and it is even easy to set up with your ",(0,n.jsx)(t.a,{href:"/replication.html",children:"custom backend"}),"."]}),"\n",(0,n.jsxs)(t.li,{children:["You can use sophisticated ",(0,n.jsx)(t.a,{href:"/replication.html#conflict-handling",children:"conflict resolution strategies"})," so you are not bound to the Firestore ",(0,n.jsx)(t.a,{href:"https://stackoverflow.com/a/47781502/3443137",children:"last-write-wins"})," strategy which is not suitable for many applications."]}),"\n",(0,n.jsx)(t.li,{children:"The initial load time of your application can be decreased because it will do an incremental replication on restarts."}),"\n"]}),"\n",(0,n.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,n.jsx)(t.p,{children:"First initialize your Firestore database like you would do without RxDB."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"import * as firebase from 'firebase/app';\nimport {\n getFirestore,\n collection\n} from 'firebase/firestore';\n\nconst projectId = 'my-project-id';\nconst app = firebase.initializeApp({\n projectId,\n databaseURL: 'http://localhost:8080?ns=' + projectId,\n /* ... */\n});\nconst firestoreDatabase = getFirestore(app);\nconst firestoreCollection = collection(firestoreDatabase, 'my-collection-name');\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Then you can start the replication by calling ",(0,n.jsx)(t.code,{children:"replicateFirestore()"})," on your ",(0,n.jsx)(t.a,{href:"/rx-collection.html",children:"RxCollection"}),"."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"const replicationState = replicateFirestore(\n {\n collection: myRxCollection,\n firestore: {\n projectId,\n database: firestoreDatabase,\n collection: firestoreCollection\n },\n pull: {},\n push: {},\n /**\n * Either do a live or a one-time replication\n * [default=true]\n */\n live: true,\n /**\n * (optional) likely you should just use the default.\n *\n * In firestore it is not possible to read out\n * the internally used write timestamp of a document.\n * Even if we could read it out, it is not indexed which\n * is required for fetch 'changes-since-x'.\n * So instead we have to rely on a custom user defined field\n * that contains the server time which is set by firestore via serverTimestamp()\n * IMPORTANT: The serverTimestampField MUST NOT be part of the collections RxJsonSchema!\n * [default='serverTimestamp']\n */\n serverTimestampField: 'serverTimestamp'\n }\n);\n"})}),"\n",(0,n.jsxs)(t.p,{children:["To observe and cancel the replication, you can use any other methods from the ",(0,n.jsx)(t.a,{href:"/replication.html",children:"ReplicationState"})," like ",(0,n.jsx)(t.code,{children:"error$"}),", ",(0,n.jsx)(t.code,{children:"cancel()"})," and ",(0,n.jsx)(t.code,{children:"awaitInitialReplication()"}),"."]}),"\n",(0,n.jsx)(t.h2,{id:"handling-deletes",children:"Handling deletes"}),"\n",(0,n.jsxs)(t.p,{children:["RxDB requires you to never ",(0,n.jsx)(t.a,{href:"/replication.html#data-layout-on-the-server",children:"fully delete documents"}),". This is needed to be able to replicate the deletion state of a document to other instances. The firestore replication will set a boolean ",(0,n.jsx)(t.code,{children:"_deleted"})," field to all documents to indicate the deletion state. You can change this by setting a different ",(0,n.jsx)(t.code,{children:"deletedField"})," in the sync options."]}),"\n",(0,n.jsxs)(t.h2,{id:"do-not-set-enableindexeddbpersistence",children:["Do not set ",(0,n.jsx)(t.code,{children:"enableIndexedDbPersistence()"})]}),"\n",(0,n.jsxs)(t.p,{children:["Firestore has the ",(0,n.jsx)(t.code,{children:"enableIndexedDbPersistence()"})," feature which caches document states locally to IndexedDB. This is not needed when you replicate your Firestore with RxDB because RxDB itself will store the data locally already."]}),"\n",(0,n.jsx)(t.h2,{id:"using-the-replication-with-an-already-existing-firestore-database-state",children:"Using the replication with an already existing Firestore Database State"}),"\n",(0,n.jsxs)(t.p,{children:["If you have not used RxDB before and you already have documents inside of your Firestore database, you have\nto manually set the ",(0,n.jsx)(t.code,{children:"_deleted"})," field to ",(0,n.jsx)(t.code,{children:"false"})," and the ",(0,n.jsx)(t.code,{children:"serverTimestamp"})," to all existing documents."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"import {\n getDocs,\n query,\n serverTimestamp\n} from 'firebase/firestore';\nconst allDocsResult = await getDocs(query(firestoreCollection));\nallDocsResult.forEach(doc => {\n doc.update({\n _deleted: false,\n serverTimestamp: serverTimestamp()\n })\n});\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Also notice that if you do writes from non-RxDB applications, you have to keep these fields in sync. It is recommended to use the ",(0,n.jsx)(t.a,{href:"https://firebase.google.com/docs/functions/firestore-events",children:"Firestore triggers"})," to ensure that."]}),"\n",(0,n.jsx)(t.h2,{id:"filtered-replication",children:"Filtered Replication"}),"\n",(0,n.jsxs)(t.p,{children:["You might need to replicate only a subset of your collection, either to or from Firestore. You can achieve this using ",(0,n.jsx)(t.code,{children:"push.filter"})," and ",(0,n.jsx)(t.code,{children:"pull.filter"})," options."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"const replicationState = replicateFirestore(\n {\n collection: myRxCollection,\n firestore: {\n projectId,\n database: firestoreDatabase,\n collection: firestoreCollection\n },\n pull: {\n filter: [\n where('ownerId', '==', userId)\n ]\n },\n push: {\n filter: (item) => item.syncEnabled === true\n }\n }\n);\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Keep in mind that you can not use inequality operators ",(0,n.jsx)(t.code,{children:"(<, <=, !=, not-in, >, or >=)"})," in ",(0,n.jsx)(t.code,{children:"pull.filter"})," since that would cause a conflict with ordering by ",(0,n.jsx)(t.code,{children:"serverTimestamp"}),"."]})]})}function h(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},8453:(e,t,i)=>{i.d(t,{R:()=>s,x:()=>a});var n=i(6540);const r={},o=n.createContext(r);function s(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9592],{9640:(e,t,i)=>{i.r(t),i.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>a,toc:()=>c});var n=i(4848),r=i(8453);const o={title:"Firestore Replication",slug:"replication-firestore.html"},s="Replication with Firestore from Firebase",a={id:"replication-firestore",title:"Firestore Replication",description:"With the replication-firestore plugin you can do a two-way realtime replication",source:"@site/docs/replication-firestore.md",sourceDirName:".",slug:"/replication-firestore.html",permalink:"/replication-firestore.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Firestore Replication",slug:"replication-firestore.html"},sidebar:"tutorialSidebar",previous:{title:"WebRTC Replication",permalink:"/replication-webrtc.html"},next:{title:"NATS Replication",permalink:"/replication-nats.html"}},l={},c=[{value:"Usage",id:"usage",level:2},{value:"Handling deletes",id:"handling-deletes",level:2},{value:"Do not set enableIndexedDbPersistence()",id:"do-not-set-enableindexeddbpersistence",level:2},{value:"Using the replication with an already existing Firestore Database State",id:"using-the-replication-with-an-already-existing-firestore-database-state",level:2},{value:"Filtered Replication",id:"filtered-replication",level:2}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,r.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"replication-with-firestore-from-firebase",children:"Replication with Firestore from Firebase"}),"\n",(0,n.jsxs)(t.p,{children:["With the ",(0,n.jsx)(t.code,{children:"replication-firestore"})," plugin you can do a two-way realtime replication\nbetween your client side ",(0,n.jsx)(t.a,{href:"./",children:"RxDB"})," Database and a ",(0,n.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore",children:"Cloud Firestore"})," database that is hosted on the Firebase platform. It will use the ",(0,n.jsx)(t.a,{href:"/replication.html",children:"RxDB Replication Protocol"})," to manage the replication streams, error- and conflict handling."]}),"\n",(0,n.jsx)("p",{align:"center",children:(0,n.jsx)("img",{src:"./files/alternatives/firebase.svg",alt:"Firebase",height:"40"})}),"\n",(0,n.jsx)(t.p,{children:"Replicating your Firestore state to RxDB can bring multiple benefits compared to using the Firestore directly:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"It can reduce your cloud fees because your queries run against the local state of the documents without touching a server and writes can be batched up locally and send to the backend in bulks. This is mostly the case for read heavy applications."}),"\n",(0,n.jsxs)(t.li,{children:["You can run complex NoSQL queries on your documents because you are not bound to the ",(0,n.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore/query-data/queries",children:"Firestore Query"})," handling. You can also use local indexes, ",(0,n.jsx)(t.a,{href:"/key-compression.html",children:"compression"})," and ",(0,n.jsx)(t.a,{href:"/encryption.html",children:"encryption"})," and do things like fulltext search, fully locally."]}),"\n",(0,n.jsxs)(t.li,{children:["Your application can be truly ",(0,n.jsx)(t.a,{href:"/offline-first.html",children:"Offline-First"})," because your data is stored in a client side database. In contrast Firestore by itself only provides options to support ",(0,n.jsx)(t.a,{href:"https://cloud.google.com/firestore/docs/manage-data/enable-offline",children:"offline also"})," which more works like a cache and requires the user to be online at application start to run authentication."]}),"\n",(0,n.jsxs)(t.li,{children:["It reduces the vendor lock in because you can switch out the backend server afterwards without having to rebuild big parts of the application. RxDB supports replication plugins with multiple technologies and it is even easy to set up with your ",(0,n.jsx)(t.a,{href:"/replication.html",children:"custom backend"}),"."]}),"\n",(0,n.jsxs)(t.li,{children:["You can use sophisticated ",(0,n.jsx)(t.a,{href:"/replication.html#conflict-handling",children:"conflict resolution strategies"})," so you are not bound to the Firestore ",(0,n.jsx)(t.a,{href:"https://stackoverflow.com/a/47781502/3443137",children:"last-write-wins"})," strategy which is not suitable for many applications."]}),"\n",(0,n.jsx)(t.li,{children:"The initial load time of your application can be decreased because it will do an incremental replication on restarts."}),"\n"]}),"\n",(0,n.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,n.jsx)(t.p,{children:"First initialize your Firestore database like you would do without RxDB."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"import * as firebase from 'firebase/app';\nimport {\n getFirestore,\n collection\n} from 'firebase/firestore';\n\nconst projectId = 'my-project-id';\nconst app = firebase.initializeApp({\n projectId,\n databaseURL: 'http://localhost:8080?ns=' + projectId,\n /* ... */\n});\nconst firestoreDatabase = getFirestore(app);\nconst firestoreCollection = collection(firestoreDatabase, 'my-collection-name');\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Then you can start the replication by calling ",(0,n.jsx)(t.code,{children:"replicateFirestore()"})," on your ",(0,n.jsx)(t.a,{href:"/rx-collection.html",children:"RxCollection"}),"."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"const replicationState = replicateFirestore(\n {\n collection: myRxCollection,\n firestore: {\n projectId,\n database: firestoreDatabase,\n collection: firestoreCollection\n },\n pull: {},\n push: {},\n /**\n * Either do a live or a one-time replication\n * [default=true]\n */\n live: true,\n /**\n * (optional) likely you should just use the default.\n *\n * In firestore it is not possible to read out\n * the internally used write timestamp of a document.\n * Even if we could read it out, it is not indexed which\n * is required for fetch 'changes-since-x'.\n * So instead we have to rely on a custom user defined field\n * that contains the server time which is set by firestore via serverTimestamp()\n * Notice that the serverTimestampField MUST NOT be part of the collections RxJsonSchema!\n * [default='serverTimestamp']\n */\n serverTimestampField: 'serverTimestamp'\n }\n);\n"})}),"\n",(0,n.jsxs)(t.p,{children:["To observe and cancel the replication, you can use any other methods from the ",(0,n.jsx)(t.a,{href:"/replication.html",children:"ReplicationState"})," like ",(0,n.jsx)(t.code,{children:"error$"}),", ",(0,n.jsx)(t.code,{children:"cancel()"})," and ",(0,n.jsx)(t.code,{children:"awaitInitialReplication()"}),"."]}),"\n",(0,n.jsx)(t.h2,{id:"handling-deletes",children:"Handling deletes"}),"\n",(0,n.jsxs)(t.p,{children:["RxDB requires you to never ",(0,n.jsx)(t.a,{href:"/replication.html#data-layout-on-the-server",children:"fully delete documents"}),". This is needed to be able to replicate the deletion state of a document to other instances. The firestore replication will set a boolean ",(0,n.jsx)(t.code,{children:"_deleted"})," field to all documents to indicate the deletion state. You can change this by setting a different ",(0,n.jsx)(t.code,{children:"deletedField"})," in the sync options."]}),"\n",(0,n.jsxs)(t.h2,{id:"do-not-set-enableindexeddbpersistence",children:["Do not set ",(0,n.jsx)(t.code,{children:"enableIndexedDbPersistence()"})]}),"\n",(0,n.jsxs)(t.p,{children:["Firestore has the ",(0,n.jsx)(t.code,{children:"enableIndexedDbPersistence()"})," feature which caches document states locally to IndexedDB. This is not needed when you replicate your Firestore with RxDB because RxDB itself will store the data locally already."]}),"\n",(0,n.jsx)(t.h2,{id:"using-the-replication-with-an-already-existing-firestore-database-state",children:"Using the replication with an already existing Firestore Database State"}),"\n",(0,n.jsxs)(t.p,{children:["If you have not used RxDB before and you already have documents inside of your Firestore database, you have\nto manually set the ",(0,n.jsx)(t.code,{children:"_deleted"})," field to ",(0,n.jsx)(t.code,{children:"false"})," and the ",(0,n.jsx)(t.code,{children:"serverTimestamp"})," to all existing documents."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"import {\n getDocs,\n query,\n serverTimestamp\n} from 'firebase/firestore';\nconst allDocsResult = await getDocs(query(firestoreCollection));\nallDocsResult.forEach(doc => {\n doc.update({\n _deleted: false,\n serverTimestamp: serverTimestamp()\n })\n});\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Also notice that if you do writes from non-RxDB applications, you have to keep these fields in sync. It is recommended to use the ",(0,n.jsx)(t.a,{href:"https://firebase.google.com/docs/functions/firestore-events",children:"Firestore triggers"})," to ensure that."]}),"\n",(0,n.jsx)(t.h2,{id:"filtered-replication",children:"Filtered Replication"}),"\n",(0,n.jsxs)(t.p,{children:["You might need to replicate only a subset of your collection, either to or from Firestore. You can achieve this using ",(0,n.jsx)(t.code,{children:"push.filter"})," and ",(0,n.jsx)(t.code,{children:"pull.filter"})," options."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",children:"const replicationState = replicateFirestore(\n {\n collection: myRxCollection,\n firestore: {\n projectId,\n database: firestoreDatabase,\n collection: firestoreCollection\n },\n pull: {\n filter: [\n where('ownerId', '==', userId)\n ]\n },\n push: {\n filter: (item) => item.syncEnabled === true\n }\n }\n);\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Keep in mind that you can not use inequality operators ",(0,n.jsx)(t.code,{children:"(<, <=, !=, not-in, >, or >=)"})," in ",(0,n.jsx)(t.code,{children:"pull.filter"})," since that would cause a conflict with ordering by ",(0,n.jsx)(t.code,{children:"serverTimestamp"}),"."]})]})}function h(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},8453:(e,t,i)=>{i.d(t,{R:()=>s,x:()=>a});var n=i(6540);const r={},o=n.createContext(r);function s(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/8aa53ed7.59980581.js b/docs/assets/js/8aa53ed7.59980581.js new file mode 100644 index 00000000000..d699e81cdaf --- /dev/null +++ b/docs/assets/js/8aa53ed7.59980581.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6723],{5870:(e,a,n)=>{n.r(a),n.d(a,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>c});var i=n(4848),t=n(8453);const s={title:"RxDB as a Database in an Angular Application",slug:"angular-database.html",description:"Discover the RxDB Revolution for Angular Apps! \ud83d\ude80 Learn how to supercharge your web applications with RxDB's reactive, offline-first database capabilities. Master real-time data synchronization and build ultra-responsive Angular applications. Click now for expert tips and techniques that will elevate your development game!"},r="RxDB as a Database in an Angular Application",o={id:"articles/angular-database",title:"RxDB as a Database in an Angular Application",description:"Discover the RxDB Revolution for Angular Apps! \ud83d\ude80 Learn how to supercharge your web applications with RxDB's reactive, offline-first database capabilities. Master real-time data synchronization and build ultra-responsive Angular applications. Click now for expert tips and techniques that will elevate your development game!",source:"@site/docs/articles/angular-database.md",sourceDirName:"articles",slug:"/articles/angular-database.html",permalink:"/articles/angular-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDB as a Database in an Angular Application",slug:"angular-database.html",description:"Discover the RxDB Revolution for Angular Apps! \ud83d\ude80 Learn how to supercharge your web applications with RxDB's reactive, offline-first database capabilities. Master real-time data synchronization and build ultra-responsive Angular applications. Click now for expert tips and techniques that will elevate your development game!"},sidebar:"tutorialSidebar",previous:{title:"Alternatives for realtime offline-first JavaScript applications and local databases",permalink:"/alternatives.html"},next:{title:"The benefits of Browser Databases and RxDB",permalink:"/articles/browser-database.html"}},l={},c=[{value:"Angular Web Applications",id:"angular-web-applications",level:2},{value:"Importance of Databases in Angular Applications",id:"importance-of-databases-in-angular-applications",level:2},{value:"Introducing RxDB as a Database Solution",id:"introducing-rxdb-as-a-database-solution",level:2},{value:"Getting Started with RxDB",id:"getting-started-with-rxdb",level:2},{value:"What is RxDB?",id:"what-is-rxdb",level:3},{value:"Reactive Data Handling",id:"reactive-data-handling",level:3},{value:"Offline-First Approach",id:"offline-first-approach",level:3},{value:"Data Replication",id:"data-replication",level:3},{value:"Observable Queries",id:"observable-queries",level:3},{value:"Multi-Tab Support",id:"multi-tab-support",level:3},{value:"RxDB vs. Other Angular Database Options",id:"rxdb-vs-other-angular-database-options",level:3},{value:"Using RxDB in an Angular Application",id:"using-rxdb-in-an-angular-application",level:2},{value:"Installing RxDB in an Angular App",id:"installing-rxdb-in-an-angular-app",level:3},{value:"Patch Change Detection with zone.js",id:"patch-change-detection-with-zonejs",level:3},{value:"Use the Angular async pipe to observe an RxDB Query",id:"use-the-angular-async-pipe-to-observe-an-rxdb-query",level:3},{value:"Different RxStorage layers for RxDB",id:"different-rxstorage-layers-for-rxdb",level:3},{value:"Synchronizing Data with RxDB between Clients and Servers",id:"synchronizing-data-with-rxdb-between-clients-and-servers",level:2},{value:"Offline-First Approach",id:"offline-first-approach-1",level:3},{value:"Conflict Resolution",id:"conflict-resolution",level:3},{value:"Bidirectional Synchronization",id:"bidirectional-synchronization",level:3},{value:"Real-Time Updates",id:"real-time-updates",level:3},{value:"Advanced RxDB Features and Techniques",id:"advanced-rxdb-features-and-techniques",level:2},{value:"Indexing and Performance Optimization",id:"indexing-and-performance-optimization",level:3},{value:"Encryption of Local Data",id:"encryption-of-local-data",level:3},{value:"Change Streams and Event Handling",id:"change-streams-and-event-handling",level:3},{value:"JSON Key Compression",id:"json-key-compression",level:3},{value:"Best Practices for Using RxDB in Angular Applications",id:"best-practices-for-using-rxdb-in-angular-applications",level:2},{value:"Use Async Pipe for Subscriptions so you do not have to unsubscribe",id:"use-async-pipe-for-subscriptions-so-you-do-not-have-to-unsubscribe",level:3},{value:"Use custom reactivity to have signals instead of rxjs observables",id:"use-custom-reactivity-to-have-signals-instead-of-rxjs-observables",level:3},{value:"Use Angular Services for Database creation",id:"use-angular-services-for-database-creation",level:3},{value:"Efficient Data Handling",id:"efficient-data-handling",level:3},{value:"Data Synchronization Strategies",id:"data-synchronization-strategies",level:3},{value:"Conclusion",id:"conclusion",level:2},{value:"Follow Up",id:"follow-up",level:2}];function d(e){const a={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(a.h1,{id:"rxdb-as-a-database-in-an-angular-application",children:"RxDB as a Database in an Angular Application"}),"\n",(0,i.jsxs)(a.p,{children:["In modern web development, Angular has emerged as a popular framework for building robust and scalable applications. As Angular applications often require persistent ",(0,i.jsx)(a.a,{href:"/articles/browser-storage.html",children:"storage"})," and efficient data handling, choosing the right database solution is crucial. One such solution is ",(0,i.jsx)(a.a,{href:"https://rxdb.info/",children:"RxDB"}),", a reactive JavaScript database for the browser, node.js, and ",(0,i.jsx)(a.a,{href:"/articles/mobile-database.html",children:"mobile devices"}),". In this article, we will explore the integration of RxDB into an Angular application and examine its various features and techniques."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("a",{href:"https://rxdb.info/",children:(0,i.jsx)("img",{src:"../files/logo/rxdb_javascript_database.svg",alt:"JavaScript Angular Database",width:"220"})})}),"\n",(0,i.jsx)(a.h2,{id:"angular-web-applications",children:"Angular Web Applications"}),"\n",(0,i.jsx)(a.p,{children:"Angular is a powerful JavaScript framework developed and maintained by Google. It enables developers to build single-page applications (SPAs) with a modular and component-based approach. Angular provides a comprehensive set of tools and features for creating dynamic and responsive web applications."}),"\n",(0,i.jsx)(a.h2,{id:"importance-of-databases-in-angular-applications",children:"Importance of Databases in Angular Applications"}),"\n",(0,i.jsx)(a.p,{children:"Databases play a vital role in Angular applications by providing a structured and efficient way to store, retrieve, and manage data. Whether it's handling user authentication, caching data, or persisting application state, a robust database solution is essential for ensuring optimal performance and user experience."}),"\n",(0,i.jsx)(a.h2,{id:"introducing-rxdb-as-a-database-solution",children:"Introducing RxDB as a Database Solution"}),"\n",(0,i.jsxs)(a.p,{children:["RxDB stands for Reactive Database and is built on the principles of reactive programming. It combines the best features of ",(0,i.jsx)(a.a,{href:"/articles/in-memory-nosql-database.html",children:"NoSQL databases"})," with the power of reactive programming to provide a scalable and efficient database solution. RxDB offers seamless integration with Angular applications and brings several unique features that make it an attractive choice for developers."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("iframe",{width:"560",height:"315",src:"https://www.youtube-nocookie.com/embed/qHWrooWyCYg",title:"RxDB Angular Video",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",allowfullscreen:!0})}),"\n",(0,i.jsx)(a.h2,{id:"getting-started-with-rxdb",children:"Getting Started with RxDB"}),"\n",(0,i.jsx)(a.p,{children:"To begin our journey with RxDB, let's understand its key concepts and features."}),"\n",(0,i.jsx)(a.h3,{id:"what-is-rxdb",children:"What is RxDB?"}),"\n",(0,i.jsxs)(a.p,{children:[(0,i.jsx)(a.a,{href:"https://rxdb.info/",children:"RxDB"})," is a client-side database that follows the principles of reactive programming. It is built on top of IndexedDB, the ",(0,i.jsx)(a.a,{href:"/articles/browser-database.html",children:"native browser database"}),", and leverages the RxJS library for reactive data handling. RxDB provides a simple and intuitive API for managing data and offers features like data replication, multi-tab support, and efficient query handling."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("a",{href:"https://rxdb.info/",children:(0,i.jsx)("img",{src:"../files/logo/rxdb_javascript_database.svg",alt:"JavaScript Angular Database",width:"220"})})}),"\n",(0,i.jsx)(a.h3,{id:"reactive-data-handling",children:"Reactive Data Handling"}),"\n",(0,i.jsx)(a.p,{children:"At the core of RxDB is the concept of reactive data handling. RxDB leverages observables and reactive streams to enable real-time updates and data synchronization. With RxDB, you can easily subscribe to data changes and react to them in a reactive and efficient manner."}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"../files/animations/realtime.gif",alt:"realtime ui updates",width:"700"})}),"\n",(0,i.jsx)(a.h3,{id:"offline-first-approach",children:"Offline-First Approach"}),"\n",(0,i.jsx)(a.p,{children:"One of the standout features of RxDB is its offline-first approach. It allows you to build applications that can work seamlessly in offline scenarios. RxDB stores data locally and automatically synchronizes changes with the server when the network becomes available. This capability is particularly useful for applications that need to function in low-connectivity or unreliable network environments."}),"\n",(0,i.jsx)(a.h3,{id:"data-replication",children:"Data Replication"}),"\n",(0,i.jsx)(a.p,{children:"RxDB provides built-in support for data replication between clients and servers. This means you can synchronize data across multiple devices or instances of your application effortlessly. RxDB handles conflict resolution and ensures that data remains consistent across all connected clients."}),"\n",(0,i.jsx)(a.h3,{id:"observable-queries",children:"Observable Queries"}),"\n",(0,i.jsx)(a.p,{children:"RxDB offers a powerful querying mechanism with support for observable queries. This allows you to create dynamic queries that automatically update when the underlying data changes. By leveraging RxDB's observable queries, you can build reactive UI components that respond to data changes in real-time."}),"\n",(0,i.jsx)(a.h3,{id:"multi-tab-support",children:"Multi-Tab Support"}),"\n",(0,i.jsx)(a.p,{children:"RxDB provides out-of-the-box support for multi-tab scenarios. This means that if your Angular application is running in multiple browser tabs, RxDB automatically keeps the data in sync across all tabs. It ensures that changes made in one tab are immediately reflected in others, providing a seamless user experience."}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"../files/multiwindow.gif",alt:"multi tab support",width:"450"})}),"\n",(0,i.jsx)(a.h3,{id:"rxdb-vs-other-angular-database-options",children:"RxDB vs. Other Angular Database Options"}),"\n",(0,i.jsx)(a.p,{children:"While there are other database options available for Angular applications, RxDB stands out with its reactive programming model, offline-first approach, and built-in synchronization capabilities. Unlike traditional SQL databases, RxDB's NoSQL-like structure and observables-based API make it well-suited for real-time applications and complex data scenarios."}),"\n",(0,i.jsx)(a.h2,{id:"using-rxdb-in-an-angular-application",children:"Using RxDB in an Angular Application"}),"\n",(0,i.jsx)(a.p,{children:"Now that we have a good understanding of RxDB and its features, let's explore how to integrate it into an Angular application."}),"\n",(0,i.jsx)(a.h3,{id:"installing-rxdb-in-an-angular-app",children:"Installing RxDB in an Angular App"}),"\n",(0,i.jsx)(a.p,{children:"To use RxDB in an Angular application, we first need to install the necessary dependencies. You can install RxDB using npm or yarn by running the following command:"}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-bash",children:"npm install rxdb --save\n"})}),"\n",(0,i.jsx)(a.p,{children:"Once installed, you can import RxDB into your Angular application and start using its API to create and manage databases."}),"\n",(0,i.jsx)(a.h3,{id:"patch-change-detection-with-zonejs",children:"Patch Change Detection with zone.js"}),"\n",(0,i.jsx)(a.p,{children:"Angular uses change detection to detect and update UI elements when data changes. However, RxDB's data handling is based on observables, which can sometimes bypass Angular's change detection mechanism. To ensure that changes made in RxDB are detected by Angular, we need to patch the change detection mechanism using zone.js. Zone.js is a library that intercepts and tracks asynchronous operations, including observables. By patching zone.js, we can make sure that Angular is aware of changes happening in RxDB."}),"\n",(0,i.jsxs)(a.admonition,{type:"warning",children:[(0,i.jsxs)(a.p,{children:["RxDB creates rxjs observables outside of angulars zone\nSo you have to import the rxjs patch to ensure the ",(0,i.jsx)(a.a,{href:"https://angular.io/guide/change-detection",children:"angular change detection"})," works correctly.\n",(0,i.jsx)(a.a,{href:"https://www.bennadel.com/blog/3448-binding-rxjs-observable-sources-outside-of-the-ngzone-in-angular-6-0-2.htm",children:"link"})]}),(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"//> app.component.ts\nimport 'zone.js/plugins/zone-patch-rxjs';\n"})})]}),"\n",(0,i.jsx)(a.h3,{id:"use-the-angular-async-pipe-to-observe-an-rxdb-query",children:"Use the Angular async pipe to observe an RxDB Query"}),"\n",(0,i.jsx)(a.p,{children:"Angular provides the async pipe, which is a convenient way to subscribe to observables and handle the subscription lifecycle automatically. When working with RxDB, you can use the async pipe to observe an RxDB query and bind the results directly to your Angular template. This ensures that the UI stays in sync with the data changes emitted by the RxDB query."}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:" constructor(\n private dbService: DatabaseService,\n private dialog: MatDialog\n ) {\n this.heroes$ = this.dbService\n .db.hero // collection\n .find({ // query\n selector: {},\n sort: [{ name: 'asc' }]\n })\n .$;\n }\n"})}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-html",children:'
    \n
  • {{hero.name}}
  • \n
\n'})}),"\n",(0,i.jsx)(a.h3,{id:"different-rxstorage-layers-for-rxdb",children:"Different RxStorage layers for RxDB"}),"\n",(0,i.jsx)(a.p,{children:"RxDB supports multiple storage layers for persisting data. Some of the available storage options include:"}),"\n",(0,i.jsxs)(a.ul,{children:["\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-dexie.html",children:"Dexie.js RxStorage"}),": Dexie.js is a minimalistic IndexedDB wrapper that provides a simple API for working with IndexedDB. RxDB leverages Dexie.js as its default storage layer."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"}),": LokiJS is an in-memory document-oriented database that can also persist data to IndexedDB. RxDB integrates with LokiJS to provide an alternative storage option."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB RxStorage"}),": RxDB directly supports IndexedDB as a storage layer. IndexedDB is a low-level browser database that offers good performance and reliability."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-opfs.html",children:"OPFS RxStorage"}),": The OPFS ",(0,i.jsx)(a.a,{href:"/rx-storage.html",children:"RxStorage"})," for RxDB is built on top of the ",(0,i.jsx)(a.a,{href:"https://webkit.org/blog/12257/the-file-system-access-api-with-origin-private-file-system/",children:"File System Access API"})," which is available in ",(0,i.jsx)(a.a,{href:"https://caniuse.com/native-filesystem-api",children:"all modern browsers"}),". It provides an API to access a sandboxed private file system to persistently store and retrieve data.\nCompared to other persistend storage options in the browser (like ",(0,i.jsx)(a.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB"}),"), the OPFS API has a ",(0,i.jsx)(a.strong,{children:"way better performance"}),"."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-memory.html",children:"Memory RxStorage"}),": In addition to persistent storage options, RxDB also provides a memory-based storage layer. This is useful for testing or scenarios where you don't need long-term data persistence.\nYou can choose the storage layer that best suits your application's requirements and configure RxDB accordingly."]}),"\n"]}),"\n",(0,i.jsx)(a.h2,{id:"synchronizing-data-with-rxdb-between-clients-and-servers",children:"Synchronizing Data with RxDB between Clients and Servers"}),"\n",(0,i.jsx)(a.p,{children:"Data replication between an Angular application and a server is a common requirement. RxDB simplifies this process and provides built-in support for data synchronization. Let's explore how to replicate data between an Angular application and a server using RxDB."}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"../files/database-replication.png",alt:"database replication",width:"200"})}),"\n",(0,i.jsx)(a.h3,{id:"offline-first-approach-1",children:"Offline-First Approach"}),"\n",(0,i.jsxs)(a.p,{children:["One of the key strengths of RxDB is its ",(0,i.jsx)(a.a,{href:"/offline-first.html",children:"offline-first approach"}),". It allows Angular applications to function seamlessly even in offline scenarios. RxDB stores data locally and automatically synchronizes changes with the server when the network becomes available. This capability is particularly useful for applications that need to operate in low-connectivity or unreliable network environments."]}),"\n",(0,i.jsx)(a.h3,{id:"conflict-resolution",children:"Conflict Resolution"}),"\n",(0,i.jsx)(a.p,{children:"In a distributed system, conflicts can arise when multiple clients modify the same data simultaneously. RxDB offers conflict resolution mechanisms to handle such scenarios. You can define conflict resolution strategies based on your application's requirements. RxDB provides hooks and events to detect conflicts and resolve them in a consistent manner."}),"\n",(0,i.jsx)(a.h3,{id:"bidirectional-synchronization",children:"Bidirectional Synchronization"}),"\n",(0,i.jsx)(a.p,{children:"RxDB supports bidirectional data synchronization, allowing updates from both the client and server to be replicated seamlessly. This ensures that data remains consistent across all connected clients and the server. RxDB handles conflicts and resolves them based on the defined conflict resolution strategies."}),"\n",(0,i.jsx)(a.h3,{id:"real-time-updates",children:"Real-Time Updates"}),"\n",(0,i.jsx)(a.p,{children:"RxDB provides real-time updates by leveraging reactive programming principles. Changes made to the data are automatically propagated to all connected clients in real-time. Angular applications can subscribe to these updates and update the user interface accordingly. This real-time capability enables collaborative features and enhances the overall user experience."}),"\n",(0,i.jsx)(a.h2,{id:"advanced-rxdb-features-and-techniques",children:"Advanced RxDB Features and Techniques"}),"\n",(0,i.jsx)(a.p,{children:"RxDB offers several advanced features and techniques that can further enhance your Angular application."}),"\n",(0,i.jsx)(a.h3,{id:"indexing-and-performance-optimization",children:"Indexing and Performance Optimization"}),"\n",(0,i.jsx)(a.p,{children:"To improve query performance, RxDB allows you to define indexes on specific fields of your documents. Indexing enables faster data retrieval and query execution, especially when working with large datasets. By strategically creating indexes, you can optimize the performance of your Angular application."}),"\n",(0,i.jsx)(a.h3,{id:"encryption-of-local-data",children:"Encryption of Local Data"}),"\n",(0,i.jsxs)(a.p,{children:["RxDB provides built-in support for ",(0,i.jsx)(a.a,{href:"/encryption.html",children:"encrypting"})," local data using the Web Crypto API. With encryption, you can protect sensitive data stored in the client-side database. RxDB transparently encrypts the data, ensuring that it remains secure even if the underlying storage is compromised."]}),"\n",(0,i.jsx)(a.h3,{id:"change-streams-and-event-handling",children:"Change Streams and Event Handling"}),"\n",(0,i.jsx)(a.p,{children:"RxDB exposes change streams, which allow you to listen for data changes at a database or collection level. By subscribing to change streams, you can react to data modifications and perform specific actions, such as updating the UI or triggering notifications. Change streams enable real-time event handling in your Angular application."}),"\n",(0,i.jsx)(a.h3,{id:"json-key-compression",children:"JSON Key Compression"}),"\n",(0,i.jsxs)(a.p,{children:["To reduce the storage footprint and improve performance, RxDB supports ",(0,i.jsx)(a.a,{href:"/key-compression.html",children:"JSON key compression"}),". With key compression, RxDB replaces long keys with shorter aliases, reducing the overall storage size. This optimization is particularly useful when working with large datasets or frequently updating data."]}),"\n",(0,i.jsx)(a.h2,{id:"best-practices-for-using-rxdb-in-angular-applications",children:"Best Practices for Using RxDB in Angular Applications"}),"\n",(0,i.jsx)(a.p,{children:"To make the most of RxDB in your Angular application, consider the following best practices:"}),"\n",(0,i.jsx)(a.h3,{id:"use-async-pipe-for-subscriptions-so-you-do-not-have-to-unsubscribe",children:"Use Async Pipe for Subscriptions so you do not have to unsubscribe"}),"\n",(0,i.jsxs)(a.p,{children:["Angular's ",(0,i.jsx)(a.code,{children:"async"})," pipe is a powerful tool for handling observables in templates. By using the async pipe, you can avoid the need to manually subscribe and unsubscribe from RxDB observables. Angular takes care of the subscription lifecycle, ensuring that resources are released when they are no longer needed. Instead of manually subscribing to Observables, you should always prefer the ",(0,i.jsx)(a.code,{children:"async"})," pipe."]}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"// WRONG:\nlet amount;\nthis.dbService\n .db.hero\n .find({\n selector: {},\n sort: [{ name: 'asc' }]\n })\n .$.subscribe(docs => {\n amount = 0;\n docs.forEach(d => amount = d.points);\n });\n\n// RIGHT:\nthis.amount$ = this.dbService\n .db.hero\n .find({\n selector: {},\n sort: [{ name: 'asc' }]\n })\n .$.pipe(\n map(docs => {\n let amount = 0;\n docs.forEach(d => amount = d.points);\n return amount;\n })\n );\n"})}),"\n",(0,i.jsx)(a.h3,{id:"use-custom-reactivity-to-have-signals-instead-of-rxjs-observables",children:"Use custom reactivity to have signals instead of rxjs observables"}),"\n",(0,i.jsxs)(a.p,{children:["RxDB supports adding custom reactivity factories that allow you to get angular signals out of the database instead of rxjs observables. ",(0,i.jsx)(a.a,{href:"/reactivity.html",children:"read more"}),"."]}),"\n",(0,i.jsx)(a.h3,{id:"use-angular-services-for-database-creation",children:"Use Angular Services for Database creation"}),"\n",(0,i.jsx)(a.p,{children:"To ensure proper separation of concerns and maintain a clean codebase, it is recommended to create an Angular service responsible for managing the RxDB database instance. This service can handle database creation, initialization, and provide methods for interacting with the database throughout your application."}),"\n",(0,i.jsx)(a.h3,{id:"efficient-data-handling",children:"Efficient Data Handling"}),"\n",(0,i.jsx)(a.p,{children:"RxDB provides various mechanisms for efficient data handling, such as batching updates, debouncing, and throttling. Leveraging these techniques can help optimize performance and reduce unnecessary UI updates. Consider the specific data handling requirements of your application and choose the appropriate strategies provided by RxDB."}),"\n",(0,i.jsx)(a.h3,{id:"data-synchronization-strategies",children:"Data Synchronization Strategies"}),"\n",(0,i.jsx)(a.p,{children:"When working with data synchronization between clients and servers, it's important to consider strategies for conflict resolution and handling network failures. RxDB provides plugins and hooks that allow you to customize the replication behavior and implement specific synchronization strategies tailored to your application's needs."}),"\n",(0,i.jsx)(a.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,i.jsx)(a.p,{children:"RxDB is a powerful database solution for Angular applications, offering reactive data handling, offline-first capabilities, and seamless data synchronization. By integrating RxDB into your Angular application, you can build responsive and scalable web applications that provide a rich user experience. Whether you're building real-time collaborative apps, progressive web applications, or offline-capable applications, RxDB's features and techniques make it a valuable addition to your Angular development toolkit."}),"\n",(0,i.jsx)(a.h2,{id:"follow-up",children:"Follow Up"}),"\n",(0,i.jsx)(a.p,{children:"To explore more about RxDB and leverage its capabilities for browser database development, check out the following resources:"}),"\n",(0,i.jsxs)(a.ul,{children:["\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"https://github.com/pubkey/rxdb",children:"RxDB GitHub Repository"}),": Visit the official GitHub repository of RxDB to access the source code, documentation, and community support."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/quickstart.html",children:"RxDB Quickstart"}),": Get started quickly with RxDB by following the provided quickstart guide, which provides step-by-step instructions for setting up and using RxDB in your projects."]}),"\n",(0,i.jsx)(a.li,{children:(0,i.jsx)(a.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/angular",children:"RxDB Angular Example at GitHub"})}),"\n"]})]})}function h(e={}){const{wrapper:a}={...(0,t.R)(),...e.components};return a?(0,i.jsx)(a,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8453:(e,a,n)=>{n.d(a,{R:()=>r,x:()=>o});var i=n(6540);const t={},s=i.createContext(t);function r(e){const a=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function o(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),i.createElement(s.Provider,{value:a},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/8aa53ed7.72dccea1.js b/docs/assets/js/8aa53ed7.72dccea1.js deleted file mode 100644 index 89eac2748ca..00000000000 --- a/docs/assets/js/8aa53ed7.72dccea1.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6723],{5870:(e,a,n)=>{n.r(a),n.d(a,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>c});var i=n(4848),t=n(8453);const s={title:"RxDB as a Database in an Angular Application",slug:"angular-database.html",description:"Discover the RxDB Revolution for Angular Apps! \ud83d\ude80 Learn how to supercharge your web applications with RxDB's reactive, offline-first database capabilities. Master real-time data synchronization and build ultra-responsive Angular applications. Click now for expert tips and techniques that will elevate your development game!"},r="RxDB as a Database in an Angular Application",o={id:"articles/angular-database",title:"RxDB as a Database in an Angular Application",description:"Discover the RxDB Revolution for Angular Apps! \ud83d\ude80 Learn how to supercharge your web applications with RxDB's reactive, offline-first database capabilities. Master real-time data synchronization and build ultra-responsive Angular applications. Click now for expert tips and techniques that will elevate your development game!",source:"@site/docs/articles/angular-database.md",sourceDirName:"articles",slug:"/articles/angular-database.html",permalink:"/articles/angular-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDB as a Database in an Angular Application",slug:"angular-database.html",description:"Discover the RxDB Revolution for Angular Apps! \ud83d\ude80 Learn how to supercharge your web applications with RxDB's reactive, offline-first database capabilities. Master real-time data synchronization and build ultra-responsive Angular applications. Click now for expert tips and techniques that will elevate your development game!"},sidebar:"tutorialSidebar",previous:{title:"Alternatives for realtime offline-first JavaScript applications and local databases",permalink:"/alternatives.html"},next:{title:"The benefits of Browser Databases and RxDB",permalink:"/articles/browser-database.html"}},l={},c=[{value:"Angular Web Applications",id:"angular-web-applications",level:2},{value:"Importance of Databases in Angular Applications",id:"importance-of-databases-in-angular-applications",level:2},{value:"Introducing RxDB as a Database Solution",id:"introducing-rxdb-as-a-database-solution",level:2},{value:"Getting Started with RxDB",id:"getting-started-with-rxdb",level:2},{value:"What is RxDB?",id:"what-is-rxdb",level:3},{value:"Reactive Data Handling",id:"reactive-data-handling",level:3},{value:"Offline-First Approach",id:"offline-first-approach",level:3},{value:"Data Replication",id:"data-replication",level:3},{value:"Observable Queries",id:"observable-queries",level:3},{value:"Multi-Tab Support",id:"multi-tab-support",level:3},{value:"RxDB vs. Other Angular Database Options",id:"rxdb-vs-other-angular-database-options",level:3},{value:"Using RxDB in an Angular Application",id:"using-rxdb-in-an-angular-application",level:2},{value:"Installing RxDB in an Angular App",id:"installing-rxdb-in-an-angular-app",level:3},{value:"Patch Change Detection with zone.js",id:"patch-change-detection-with-zonejs",level:3},{value:"Use the Angular async pipe to observe an RxDB Query",id:"use-the-angular-async-pipe-to-observe-an-rxdb-query",level:3},{value:"Different RxStorage layers for RxDB",id:"different-rxstorage-layers-for-rxdb",level:3},{value:"Synchronizing Data with RxDB between Clients and Servers",id:"synchronizing-data-with-rxdb-between-clients-and-servers",level:2},{value:"Offline-First Approach",id:"offline-first-approach-1",level:3},{value:"Conflict Resolution",id:"conflict-resolution",level:3},{value:"Bidirectional Synchronization",id:"bidirectional-synchronization",level:3},{value:"Real-Time Updates",id:"real-time-updates",level:3},{value:"Advanced RxDB Features and Techniques",id:"advanced-rxdb-features-and-techniques",level:2},{value:"Indexing and Performance Optimization",id:"indexing-and-performance-optimization",level:3},{value:"Encryption of Local Data",id:"encryption-of-local-data",level:3},{value:"Change Streams and Event Handling",id:"change-streams-and-event-handling",level:3},{value:"JSON Key Compression",id:"json-key-compression",level:3},{value:"Best Practices for Using RxDB in Angular Applications",id:"best-practices-for-using-rxdb-in-angular-applications",level:2},{value:"Use Async Pipe for Subscriptions so you do not have to unsubscribe",id:"use-async-pipe-for-subscriptions-so-you-do-not-have-to-unsubscribe",level:3},{value:"Use custom reactivity to have signals instead of rxjs observables",id:"use-custom-reactivity-to-have-signals-instead-of-rxjs-observables",level:3},{value:"Use Angular Services for Database creation",id:"use-angular-services-for-database-creation",level:3},{value:"Efficient Data Handling",id:"efficient-data-handling",level:3},{value:"Data Synchronization Strategies",id:"data-synchronization-strategies",level:3},{value:"Conclusion",id:"conclusion",level:2},{value:"Follow Up",id:"follow-up",level:2}];function d(e){const a={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(a.h1,{id:"rxdb-as-a-database-in-an-angular-application",children:"RxDB as a Database in an Angular Application"}),"\n",(0,i.jsxs)(a.p,{children:["In modern web development, Angular has emerged as a popular framework for building robust and scalable applications. As Angular applications often require persistent ",(0,i.jsx)(a.a,{href:"/articles/browser-storage.html",children:"storage"})," and efficient data handling, choosing the right database solution is crucial. One such solution is ",(0,i.jsx)(a.a,{href:"https://rxdb.info/",children:"RxDB"}),", a reactive JavaScript database for the browser, node.js, and ",(0,i.jsx)(a.a,{href:"/articles/mobile-database.html",children:"mobile devices"}),". In this article, we will explore the integration of RxDB into an Angular application and examine its various features and techniques."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("a",{href:"https://rxdb.info/",children:(0,i.jsx)("img",{src:"../files/logo/rxdb_javascript_database.svg",alt:"JavaScript Angular Database",width:"220"})})}),"\n",(0,i.jsx)(a.h2,{id:"angular-web-applications",children:"Angular Web Applications"}),"\n",(0,i.jsx)(a.p,{children:"Angular is a powerful JavaScript framework developed and maintained by Google. It enables developers to build single-page applications (SPAs) with a modular and component-based approach. Angular provides a comprehensive set of tools and features for creating dynamic and responsive web applications."}),"\n",(0,i.jsx)(a.h2,{id:"importance-of-databases-in-angular-applications",children:"Importance of Databases in Angular Applications"}),"\n",(0,i.jsx)(a.p,{children:"Databases play a vital role in Angular applications by providing a structured and efficient way to store, retrieve, and manage data. Whether it's handling user authentication, caching data, or persisting application state, a robust database solution is essential for ensuring optimal performance and user experience."}),"\n",(0,i.jsx)(a.h2,{id:"introducing-rxdb-as-a-database-solution",children:"Introducing RxDB as a Database Solution"}),"\n",(0,i.jsxs)(a.p,{children:["RxDB stands for Reactive Database and is built on the principles of reactive programming. It combines the best features of ",(0,i.jsx)(a.a,{href:"/articles/in-memory-nosql-database.html",children:"NoSQL databases"})," with the power of reactive programming to provide a scalable and efficient database solution. RxDB offers seamless integration with Angular applications and brings several unique features that make it an attractive choice for developers."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("iframe",{width:"560",height:"315",src:"https://www.youtube-nocookie.com/embed/qHWrooWyCYg",title:"RxDB Angular Video",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",allowfullscreen:!0})}),"\n",(0,i.jsx)(a.h2,{id:"getting-started-with-rxdb",children:"Getting Started with RxDB"}),"\n",(0,i.jsx)(a.p,{children:"To begin our journey with RxDB, let's understand its key concepts and features."}),"\n",(0,i.jsx)(a.h3,{id:"what-is-rxdb",children:"What is RxDB?"}),"\n",(0,i.jsxs)(a.p,{children:[(0,i.jsx)(a.a,{href:"https://rxdb.info/",children:"RxDB"})," is a client-side database that follows the principles of reactive programming. It is built on top of IndexedDB, the ",(0,i.jsx)(a.a,{href:"/articles/browser-database.html",children:"native browser database"}),", and leverages the RxJS library for reactive data handling. RxDB provides a simple and intuitive API for managing data and offers features like data replication, multi-tab support, and efficient query handling."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("a",{href:"https://rxdb.info/",children:(0,i.jsx)("img",{src:"../files/logo/rxdb_javascript_database.svg",alt:"JavaScript Angular Database",width:"220"})})}),"\n",(0,i.jsx)(a.h3,{id:"reactive-data-handling",children:"Reactive Data Handling"}),"\n",(0,i.jsx)(a.p,{children:"At the core of RxDB is the concept of reactive data handling. RxDB leverages observables and reactive streams to enable real-time updates and data synchronization. With RxDB, you can easily subscribe to data changes and react to them in a reactive and efficient manner."}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"../files/animations/realtime.gif",alt:"realtime ui updates",width:"700"})}),"\n",(0,i.jsx)(a.h3,{id:"offline-first-approach",children:"Offline-First Approach"}),"\n",(0,i.jsx)(a.p,{children:"One of the standout features of RxDB is its offline-first approach. It allows you to build applications that can work seamlessly in offline scenarios. RxDB stores data locally and automatically synchronizes changes with the server when the network becomes available. This capability is particularly useful for applications that need to function in low-connectivity or unreliable network environments."}),"\n",(0,i.jsx)(a.h3,{id:"data-replication",children:"Data Replication"}),"\n",(0,i.jsx)(a.p,{children:"RxDB provides built-in support for data replication between clients and servers. This means you can synchronize data across multiple devices or instances of your application effortlessly. RxDB handles conflict resolution and ensures that data remains consistent across all connected clients."}),"\n",(0,i.jsx)(a.h3,{id:"observable-queries",children:"Observable Queries"}),"\n",(0,i.jsx)(a.p,{children:"RxDB offers a powerful querying mechanism with support for observable queries. This allows you to create dynamic queries that automatically update when the underlying data changes. By leveraging RxDB's observable queries, you can build reactive UI components that respond to data changes in real-time."}),"\n",(0,i.jsx)(a.h3,{id:"multi-tab-support",children:"Multi-Tab Support"}),"\n",(0,i.jsx)(a.p,{children:"RxDB provides out-of-the-box support for multi-tab scenarios. This means that if your Angular application is running in multiple browser tabs, RxDB automatically keeps the data in sync across all tabs. It ensures that changes made in one tab are immediately reflected in others, providing a seamless user experience."}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"../files/multiwindow.gif",alt:"multi tab support",width:"450"})}),"\n",(0,i.jsx)(a.h3,{id:"rxdb-vs-other-angular-database-options",children:"RxDB vs. Other Angular Database Options"}),"\n",(0,i.jsx)(a.p,{children:"While there are other database options available for Angular applications, RxDB stands out with its reactive programming model, offline-first approach, and built-in synchronization capabilities. Unlike traditional SQL databases, RxDB's NoSQL-like structure and observables-based API make it well-suited for real-time applications and complex data scenarios."}),"\n",(0,i.jsx)(a.h2,{id:"using-rxdb-in-an-angular-application",children:"Using RxDB in an Angular Application"}),"\n",(0,i.jsx)(a.p,{children:"Now that we have a good understanding of RxDB and its features, let's explore how to integrate it into an Angular application."}),"\n",(0,i.jsx)(a.h3,{id:"installing-rxdb-in-an-angular-app",children:"Installing RxDB in an Angular App"}),"\n",(0,i.jsx)(a.p,{children:"To use RxDB in an Angular application, we first need to install the necessary dependencies. You can install RxDB using npm or yarn by running the following command:"}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-bash",children:"npm install rxdb --save\n"})}),"\n",(0,i.jsx)(a.p,{children:"Once installed, you can import RxDB into your Angular application and start using its API to create and manage databases."}),"\n",(0,i.jsx)(a.h3,{id:"patch-change-detection-with-zonejs",children:"Patch Change Detection with zone.js"}),"\n",(0,i.jsx)(a.p,{children:"Angular uses change detection to detect and update UI elements when data changes. However, RxDB's data handling is based on observables, which can sometimes bypass Angular's change detection mechanism. To ensure that changes made in RxDB are detected by Angular, we need to patch the change detection mechanism using zone.js. Zone.js is a library that intercepts and tracks asynchronous operations, including observables. By patching zone.js, we can make sure that Angular is aware of changes happening in RxDB."}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"//> app.component.ts\n/**\n * IMPORTANT: RxDB creates rxjs observables outside of angulars zone\n * So you have to import the rxjs patch to ensure changedetection works correctly.\n * @link https://www.bennadel.com/blog/3448-binding-rxjs-observable-sources-outside-of-the-ngzone-in-angular-6-0-2.htm\n */\nimport 'zone.js/plugins/zone-patch-rxjs';\n"})}),"\n",(0,i.jsx)(a.h3,{id:"use-the-angular-async-pipe-to-observe-an-rxdb-query",children:"Use the Angular async pipe to observe an RxDB Query"}),"\n",(0,i.jsx)(a.p,{children:"Angular provides the async pipe, which is a convenient way to subscribe to observables and handle the subscription lifecycle automatically. When working with RxDB, you can use the async pipe to observe an RxDB query and bind the results directly to your Angular template. This ensures that the UI stays in sync with the data changes emitted by the RxDB query."}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:" constructor(\n private dbService: DatabaseService,\n private dialog: MatDialog\n ) {\n this.heroes$ = this.dbService\n .db.hero // collection\n .find({ // query\n selector: {},\n sort: [{ name: 'asc' }]\n })\n .$;\n }\n"})}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-html",children:'
    \n
  • {{hero.name}}
  • \n
\n'})}),"\n",(0,i.jsx)(a.h3,{id:"different-rxstorage-layers-for-rxdb",children:"Different RxStorage layers for RxDB"}),"\n",(0,i.jsx)(a.p,{children:"RxDB supports multiple storage layers for persisting data. Some of the available storage options include:"}),"\n",(0,i.jsxs)(a.ul,{children:["\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-dexie.html",children:"Dexie.js RxStorage"}),": Dexie.js is a minimalistic IndexedDB wrapper that provides a simple API for working with IndexedDB. RxDB leverages Dexie.js as its default storage layer."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"}),": LokiJS is an in-memory document-oriented database that can also persist data to IndexedDB. RxDB integrates with LokiJS to provide an alternative storage option."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB RxStorage"}),": RxDB directly supports IndexedDB as a storage layer. IndexedDB is a low-level browser database that offers good performance and reliability."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-opfs.html",children:"OPFS RxStorage"}),": The OPFS ",(0,i.jsx)(a.a,{href:"/rx-storage.html",children:"RxStorage"})," for RxDB is built on top of the ",(0,i.jsx)(a.a,{href:"https://webkit.org/blog/12257/the-file-system-access-api-with-origin-private-file-system/",children:"File System Access API"})," which is available in ",(0,i.jsx)(a.a,{href:"https://caniuse.com/native-filesystem-api",children:"all modern browsers"}),". It provides an API to access a sandboxed private file system to persistently store and retrieve data.\nCompared to other persistend storage options in the browser (like ",(0,i.jsx)(a.a,{href:"/rx-storage-indexeddb.html",children:"IndexedDB"}),"), the OPFS API has a ",(0,i.jsx)(a.strong,{children:"way better performance"}),"."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/rx-storage-memory.html",children:"Memory RxStorage"}),": In addition to persistent storage options, RxDB also provides a memory-based storage layer. This is useful for testing or scenarios where you don't need long-term data persistence.\nYou can choose the storage layer that best suits your application's requirements and configure RxDB accordingly."]}),"\n"]}),"\n",(0,i.jsx)(a.h2,{id:"synchronizing-data-with-rxdb-between-clients-and-servers",children:"Synchronizing Data with RxDB between Clients and Servers"}),"\n",(0,i.jsx)(a.p,{children:"Data replication between an Angular application and a server is a common requirement. RxDB simplifies this process and provides built-in support for data synchronization. Let's explore how to replicate data between an Angular application and a server using RxDB."}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"../files/database-replication.png",alt:"database replication",width:"200"})}),"\n",(0,i.jsx)(a.h3,{id:"offline-first-approach-1",children:"Offline-First Approach"}),"\n",(0,i.jsxs)(a.p,{children:["One of the key strengths of RxDB is its ",(0,i.jsx)(a.a,{href:"/offline-first.html",children:"offline-first approach"}),". It allows Angular applications to function seamlessly even in offline scenarios. RxDB stores data locally and automatically synchronizes changes with the server when the network becomes available. This capability is particularly useful for applications that need to operate in low-connectivity or unreliable network environments."]}),"\n",(0,i.jsx)(a.h3,{id:"conflict-resolution",children:"Conflict Resolution"}),"\n",(0,i.jsx)(a.p,{children:"In a distributed system, conflicts can arise when multiple clients modify the same data simultaneously. RxDB offers conflict resolution mechanisms to handle such scenarios. You can define conflict resolution strategies based on your application's requirements. RxDB provides hooks and events to detect conflicts and resolve them in a consistent manner."}),"\n",(0,i.jsx)(a.h3,{id:"bidirectional-synchronization",children:"Bidirectional Synchronization"}),"\n",(0,i.jsx)(a.p,{children:"RxDB supports bidirectional data synchronization, allowing updates from both the client and server to be replicated seamlessly. This ensures that data remains consistent across all connected clients and the server. RxDB handles conflicts and resolves them based on the defined conflict resolution strategies."}),"\n",(0,i.jsx)(a.h3,{id:"real-time-updates",children:"Real-Time Updates"}),"\n",(0,i.jsx)(a.p,{children:"RxDB provides real-time updates by leveraging reactive programming principles. Changes made to the data are automatically propagated to all connected clients in real-time. Angular applications can subscribe to these updates and update the user interface accordingly. This real-time capability enables collaborative features and enhances the overall user experience."}),"\n",(0,i.jsx)(a.h2,{id:"advanced-rxdb-features-and-techniques",children:"Advanced RxDB Features and Techniques"}),"\n",(0,i.jsx)(a.p,{children:"RxDB offers several advanced features and techniques that can further enhance your Angular application."}),"\n",(0,i.jsx)(a.h3,{id:"indexing-and-performance-optimization",children:"Indexing and Performance Optimization"}),"\n",(0,i.jsx)(a.p,{children:"To improve query performance, RxDB allows you to define indexes on specific fields of your documents. Indexing enables faster data retrieval and query execution, especially when working with large datasets. By strategically creating indexes, you can optimize the performance of your Angular application."}),"\n",(0,i.jsx)(a.h3,{id:"encryption-of-local-data",children:"Encryption of Local Data"}),"\n",(0,i.jsxs)(a.p,{children:["RxDB provides built-in support for ",(0,i.jsx)(a.a,{href:"/encryption.html",children:"encrypting"})," local data using the Web Crypto API. With encryption, you can protect sensitive data stored in the client-side database. RxDB transparently encrypts the data, ensuring that it remains secure even if the underlying storage is compromised."]}),"\n",(0,i.jsx)(a.h3,{id:"change-streams-and-event-handling",children:"Change Streams and Event Handling"}),"\n",(0,i.jsx)(a.p,{children:"RxDB exposes change streams, which allow you to listen for data changes at a database or collection level. By subscribing to change streams, you can react to data modifications and perform specific actions, such as updating the UI or triggering notifications. Change streams enable real-time event handling in your Angular application."}),"\n",(0,i.jsx)(a.h3,{id:"json-key-compression",children:"JSON Key Compression"}),"\n",(0,i.jsxs)(a.p,{children:["To reduce the storage footprint and improve performance, RxDB supports ",(0,i.jsx)(a.a,{href:"/key-compression.html",children:"JSON key compression"}),". With key compression, RxDB replaces long keys with shorter aliases, reducing the overall storage size. This optimization is particularly useful when working with large datasets or frequently updating data."]}),"\n",(0,i.jsx)(a.h2,{id:"best-practices-for-using-rxdb-in-angular-applications",children:"Best Practices for Using RxDB in Angular Applications"}),"\n",(0,i.jsx)(a.p,{children:"To make the most of RxDB in your Angular application, consider the following best practices:"}),"\n",(0,i.jsx)(a.h3,{id:"use-async-pipe-for-subscriptions-so-you-do-not-have-to-unsubscribe",children:"Use Async Pipe for Subscriptions so you do not have to unsubscribe"}),"\n",(0,i.jsxs)(a.p,{children:["Angular's ",(0,i.jsx)(a.code,{children:"async"})," pipe is a powerful tool for handling observables in templates. By using the async pipe, you can avoid the need to manually subscribe and unsubscribe from RxDB observables. Angular takes care of the subscription lifecycle, ensuring that resources are released when they are no longer needed. Instead of manually subscribing to Observables, you should always prefer the ",(0,i.jsx)(a.code,{children:"async"})," pipe."]}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"// WRONG:\nlet amount;\nthis.dbService\n .db.hero\n .find({\n selector: {},\n sort: [{ name: 'asc' }]\n })\n .$.subscribe(docs => {\n amount = 0;\n docs.forEach(d => amount = d.points);\n });\n\n// RIGHT:\nthis.amount$ = this.dbService\n .db.hero\n .find({\n selector: {},\n sort: [{ name: 'asc' }]\n })\n .$.pipe(\n map(docs => {\n let amount = 0;\n docs.forEach(d => amount = d.points);\n return amount;\n })\n );\n"})}),"\n",(0,i.jsx)(a.h3,{id:"use-custom-reactivity-to-have-signals-instead-of-rxjs-observables",children:"Use custom reactivity to have signals instead of rxjs observables"}),"\n",(0,i.jsxs)(a.p,{children:["RxDB supports adding custom reactivity factories that allow you to get angular signals out of the database instead of rxjs observables. ",(0,i.jsx)(a.a,{href:"/reactivity.html",children:"read more"}),"."]}),"\n",(0,i.jsx)(a.h3,{id:"use-angular-services-for-database-creation",children:"Use Angular Services for Database creation"}),"\n",(0,i.jsx)(a.p,{children:"To ensure proper separation of concerns and maintain a clean codebase, it is recommended to create an Angular service responsible for managing the RxDB database instance. This service can handle database creation, initialization, and provide methods for interacting with the database throughout your application."}),"\n",(0,i.jsx)(a.h3,{id:"efficient-data-handling",children:"Efficient Data Handling"}),"\n",(0,i.jsx)(a.p,{children:"RxDB provides various mechanisms for efficient data handling, such as batching updates, debouncing, and throttling. Leveraging these techniques can help optimize performance and reduce unnecessary UI updates. Consider the specific data handling requirements of your application and choose the appropriate strategies provided by RxDB."}),"\n",(0,i.jsx)(a.h3,{id:"data-synchronization-strategies",children:"Data Synchronization Strategies"}),"\n",(0,i.jsx)(a.p,{children:"When working with data synchronization between clients and servers, it's important to consider strategies for conflict resolution and handling network failures. RxDB provides plugins and hooks that allow you to customize the replication behavior and implement specific synchronization strategies tailored to your application's needs."}),"\n",(0,i.jsx)(a.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,i.jsx)(a.p,{children:"RxDB is a powerful database solution for Angular applications, offering reactive data handling, offline-first capabilities, and seamless data synchronization. By integrating RxDB into your Angular application, you can build responsive and scalable web applications that provide a rich user experience. Whether you're building real-time collaborative apps, progressive web applications, or offline-capable applications, RxDB's features and techniques make it a valuable addition to your Angular development toolkit."}),"\n",(0,i.jsx)(a.h2,{id:"follow-up",children:"Follow Up"}),"\n",(0,i.jsx)(a.p,{children:"To explore more about RxDB and leverage its capabilities for browser database development, check out the following resources:"}),"\n",(0,i.jsxs)(a.ul,{children:["\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"https://github.com/pubkey/rxdb",children:"RxDB GitHub Repository"}),": Visit the official GitHub repository of RxDB to access the source code, documentation, and community support."]}),"\n",(0,i.jsxs)(a.li,{children:[(0,i.jsx)(a.a,{href:"/quickstart.html",children:"RxDB Quickstart"}),": Get started quickly with RxDB by following the provided quickstart guide, which provides step-by-step instructions for setting up and using RxDB in your projects."]}),"\n",(0,i.jsx)(a.li,{children:(0,i.jsx)(a.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/angular",children:"RxDB Angular Example at GitHub"})}),"\n"]})]})}function h(e={}){const{wrapper:a}={...(0,t.R)(),...e.components};return a?(0,i.jsx)(a,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8453:(e,a,n)=>{n.d(a,{R:()=>r,x:()=>o});var i=n(6540);const t={},s=i.createContext(t);function r(e){const a=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function o(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),i.createElement(s.Provider,{value:a},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/8bc07e20.52a1c000.js b/docs/assets/js/8bc07e20.52a1c000.js deleted file mode 100644 index 4367384e383..00000000000 --- a/docs/assets/js/8bc07e20.52a1c000.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[1850],{5054:(e,a,t)=>{t.r(a),t.d(a,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>l});var i=t(4848),n=t(8453);const s={title:"Capacitor Database - SQLite, RxDB and others",slug:"capacitor-database.html",description:"Explore Capacitor's database options including SQLite, RxDB, and Preferences API for web-native apps. Learn about their uses, limitations, and integration in this comprehensive guide."},r="Capacitor Database - SQLite, RxDB and others",o={id:"capacitor-database",title:"Capacitor Database - SQLite, RxDB and others",description:"Explore Capacitor's database options including SQLite, RxDB, and Preferences API for web-native apps. Learn about their uses, limitations, and integration in this comprehensive guide.",source:"@site/docs/capacitor-database.md",sourceDirName:".",slug:"/capacitor-database.html",permalink:"/capacitor-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Capacitor Database - SQLite, RxDB and others",slug:"capacitor-database.html",description:"Explore Capacitor's database options including SQLite, RxDB, and Preferences API for web-native apps. Learn about their uses, limitations, and integration in this comprehensive guide."},sidebar:"tutorialSidebar",previous:{title:"What is a realtime database?",permalink:"/articles/realtime-database.html"},next:{title:"Electron Database - Storage adapters for SQLite, Filesystem and In-Memory",permalink:"/electron-database.html"}},c={},l=[{value:"Database Solutions for Capacitor",id:"database-solutions-for-capacitor",level:2},{value:"Preferences API",id:"preferences-api",level:3},{value:"Localstorage/IndexedDB/WebSQL",id:"localstorageindexeddbwebsql",level:3},{value:"SQLite",id:"sqlite",level:3},{value:"RxDB",id:"rxdb",level:3},{value:"Follow up",id:"follow-up",level:2}];function d(e){const a={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,n.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(a.h1,{id:"capacitor-database---sqlite-rxdb-and-others",children:"Capacitor Database - SQLite, RxDB and others"}),"\n",(0,i.jsxs)(a.p,{children:[(0,i.jsx)(a.a,{href:"https://capacitorjs.com/",children:"Capacitor"})," is an open source native JavaScript runtime to build Web based Native apps. You can use it to create cross-platform iOS, Android, and Progressive Web Apps with the web technologies JavaScript, HTML, and CSS.\nIt is developed by the Ionic Team and provides a great alternative to create hybrid apps. Compared to ",(0,i.jsx)(a.a,{href:"/react-native-database.html",children:"React Native"}),", Capacitor is more Web-Like because the JavaScript runtime supports most Web APIs like IndexedDB, fetch, and so on."]}),"\n",(0,i.jsx)(a.p,{children:"To read and write persistent data in Capacitor, there are multiple solutions which are shown in the following."}),"\n",(0,i.jsxs)(a.p,{children:[(0,i.jsx)(a.strong,{children:"NOTICE:"})," You are reading this inside of the ",(0,i.jsx)(a.a,{href:"https://rxdb.info/",children:"RxDB"})," documentation, so everything might be opinionated."]}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/icons/capacitor.svg",alt:"Capacitor",width:"50"})}),"\n",(0,i.jsx)(a.h2,{id:"database-solutions-for-capacitor",children:"Database Solutions for Capacitor"}),"\n",(0,i.jsx)(a.h3,{id:"preferences-api",children:"Preferences API"}),"\n",(0,i.jsxs)(a.p,{children:["Capacitor comes with a native ",(0,i.jsx)(a.a,{href:"https://capacitorjs.com/docs/apis/preferences",children:"Preferences API"})," which is a simple, persistent key->value store for lightweight data, similar to the browsers localstorage or React Native ",(0,i.jsx)(a.a,{href:"/react-native-database.html#asyncstorage",children:"AsyncStorage"}),"."]}),"\n",(0,i.jsxs)(a.p,{children:["To use it, you first have to install it from npm ",(0,i.jsx)(a.code,{children:"npm install @capacitor/preferences"})," and then you can import it and write/read data.\nNotice that all calls to the preferences API are asynchronous so they return a ",(0,i.jsx)(a.code,{children:"Promise"})," that must be ",(0,i.jsx)(a.code,{children:"await"}),"-ed."]}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"import { Preferences } from '@capacitor/preferences';\n\n\n// write\nawait Preferences.set({\n key: 'foo',\n value: 'baar',\n});\n\n// read\nconst { value } = await Preferences.get({ key: 'foo' }); // > 'bar'\n\n// delete\nawait Preferences.remove({ key: 'foo' });\n"})}),"\n",(0,i.jsx)(a.p,{children:"The preferences API is good when only a small amount of data needs to be stored and when no query capabilities besides the key access are required. Complex queries or other features like indexes or replication are not supported which makes the preferences API not suitable for anything more then storing simple data like user settings."}),"\n",(0,i.jsx)(a.h3,{id:"localstorageindexeddbwebsql",children:"Localstorage/IndexedDB/WebSQL"}),"\n",(0,i.jsxs)(a.p,{children:["Since Capacitor apps run in a web view, Web APIs like IndexedDB, ",(0,i.jsx)(a.a,{href:"/articles/localstorage.html",children:"Localstorage"})," and WebSQL are available. But the default browser behavior is to clean up these storages regularly when they are not in use for a long time or the device is low on space. Therefore you cannot 100% rely on the persistence of the stored data and your application needs to expect that the data will be lost eventually."]}),"\n",(0,i.jsx)(a.p,{children:"Storing data in these storages can be done in browsers, because there is no other option. But in Capacitor iOS and Android, you should not rely on these."}),"\n",(0,i.jsx)(a.h3,{id:"sqlite",children:"SQLite"}),"\n",(0,i.jsx)(a.p,{children:"SQLite is a SQL based relational database written in C that was crafted to be embed inside of applications. Operations are written in the SQL query language and SQLite generally follows the PostgreSQL syntax."}),"\n",(0,i.jsx)(a.p,{children:"To use SQLite in Capacitor, there are three options:"}),"\n",(0,i.jsxs)(a.ul,{children:["\n",(0,i.jsxs)(a.li,{children:["The ",(0,i.jsx)(a.a,{href:"https://github.com/capacitor-community/sqlite",children:"@capacitor-community/sqlite"})," package"]}),"\n",(0,i.jsxs)(a.li,{children:["The ",(0,i.jsx)(a.a,{href:"https://github.com/storesafe/cordova-sqlite-storage",children:"cordova-sqlite-storage"})," package"]}),"\n",(0,i.jsxs)(a.li,{children:["The non-free ",(0,i.jsx)(a.a,{href:"/articles/ionic-database.html",children:"Ionic"})," ",(0,i.jsx)(a.a,{href:"https://ionic.io/products/secure-storage",children:"Secure Storage"})," which comes at ",(0,i.jsx)(a.strong,{children:"999$"})," per month."]}),"\n"]}),"\n",(0,i.jsxs)(a.p,{children:["It is recommended to use the ",(0,i.jsx)(a.code,{children:"@capacitor-community/sqlite"})," because it has the best maintenance and is open source. Install it first ",(0,i.jsx)(a.code,{children:"npm install --save @capacitor-community/sqlite"})," and then set the storage location for iOS apps:"]}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-json",children:'{\n "plugins": {\n "CapacitorSQLite": {\n "iosDatabaseLocation": "Library/CapacitorDatabase"\n }\n }\n}\n'})}),"\n",(0,i.jsx)(a.p,{children:"Now you can create a database connection and use the SQLite database."}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"import { Capacitor } from '@capacitor/core';\nimport {\n CapacitorSQLite, SQLiteDBConnection, SQLiteConnection, capSQLiteSet,\n capSQLiteChanges, capSQLiteValues, capEchoResult, capSQLiteResult,\n capNCDatabasePathResult\n} from '@capacitor-community/sqlite';\n\nconst sqlite = new SQLiteConnection(CapacitorSQLite);\nconst database: SQLiteDBConnection = await this.sqlite.createConnection(databaseName, encrypted, mode, version, readOnly);\nlet { rows } = database.query('SELECT somevalue FROM sometable');\n"})}),"\n",(0,i.jsx)(a.p,{children:"The downside of SQLite is that it is lacking many features that are handful when using a database together with an UI based application like your Capacitor app. For example it is not possible to observe queries or document fields. Also there is no realtime replication feature, you can only import json files. This makes SQLite a good solution when you just want to store data on the client, but when you want to sync data with a server or other clients or create big complex realtime applications, you have to use something else."}),"\n",(0,i.jsx)(a.h3,{id:"rxdb",children:"RxDB"}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/logo/rxdb_javascript_database.svg",alt:"RxDB",width:"170"})}),"\n",(0,i.jsxs)(a.p,{children:[(0,i.jsx)(a.a,{href:"https://rxdb.info/",children:"RxDB"})," is an local first, NoSQL database for JavaScript Applications like hybrid apps. Because it is reactive, you can subscribe to all state changes like the result of a query or even a single field of a document. This is great for UI-based realtime applications in a way that makes it easy to develop realtime applications like what you need in Capacitor."]}),"\n",(0,i.jsxs)(a.p,{children:["Because RxDB is made for Web applications, most of the ",(0,i.jsx)(a.a,{href:"/rx-storage.html",children:"available RxStorage"})," plugins can be used to store and query data in a Capacitor app. However it is recommended to use the ",(0,i.jsx)(a.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"})," because it stores the data on the filesystem of the device, not in the JavaScript runtime (like IndexedDB). Storing data on the filesystem ensures it is persistent and will not be cleaned up by any process. Also the performance of SQLite is ",(0,i.jsx)(a.a,{href:"/rx-storage.html#performance-comparison",children:"much faster"})," compared to IndexedDB, because SQLite does not have to go through a browsers permission layers. For the SQLite binding you should use the ",(0,i.jsx)(a.a,{href:"https://github.com/capacitor-community/sqlite",children:"@capacitor-community/sqlite"})," package."]}),"\n",(0,i.jsxs)(a.p,{children:["Because the SQLite RxStorage is part of the ",(0,i.jsx)(a.a,{href:"/premium",children:"\ud83d\udc51 Premium Plugins"})," which must be purchased, it is recommended to use the ",(0,i.jsx)(a.a,{href:"/rx-storage-dexie.html",children:"Dexie.js RxStorage"})," while testing and prototyping your Capacitor app."]}),"\n",(0,i.jsxs)(a.p,{children:["To use the SQLite RxStorage in Capacitor you have to install all dependencies via ",(0,i.jsx)(a.code,{children:"npm install rxdb rxjs rxdb-premium @capacitor-community/sqlite"}),"."]}),"\n",(0,i.jsx)(a.p,{children:"For iOS apps you should add a database location in your Capacitor settings:"}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-json",children:'{\n "plugins": {\n "CapacitorSQLite": {\n "iosDatabaseLocation": "Library/CapacitorDatabase"\n }\n }\n}\n'})}),"\n",(0,i.jsx)(a.p,{children:"Then you can assemble the RxStorage and create a database with it:"}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport {\n getRxStorageSQLite,\n getSQLiteBasicsCapacitor\n} from 'rxdb-premium/plugins/storage-sqlite';\nimport {\n CapacitorSQLite,\n SQLiteConnection\n} from '@capacitor-community/sqlite';\nimport { Capacitor } from '@capacitor/core';\nconst sqlite = new SQLiteConnection(CapacitorSQLite);\n\n// create database\nconst myRxDatabase = await createRxDatabase({\n name: 'exampledb',\n storage: getRxStorageSQLite({\n sqliteBasics: getSQLiteBasicsCapacitor(sqlite, Capacitor)\n })\n});\n\n// create collections\nconst collections = await myRxDatabase.addCollections({\n humans: {\n /* ... */\n }\n});\n\n// insert document\nawait collections.humans.insert({id: 'foo', name: 'bar'});\n\n// run a query\nconst result = await collections.humans.find({\n selector: {\n name: 'bar'\n }\n}).exec();\n\n// observe a query\nawait collections.humans.find({\n selector: {\n name: 'bar'\n }\n}).$.subscribe(result => {/* ... */});\n"})}),"\n",(0,i.jsx)(a.h2,{id:"follow-up",children:"Follow up"}),"\n",(0,i.jsxs)(a.ul,{children:["\n",(0,i.jsxs)(a.li,{children:["If you haven't done yet, you should start learning about RxDB with the ",(0,i.jsx)(a.a,{href:"/quickstart.html",children:"Quickstart Tutorial"}),"."]}),"\n",(0,i.jsxs)(a.li,{children:["There is a followup list of other ",(0,i.jsx)(a.a,{href:"/alternatives.html",children:"client side database alternatives"}),"."]}),"\n"]})]})}function h(e={}){const{wrapper:a}={...(0,n.R)(),...e.components};return a?(0,i.jsx)(a,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8453:(e,a,t)=>{t.d(a,{R:()=>r,x:()=>o});var i=t(6540);const n={},s=i.createContext(n);function r(e){const a=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function o(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:r(e.components),i.createElement(s.Provider,{value:a},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/8bc07e20.cab960e1.js b/docs/assets/js/8bc07e20.cab960e1.js new file mode 100644 index 00000000000..5f5cc59d020 --- /dev/null +++ b/docs/assets/js/8bc07e20.cab960e1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[1850],{5054:(e,a,t)=>{t.r(a),t.d(a,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>l});var i=t(4848),n=t(8453);const s={title:"Capacitor Database - SQLite, RxDB and others",slug:"capacitor-database.html",description:"Explore Capacitor's database options including SQLite, RxDB, and Preferences API for web-native apps. Learn about their uses, limitations, and integration in this comprehensive guide."},r="Capacitor Database - SQLite, RxDB and others",o={id:"capacitor-database",title:"Capacitor Database - SQLite, RxDB and others",description:"Explore Capacitor's database options including SQLite, RxDB, and Preferences API for web-native apps. Learn about their uses, limitations, and integration in this comprehensive guide.",source:"@site/docs/capacitor-database.md",sourceDirName:".",slug:"/capacitor-database.html",permalink:"/capacitor-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Capacitor Database - SQLite, RxDB and others",slug:"capacitor-database.html",description:"Explore Capacitor's database options including SQLite, RxDB, and Preferences API for web-native apps. Learn about their uses, limitations, and integration in this comprehensive guide."},sidebar:"tutorialSidebar",previous:{title:"What is a realtime database?",permalink:"/articles/realtime-database.html"},next:{title:"Electron Database - Storage adapters for SQLite, Filesystem and In-Memory",permalink:"/electron-database.html"}},c={},l=[{value:"Database Solutions for Capacitor",id:"database-solutions-for-capacitor",level:2},{value:"Preferences API",id:"preferences-api",level:3},{value:"Localstorage/IndexedDB/WebSQL",id:"localstorageindexeddbwebsql",level:3},{value:"SQLite",id:"sqlite",level:3},{value:"RxDB",id:"rxdb",level:3},{value:"Follow up",id:"follow-up",level:2}];function d(e){const a={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,n.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(a.h1,{id:"capacitor-database---sqlite-rxdb-and-others",children:"Capacitor Database - SQLite, RxDB and others"}),"\n",(0,i.jsxs)(a.p,{children:[(0,i.jsx)(a.a,{href:"https://capacitorjs.com/",children:"Capacitor"})," is an open source native JavaScript runtime to build Web based Native apps. You can use it to create cross-platform iOS, Android, and Progressive Web Apps with the web technologies JavaScript, HTML, and CSS.\nIt is developed by the Ionic Team and provides a great alternative to create hybrid apps. Compared to ",(0,i.jsx)(a.a,{href:"/react-native-database.html",children:"React Native"}),", Capacitor is more Web-Like because the JavaScript runtime supports most Web APIs like IndexedDB, fetch, and so on."]}),"\n",(0,i.jsx)(a.p,{children:"To read and write persistent data in Capacitor, there are multiple solutions which are shown in the following."}),"\n",(0,i.jsx)(a.admonition,{type:"note",children:(0,i.jsxs)(a.p,{children:["You are reading this inside of the ",(0,i.jsx)(a.a,{href:"https://rxdb.info/",children:"RxDB"})," documentation, so everything might be opinionated."]})}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/icons/capacitor.svg",alt:"Capacitor",width:"50"})}),"\n",(0,i.jsx)(a.h2,{id:"database-solutions-for-capacitor",children:"Database Solutions for Capacitor"}),"\n",(0,i.jsx)(a.h3,{id:"preferences-api",children:"Preferences API"}),"\n",(0,i.jsxs)(a.p,{children:["Capacitor comes with a native ",(0,i.jsx)(a.a,{href:"https://capacitorjs.com/docs/apis/preferences",children:"Preferences API"})," which is a simple, persistent key->value store for lightweight data, similar to the browsers localstorage or React Native ",(0,i.jsx)(a.a,{href:"/react-native-database.html#asyncstorage",children:"AsyncStorage"}),"."]}),"\n",(0,i.jsxs)(a.p,{children:["To use it, you first have to install it from npm ",(0,i.jsx)(a.code,{children:"npm install @capacitor/preferences"})," and then you can import it and write/read data.\nNotice that all calls to the preferences API are asynchronous so they return a ",(0,i.jsx)(a.code,{children:"Promise"})," that must be ",(0,i.jsx)(a.code,{children:"await"}),"-ed."]}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"import { Preferences } from '@capacitor/preferences';\n\n\n// write\nawait Preferences.set({\n key: 'foo',\n value: 'baar',\n});\n\n// read\nconst { value } = await Preferences.get({ key: 'foo' }); // > 'bar'\n\n// delete\nawait Preferences.remove({ key: 'foo' });\n"})}),"\n",(0,i.jsx)(a.p,{children:"The preferences API is good when only a small amount of data needs to be stored and when no query capabilities besides the key access are required. Complex queries or other features like indexes or replication are not supported which makes the preferences API not suitable for anything more then storing simple data like user settings."}),"\n",(0,i.jsx)(a.h3,{id:"localstorageindexeddbwebsql",children:"Localstorage/IndexedDB/WebSQL"}),"\n",(0,i.jsxs)(a.p,{children:["Since Capacitor apps run in a web view, Web APIs like IndexedDB, ",(0,i.jsx)(a.a,{href:"/articles/localstorage.html",children:"Localstorage"})," and WebSQL are available. But the default browser behavior is to clean up these storages regularly when they are not in use for a long time or the device is low on space. Therefore you cannot 100% rely on the persistence of the stored data and your application needs to expect that the data will be lost eventually."]}),"\n",(0,i.jsx)(a.p,{children:"Storing data in these storages can be done in browsers, because there is no other option. But in Capacitor iOS and Android, you should not rely on these."}),"\n",(0,i.jsx)(a.h3,{id:"sqlite",children:"SQLite"}),"\n",(0,i.jsx)(a.p,{children:"SQLite is a SQL based relational database written in C that was crafted to be embed inside of applications. Operations are written in the SQL query language and SQLite generally follows the PostgreSQL syntax."}),"\n",(0,i.jsx)(a.p,{children:"To use SQLite in Capacitor, there are three options:"}),"\n",(0,i.jsxs)(a.ul,{children:["\n",(0,i.jsxs)(a.li,{children:["The ",(0,i.jsx)(a.a,{href:"https://github.com/capacitor-community/sqlite",children:"@capacitor-community/sqlite"})," package"]}),"\n",(0,i.jsxs)(a.li,{children:["The ",(0,i.jsx)(a.a,{href:"https://github.com/storesafe/cordova-sqlite-storage",children:"cordova-sqlite-storage"})," package"]}),"\n",(0,i.jsxs)(a.li,{children:["The non-free ",(0,i.jsx)(a.a,{href:"/articles/ionic-database.html",children:"Ionic"})," ",(0,i.jsx)(a.a,{href:"https://ionic.io/products/secure-storage",children:"Secure Storage"})," which comes at ",(0,i.jsx)(a.strong,{children:"999$"})," per month."]}),"\n"]}),"\n",(0,i.jsxs)(a.p,{children:["It is recommended to use the ",(0,i.jsx)(a.code,{children:"@capacitor-community/sqlite"})," because it has the best maintenance and is open source. Install it first ",(0,i.jsx)(a.code,{children:"npm install --save @capacitor-community/sqlite"})," and then set the storage location for iOS apps:"]}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-json",children:'{\n "plugins": {\n "CapacitorSQLite": {\n "iosDatabaseLocation": "Library/CapacitorDatabase"\n }\n }\n}\n'})}),"\n",(0,i.jsx)(a.p,{children:"Now you can create a database connection and use the SQLite database."}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"import { Capacitor } from '@capacitor/core';\nimport {\n CapacitorSQLite, SQLiteDBConnection, SQLiteConnection, capSQLiteSet,\n capSQLiteChanges, capSQLiteValues, capEchoResult, capSQLiteResult,\n capNCDatabasePathResult\n} from '@capacitor-community/sqlite';\n\nconst sqlite = new SQLiteConnection(CapacitorSQLite);\nconst database: SQLiteDBConnection = await this.sqlite.createConnection(databaseName, encrypted, mode, version, readOnly);\nlet { rows } = database.query('SELECT somevalue FROM sometable');\n"})}),"\n",(0,i.jsx)(a.p,{children:"The downside of SQLite is that it is lacking many features that are handful when using a database together with an UI based application like your Capacitor app. For example it is not possible to observe queries or document fields. Also there is no realtime replication feature, you can only import json files. This makes SQLite a good solution when you just want to store data on the client, but when you want to sync data with a server or other clients or create big complex realtime applications, you have to use something else."}),"\n",(0,i.jsx)(a.h3,{id:"rxdb",children:"RxDB"}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/logo/rxdb_javascript_database.svg",alt:"RxDB",width:"170"})}),"\n",(0,i.jsxs)(a.p,{children:[(0,i.jsx)(a.a,{href:"https://rxdb.info/",children:"RxDB"})," is an local first, NoSQL database for JavaScript Applications like hybrid apps. Because it is reactive, you can subscribe to all state changes like the result of a query or even a single field of a document. This is great for UI-based realtime applications in a way that makes it easy to develop realtime applications like what you need in Capacitor."]}),"\n",(0,i.jsxs)(a.p,{children:["Because RxDB is made for Web applications, most of the ",(0,i.jsx)(a.a,{href:"/rx-storage.html",children:"available RxStorage"})," plugins can be used to store and query data in a Capacitor app. However it is recommended to use the ",(0,i.jsx)(a.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"})," because it stores the data on the filesystem of the device, not in the JavaScript runtime (like IndexedDB). Storing data on the filesystem ensures it is persistent and will not be cleaned up by any process. Also the performance of SQLite is ",(0,i.jsx)(a.a,{href:"/rx-storage.html#performance-comparison",children:"much faster"})," compared to IndexedDB, because SQLite does not have to go through a browsers permission layers. For the SQLite binding you should use the ",(0,i.jsx)(a.a,{href:"https://github.com/capacitor-community/sqlite",children:"@capacitor-community/sqlite"})," package."]}),"\n",(0,i.jsxs)(a.p,{children:["Because the SQLite RxStorage is part of the ",(0,i.jsx)(a.a,{href:"/premium",children:"\ud83d\udc51 Premium Plugins"})," which must be purchased, it is recommended to use the ",(0,i.jsx)(a.a,{href:"/rx-storage-dexie.html",children:"Dexie.js RxStorage"})," while testing and prototyping your Capacitor app."]}),"\n",(0,i.jsxs)(a.p,{children:["To use the SQLite RxStorage in Capacitor you have to install all dependencies via ",(0,i.jsx)(a.code,{children:"npm install rxdb rxjs rxdb-premium @capacitor-community/sqlite"}),"."]}),"\n",(0,i.jsx)(a.p,{children:"For iOS apps you should add a database location in your Capacitor settings:"}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-json",children:'{\n "plugins": {\n "CapacitorSQLite": {\n "iosDatabaseLocation": "Library/CapacitorDatabase"\n }\n }\n}\n'})}),"\n",(0,i.jsx)(a.p,{children:"Then you can assemble the RxStorage and create a database with it:"}),"\n",(0,i.jsx)(a.pre,{children:(0,i.jsx)(a.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport {\n getRxStorageSQLite,\n getSQLiteBasicsCapacitor\n} from 'rxdb-premium/plugins/storage-sqlite';\nimport {\n CapacitorSQLite,\n SQLiteConnection\n} from '@capacitor-community/sqlite';\nimport { Capacitor } from '@capacitor/core';\nconst sqlite = new SQLiteConnection(CapacitorSQLite);\n\n// create database\nconst myRxDatabase = await createRxDatabase({\n name: 'exampledb',\n storage: getRxStorageSQLite({\n sqliteBasics: getSQLiteBasicsCapacitor(sqlite, Capacitor)\n })\n});\n\n// create collections\nconst collections = await myRxDatabase.addCollections({\n humans: {\n /* ... */\n }\n});\n\n// insert document\nawait collections.humans.insert({id: 'foo', name: 'bar'});\n\n// run a query\nconst result = await collections.humans.find({\n selector: {\n name: 'bar'\n }\n}).exec();\n\n// observe a query\nawait collections.humans.find({\n selector: {\n name: 'bar'\n }\n}).$.subscribe(result => {/* ... */});\n"})}),"\n",(0,i.jsx)(a.h2,{id:"follow-up",children:"Follow up"}),"\n",(0,i.jsxs)(a.ul,{children:["\n",(0,i.jsxs)(a.li,{children:["If you haven't done yet, you should start learning about RxDB with the ",(0,i.jsx)(a.a,{href:"/quickstart.html",children:"Quickstart Tutorial"}),"."]}),"\n",(0,i.jsxs)(a.li,{children:["There is a followup list of other ",(0,i.jsx)(a.a,{href:"/alternatives.html",children:"client side database alternatives"}),"."]}),"\n"]})]})}function h(e={}){const{wrapper:a}={...(0,n.R)(),...e.components};return a?(0,i.jsx)(a,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8453:(e,a,t)=>{t.d(a,{R:()=>r,x:()=>o});var i=t(6540);const n={},s=i.createContext(n);function r(e){const a=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function o(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:r(e.components),i.createElement(s.Provider,{value:a},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/91b454ee.3a25b812.js b/docs/assets/js/91b454ee.3a25b812.js deleted file mode 100644 index e5cc71ab09a..00000000000 --- a/docs/assets/js/91b454ee.3a25b812.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[4202],{9107:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>o,default:()=>g,frontMatter:()=>s,metadata:()=>i,toc:()=>l});var r=t(4848),a=t(8453);const s={title:"Sharding RxStorage \ud83d\udc51",slug:"rx-storage-sharding.html"},o="Sharding RxStorage",i={id:"rx-storage-sharding",title:"Sharding RxStorage \ud83d\udc51",description:"With the sharding plugin, you can improve the write and query times of some RxStorage implementations.",source:"@site/docs/rx-storage-sharding.md",sourceDirName:".",slug:"/rx-storage-sharding.html",permalink:"/rx-storage-sharding.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Sharding RxStorage \ud83d\udc51",slug:"rx-storage-sharding.html"},sidebar:"tutorialSidebar",previous:{title:"Memory Synced RxStorage \ud83d\udc51",permalink:"/rx-storage-memory-synced.html"},next:{title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",permalink:"/rx-storage-localstorage-meta-optimizer.html"}},d={},l=[{value:"Using the sharding plugin",id:"using-the-sharding-plugin",level:2}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,a.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"sharding-rxstorage",children:"Sharding RxStorage"}),"\n",(0,r.jsxs)(n.p,{children:["With the sharding plugin, you can improve the write and query times of ",(0,r.jsx)(n.strong,{children:"some"})," ",(0,r.jsx)(n.code,{children:"RxStorage"})," implementations.\nFor example on ",(0,r.jsx)(n.a,{href:"/slow-indexeddb.html",children:"slow IndexedDB"}),", a performance gain of ",(0,r.jsx)(n.strong,{children:"30-50% on reads"}),", and ",(0,r.jsx)(n.strong,{children:"25% on writes"})," can be achieved by using multiple IndexedDB Stores instead of putting all documents into the same store."]}),"\n",(0,r.jsxs)(n.p,{children:["The sharding plugin works as a wrapper around any other ",(0,r.jsx)(n.code,{children:"RxStorage"}),". The sharding plugin will automatically create multiple shards per storage instance and it will merge and split read and write calls to it."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"NOTICE:"})," The sharding plugin is part of ",(0,r.jsx)(n.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]}),"\n",(0,r.jsx)(n.h2,{id:"using-the-sharding-plugin",children:"Using the sharding plugin"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"import {\n getRxStorageSharding\n} from 'rxdb-premium/plugins/storage-sharding';\n\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n\n/**\n * First wrap the original RxStorage with the sharding RxStorage.\n */\nconst shardedRxStorage = getRxStorageSharding({\n\n /**\n * Here we use the dexie.js RxStorage,\n * it is also possible to use any other RxStorage instead.\n */\n storage: getRxStorageDexie()\n});\n\n\n/**\n * Add the sharding options to your schema.\n * Changing these options will require a data migration.\n */\nconst mySchema = {\n /* ... */\n sharding: {\n /**\n * Amount of shards per RxStorage instance.\n * Depending on your data size and query patterns, the optimal shard amount may differ.\n * Do a performance test to optimize that value.\n * 10 Shards is a good value to start with.\n * \n * IMPORTANT: Changing the value of shards is not possible on a already existing database state,\n * you will loose access to your data.\n */\n shards: 10,\n /**\n * Sharding mode,\n * you can either shard by collection or by database.\n * For most cases you should use 'collection' which will shard on the collection level.\n * For example with the IndexedDB RxStorage, it will then create multiple stores per IndexedDB database\n * and not multiple IndexedDB databases, which would be slower.\n */\n mode: 'collection'\n }\n /* ... */\n}\n\n\n/**\n * Create the RxDatabase with the wrapped RxStorage. \n */\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: shardedRxStorage\n});\n\n"})})]})}function g(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>o,x:()=>i});var r=t(6540);const a={},s=r.createContext(a);function o(e){const n=r.useContext(s);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:o(e.components),r.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/91b454ee.9690ff41.js b/docs/assets/js/91b454ee.9690ff41.js new file mode 100644 index 00000000000..eb94fe4cc22 --- /dev/null +++ b/docs/assets/js/91b454ee.9690ff41.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[4202],{9107:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>i,default:()=>g,frontMatter:()=>o,metadata:()=>s,toc:()=>l});var a=t(4848),r=t(8453);const o={title:"Sharding RxStorage \ud83d\udc51",slug:"rx-storage-sharding.html"},i="Sharding RxStorage",s={id:"rx-storage-sharding",title:"Sharding RxStorage \ud83d\udc51",description:"With the sharding plugin, you can improve the write and query times of some RxStorage implementations.",source:"@site/docs/rx-storage-sharding.md",sourceDirName:".",slug:"/rx-storage-sharding.html",permalink:"/rx-storage-sharding.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Sharding RxStorage \ud83d\udc51",slug:"rx-storage-sharding.html"},sidebar:"tutorialSidebar",previous:{title:"Memory Synced RxStorage \ud83d\udc51",permalink:"/rx-storage-memory-synced.html"},next:{title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",permalink:"/rx-storage-localstorage-meta-optimizer.html"}},d={},l=[{value:"Using the sharding plugin",id:"using-the-sharding-plugin",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,r.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.h1,{id:"sharding-rxstorage",children:"Sharding RxStorage"}),"\n",(0,a.jsxs)(n.p,{children:["With the sharding plugin, you can improve the write and query times of ",(0,a.jsx)(n.strong,{children:"some"})," ",(0,a.jsx)(n.code,{children:"RxStorage"})," implementations.\nFor example on ",(0,a.jsx)(n.a,{href:"/slow-indexeddb.html",children:"slow IndexedDB"}),", a performance gain of ",(0,a.jsx)(n.strong,{children:"30-50% on reads"}),", and ",(0,a.jsx)(n.strong,{children:"25% on writes"})," can be achieved by using multiple IndexedDB Stores instead of putting all documents into the same store."]}),"\n",(0,a.jsxs)(n.p,{children:["The sharding plugin works as a wrapper around any other ",(0,a.jsx)(n.code,{children:"RxStorage"}),". The sharding plugin will automatically create multiple shards per storage instance and it will merge and split read and write calls to it."]}),"\n",(0,a.jsx)(n.admonition,{title:"Premium",type:"note",children:(0,a.jsxs)(n.p,{children:["The sharding plugin is part of ",(0,a.jsx)(n.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]})}),"\n",(0,a.jsx)(n.h2,{id:"using-the-sharding-plugin",children:"Using the sharding plugin"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-ts",children:"import {\n getRxStorageSharding\n} from 'rxdb-premium/plugins/storage-sharding';\n\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n\n/**\n * First wrap the original RxStorage with the sharding RxStorage.\n */\nconst shardedRxStorage = getRxStorageSharding({\n\n /**\n * Here we use the dexie.js RxStorage,\n * it is also possible to use any other RxStorage instead.\n */\n storage: getRxStorageDexie()\n});\n\n\n/**\n * Add the sharding options to your schema.\n * Changing these options will require a data migration.\n */\nconst mySchema = {\n /* ... */\n sharding: {\n /**\n * Amount of shards per RxStorage instance.\n * Depending on your data size and query patterns, the optimal shard amount may differ.\n * Do a performance test to optimize that value.\n * 10 Shards is a good value to start with.\n * \n * IMPORTANT: Changing the value of shards is not possible on a already existing database state,\n * you will loose access to your data.\n */\n shards: 10,\n /**\n * Sharding mode,\n * you can either shard by collection or by database.\n * For most cases you should use 'collection' which will shard on the collection level.\n * For example with the IndexedDB RxStorage, it will then create multiple stores per IndexedDB database\n * and not multiple IndexedDB databases, which would be slower.\n */\n mode: 'collection'\n }\n /* ... */\n}\n\n\n/**\n * Create the RxDatabase with the wrapped RxStorage. \n */\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: shardedRxStorage\n});\n\n"})})]})}function g(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>i,x:()=>s});var a=t(6540);const r={},o=a.createContext(r);function i(e){const n=a.useContext(o);return a.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),a.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/924d6dd6.5047a839.js b/docs/assets/js/924d6dd6.5047a839.js new file mode 100644 index 00000000000..fb7addb6d0c --- /dev/null +++ b/docs/assets/js/924d6dd6.5047a839.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[5122],{2414:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>s,contentTitle:()=>c,default:()=>p,frontMatter:()=>a,metadata:()=>i,toc:()=>l});var t=r(4848),o=r(8453);const a={title:"Electron Plugin",slug:"electron.html"},c="Electron Plugin",i={id:"electron",title:"Electron Plugin",description:"RxStorage Electron IpcRenderer & IpcMain",source:"@site/docs/electron.md",sourceDirName:".",slug:"/electron.html",permalink:"/electron.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Electron Plugin",slug:"electron.html"},sidebar:"tutorialSidebar",previous:{title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",permalink:"/rx-storage-localstorage-meta-optimizer.html"},next:{title:"\u2699\ufe0f Replication Protocol",permalink:"/replication.html"}},s={},l=[{value:"RxStorage Electron IpcRenderer & IpcMain",id:"rxstorage-electron-ipcrenderer--ipcmain",level:2},{value:"Related",id:"related",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"electron-plugin",children:"Electron Plugin"}),"\n",(0,t.jsx)(n.h2,{id:"rxstorage-electron-ipcrenderer--ipcmain",children:"RxStorage Electron IpcRenderer & IpcMain"}),"\n",(0,t.jsxs)(n.p,{children:["To use RxDB in ",(0,t.jsx)(n.a,{href:"/electron-database.html",children:"electron"}),", it is recommended to run the RxStorage in the main process and the RxDatabase in the renderer processes. With the rxdb electron plugin you can create a remote RxStorage and consume it from the renderer process."]}),"\n",(0,t.jsxs)(n.p,{children:["To do this in a convenient way, the RxDB electron plugin provides the helper functions ",(0,t.jsx)(n.code,{children:"exposeIpcMainRxStorage"})," and ",(0,t.jsx)(n.code,{children:"getRxStorageIpcRenderer"}),".\nSimilar to the ",(0,t.jsx)(n.a,{href:"/rx-storage-worker.html",children:"Worker RxStorage"}),", these wrap any other ",(0,t.jsx)(n.a,{href:"/rx-storage.html",children:"RxStorage"})," once in the main process and once in each renderer process. In the renderer you can then use the storage to create a ",(0,t.jsx)(n.a,{href:"/rx-database.html",children:"RxDatabase"})," which communicates with the storage of the main process to store and query data."]}),"\n",(0,t.jsx)(n.admonition,{type:"note",children:(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"nodeIntegration"})," must be enabled in ",(0,t.jsx)(n.a,{href:"https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions",children:"Electron"}),"."]})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// main.js\nconst { exposeIpcMainRxStorage } = require('rxdb/plugins/electron');\nconst { getRxStorageMemory } = require('rxdb/plugins/storage-memory');\napp.on('ready', async function () {\n exposeIpcMainRxStorage({\n key: 'main-storage',\n storage: getRxStorageMemory(),\n ipcMain: electron.ipcMain\n });\n});\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// renderer.js\nconst { getRxStorageIpcRenderer } = require('rxdb/plugins/electron');\nconst { getRxStorageMemory } = require('rxdb/plugins/storage-memory');\n\nconst db = await createRxDatabase({\n name,\n storage: getRxStorageIpcRenderer({\n key: 'main-storage',\n ipcRenderer: electron.ipcRenderer\n })\n});\n/* ... */\n"})}),"\n",(0,t.jsx)(n.h2,{id:"related",children:"Related"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"/electron-database.html",children:"Comparison of Electron Databases"})}),"\n"]})]})}function p(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},8453:(e,n,r)=>{r.d(n,{R:()=>c,x:()=>i});var t=r(6540);const o={},a=t.createContext(o);function c(e){const n=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:c(e.components),t.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/924d6dd6.f1e4b4ec.js b/docs/assets/js/924d6dd6.f1e4b4ec.js deleted file mode 100644 index 963ace8ba2c..00000000000 --- a/docs/assets/js/924d6dd6.f1e4b4ec.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[5122],{2414:(e,r,n)=>{n.r(r),n.d(r,{assets:()=>i,contentTitle:()=>c,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>l});var t=n(4848),o=n(8453);const a={title:"Electron Plugin",slug:"electron.html"},c="Electron Plugin",s={id:"electron",title:"Electron Plugin",description:"RxStorage Electron IpcRenderer & IpcMain",source:"@site/docs/electron.md",sourceDirName:".",slug:"/electron.html",permalink:"/electron.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Electron Plugin",slug:"electron.html"},sidebar:"tutorialSidebar",previous:{title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",permalink:"/rx-storage-localstorage-meta-optimizer.html"},next:{title:"\u2699\ufe0f Replication Protocol",permalink:"/replication.html"}},i={},l=[{value:"RxStorage Electron IpcRenderer & IpcMain",id:"rxstorage-electron-ipcrenderer--ipcmain",level:2},{value:"Related",id:"related",level:2}];function d(e){const r={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(r.h1,{id:"electron-plugin",children:"Electron Plugin"}),"\n",(0,t.jsx)(r.h2,{id:"rxstorage-electron-ipcrenderer--ipcmain",children:"RxStorage Electron IpcRenderer & IpcMain"}),"\n",(0,t.jsxs)(r.p,{children:["To use RxDB in ",(0,t.jsx)(r.a,{href:"/electron-database.html",children:"electron"}),", it is recommended to run the RxStorage in the main process and the RxDatabase in the renderer processes. With the rxdb electron plugin you can create a remote RxStorage and consume it from the renderer process."]}),"\n",(0,t.jsxs)(r.p,{children:["To do this in a convenient way, the RxDB electron plugin provides the helper functions ",(0,t.jsx)(r.code,{children:"exposeIpcMainRxStorage"})," and ",(0,t.jsx)(r.code,{children:"getRxStorageIpcRenderer"}),".\nSimilar to the ",(0,t.jsx)(r.a,{href:"/rx-storage-worker.html",children:"Worker RxStorage"}),", these wrap any other ",(0,t.jsx)(r.a,{href:"/rx-storage.html",children:"RxStorage"})," once in the main process and once in each renderer process. In the renderer you can then use the storage to create a ",(0,t.jsx)(r.a,{href:"/rx-database.html",children:"RxDatabase"})," which communicates with the storage of the main process to store and query data."]}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.strong,{children:"NOTICE"}),": ",(0,t.jsx)(r.code,{children:"nodeIntegration"})," must be enabled in ",(0,t.jsx)(r.a,{href:"https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions",children:"Electron"}),"."]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-ts",children:"// main.js\nconst { exposeIpcMainRxStorage } = require('rxdb/plugins/electron');\nconst { getRxStorageMemory } = require('rxdb/plugins/storage-memory');\napp.on('ready', async function () {\n exposeIpcMainRxStorage({\n key: 'main-storage',\n storage: getRxStorageMemory(),\n ipcMain: electron.ipcMain\n });\n});\n"})}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-ts",children:"// renderer.js\nconst { getRxStorageIpcRenderer } = require('rxdb/plugins/electron');\nconst { getRxStorageMemory } = require('rxdb/plugins/storage-memory');\n\nconst db = await createRxDatabase({\n name,\n storage: getRxStorageIpcRenderer({\n key: 'main-storage',\n ipcRenderer: electron.ipcRenderer\n })\n});\n/* ... */\n"})}),"\n",(0,t.jsx)(r.h2,{id:"related",children:"Related"}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsx)(r.li,{children:(0,t.jsx)(r.a,{href:"/electron-database.html",children:"Comparison of Electron Databases"})}),"\n"]})]})}function h(e={}){const{wrapper:r}={...(0,o.R)(),...e.components};return r?(0,t.jsx)(r,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},8453:(e,r,n)=>{n.d(r,{R:()=>c,x:()=>s});var t=n(6540);const o={},a=t.createContext(o);function c(e){const r=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function s(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:c(e.components),t.createElement(a.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/931f4566.5b50de3c.js b/docs/assets/js/931f4566.5b50de3c.js deleted file mode 100644 index 02021d821ab..00000000000 --- a/docs/assets/js/931f4566.5b50de3c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3595],{2439:(e,a,t)=>{t.r(a),t.d(a,{assets:()=>d,contentTitle:()=>o,default:()=>c,frontMatter:()=>s,metadata:()=>r,toc:()=>l});var n=t(4848),i=t(8453);const s={title:"Schema Validation",slug:"schema-validation.html"},o="Schema validation",r={id:"schema-validation",title:"Schema Validation",description:"RxDB has multiple validation implementations that can be used to ensure that your document data is always matching the provided JSON",source:"@site/docs/schema-validation.md",sourceDirName:".",slug:"/schema-validation.html",permalink:"/schema-validation.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Schema Validation",slug:"schema-validation.html"}},d={},l=[{value:"validate-ajv",id:"validate-ajv",level:3},{value:"validate-z-schema",id:"validate-z-schema",level:3},{value:"validate-is-my-json-valid",id:"validate-is-my-json-valid",level:3}];function h(e){const a={a:"a",code:"code",h1:"h1",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(a.h1,{id:"schema-validation",children:"Schema validation"}),"\n",(0,n.jsxs)(a.p,{children:["RxDB has multiple validation implementations that can be used to ensure that your document data is always matching the provided JSON\nschema of your ",(0,n.jsx)(a.code,{children:"RxCollection"}),"."]}),"\n",(0,n.jsxs)(a.p,{children:["The schema validation is ",(0,n.jsx)(a.strong,{children:"not a plugin"})," but comes in as a wrapper around any other ",(0,n.jsx)(a.code,{children:"RxStorage"})," and it will then validate all data that is written into that storage. This is required for multiple reasons:"]}),"\n",(0,n.jsxs)(a.ul,{children:["\n",(0,n.jsxs)(a.li,{children:["It allows us to run the validation inside of a ",(0,n.jsx)(a.a,{href:"/rx-storage-worker.html",children:"Worker RxStorage"})," instead of running it in the main JavaScript process."]}),"\n",(0,n.jsxs)(a.li,{children:["It allows us to configure which ",(0,n.jsx)(a.code,{children:"RxDatabase"})," instance must use the validation and which does not. In production it often makes sense to validate user data, but you might not need the validation for data that is only replicated from the backend."]}),"\n"]}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.strong,{children:"NOTICE:"})," Schema validation can be ",(0,n.jsx)(a.strong,{children:"CPU expensive"})," and increases your build size. You should always use a schema validation in development mode. For most use cases, you ",(0,n.jsx)(a.strong,{children:"should not"})," use a validation in production for better performance."]}),"\n",(0,n.jsxs)(a.p,{children:["When no validation is used, any document data can be saved but there might be ",(0,n.jsx)(a.strong,{children:"undefined behavior"})," when saving data that does not comply to the schema of a ",(0,n.jsx)(a.code,{children:"RxCollection"}),"."]}),"\n",(0,n.jsxs)(a.p,{children:["RxDB has different implementations to validate data, each of them is based on a different ",(0,n.jsx)(a.a,{href:"https://json-schema.org/implementations.html",children:"JSON Schema library"}),". In this example we use the ",(0,n.jsx)(a.a,{href:"/rx-storage-dexie.html",children:"Dexie.js RxStorage"}),", but you can wrap the validation around ",(0,n.jsx)(a.strong,{children:"any other"})," ",(0,n.jsx)(a.a,{href:"/rx-storage.html",children:"RxStorage"}),"."]}),"\n",(0,n.jsx)(a.h3,{id:"validate-ajv",children:"validate-ajv"}),"\n",(0,n.jsxs)(a.p,{children:["A validation-module that does the schema-validation. This one is using ",(0,n.jsx)(a.a,{href:"https://github.com/epoberezkin/ajv",children:"ajv"})," as validator which is a bit faster. Better compliant to the jsonschema-standard but also has a bigger build-size."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv';\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n// wrap the validation around the main RxStorage\nconst storage = wrappedValidateAjvStorage({\n storage: getRxStorageDexie()\n});\n\nconst db = await createRxDatabase({\n name: randomCouchString(10),\n storage\n});\n"})}),"\n",(0,n.jsx)(a.h3,{id:"validate-z-schema",children:"validate-z-schema"}),"\n",(0,n.jsxs)(a.p,{children:["Both ",(0,n.jsx)(a.code,{children:"is-my-json-valid"})," and ",(0,n.jsx)(a.code,{children:"validate-ajv"})," use ",(0,n.jsx)(a.code,{children:"eval()"})," to perform validation which might not be wanted when ",(0,n.jsx)(a.code,{children:"'unsafe-eval'"})," is not allowed in Content Security Policies. This one is using ",(0,n.jsx)(a.a,{href:"https://github.com/zaggino/z-schema",children:"z-schema"})," as validator which doesn't use ",(0,n.jsx)(a.code,{children:"eval"}),"."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { wrappedValidateZSchemaStorage } from 'rxdb/plugins/validate-z-schema';\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n// wrap the validation around the main RxStorage\nconst storage = wrappedValidateZSchemaStorage({\n storage: getRxStorageDexie()\n});\n\nconst db = await createRxDatabase({\n name: randomCouchString(10),\n storage\n});\n"})}),"\n",(0,n.jsx)(a.h3,{id:"validate-is-my-json-valid",children:"validate-is-my-json-valid"}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.strong,{children:"WARNING"}),": The ",(0,n.jsx)(a.code,{children:"is-my-json-valid"})," validation is no longer supported until ",(0,n.jsx)(a.a,{href:"https://github.com/mafintosh/is-my-json-valid/pull/192",children:"this bug"})," is fixed."]}),"\n",(0,n.jsxs)(a.p,{children:["The ",(0,n.jsx)(a.code,{children:"validate-is-my-json-valid"})," plugin uses ",(0,n.jsx)(a.a,{href:"https://www.npmjs.com/package/is-my-json-valid",children:"is-my-json-valid"})," for schema validation."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { wrappedValidateIsMyJsonValidStorage } from 'rxdb/plugins/validate-is-my-json-valid';\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n// wrap the validation around the main RxStorage\nconst storage = wrappedValidateIsMyJsonValidStorage({\n storage: getRxStorageDexie()\n});\n\nconst db = await createRxDatabase({\n name: randomCouchString(10),\n storage\n});\n"})})]})}function c(e={}){const{wrapper:a}={...(0,i.R)(),...e.components};return a?(0,n.jsx)(a,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}},8453:(e,a,t)=>{t.d(a,{R:()=>o,x:()=>r});var n=t(6540);const i={},s=n.createContext(i);function o(e){const a=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function r(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),n.createElement(s.Provider,{value:a},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/931f4566.90378afb.js b/docs/assets/js/931f4566.90378afb.js new file mode 100644 index 00000000000..bef3daff029 --- /dev/null +++ b/docs/assets/js/931f4566.90378afb.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3595],{2439:(e,a,t)=>{t.r(a),t.d(a,{assets:()=>d,contentTitle:()=>o,default:()=>c,frontMatter:()=>s,metadata:()=>r,toc:()=>l});var n=t(4848),i=t(8453);const s={title:"Schema Validation",slug:"schema-validation.html"},o="Schema validation",r={id:"schema-validation",title:"Schema Validation",description:"RxDB has multiple validation implementations that can be used to ensure that your document data is always matching the provided JSON",source:"@site/docs/schema-validation.md",sourceDirName:".",slug:"/schema-validation.html",permalink:"/schema-validation.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Schema Validation",slug:"schema-validation.html"}},d={},l=[{value:"validate-ajv",id:"validate-ajv",level:3},{value:"validate-z-schema",id:"validate-z-schema",level:3},{value:"validate-is-my-json-valid",id:"validate-is-my-json-valid",level:3}];function h(e){const a={a:"a",admonition:"admonition",code:"code",h1:"h1",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(a.h1,{id:"schema-validation",children:"Schema validation"}),"\n",(0,n.jsxs)(a.p,{children:["RxDB has multiple validation implementations that can be used to ensure that your document data is always matching the provided JSON\nschema of your ",(0,n.jsx)(a.code,{children:"RxCollection"}),"."]}),"\n",(0,n.jsxs)(a.p,{children:["The schema validation is ",(0,n.jsx)(a.strong,{children:"not a plugin"})," but comes in as a wrapper around any other ",(0,n.jsx)(a.code,{children:"RxStorage"})," and it will then validate all data that is written into that storage. This is required for multiple reasons:"]}),"\n",(0,n.jsxs)(a.ul,{children:["\n",(0,n.jsxs)(a.li,{children:["It allows us to run the validation inside of a ",(0,n.jsx)(a.a,{href:"/rx-storage-worker.html",children:"Worker RxStorage"})," instead of running it in the main JavaScript process."]}),"\n",(0,n.jsxs)(a.li,{children:["It allows us to configure which ",(0,n.jsx)(a.code,{children:"RxDatabase"})," instance must use the validation and which does not. In production it often makes sense to validate user data, but you might not need the validation for data that is only replicated from the backend."]}),"\n"]}),"\n",(0,n.jsx)(a.admonition,{type:"warning",children:(0,n.jsxs)(a.p,{children:["Schema validation can be ",(0,n.jsx)(a.strong,{children:"CPU expensive"})," and increases your build size. You should always use a schema validation in development mode. For most use cases, you ",(0,n.jsx)(a.strong,{children:"should not"})," use a validation in production for better performance."]})}),"\n",(0,n.jsxs)(a.p,{children:["When no validation is used, any document data can be saved but there might be ",(0,n.jsx)(a.strong,{children:"undefined behavior"})," when saving data that does not comply to the schema of a ",(0,n.jsx)(a.code,{children:"RxCollection"}),"."]}),"\n",(0,n.jsxs)(a.p,{children:["RxDB has different implementations to validate data, each of them is based on a different ",(0,n.jsx)(a.a,{href:"https://json-schema.org/implementations.html",children:"JSON Schema library"}),". In this example we use the ",(0,n.jsx)(a.a,{href:"/rx-storage-dexie.html",children:"Dexie.js RxStorage"}),", but you can wrap the validation around ",(0,n.jsx)(a.strong,{children:"any other"})," ",(0,n.jsx)(a.a,{href:"/rx-storage.html",children:"RxStorage"}),"."]}),"\n",(0,n.jsx)(a.h3,{id:"validate-ajv",children:"validate-ajv"}),"\n",(0,n.jsxs)(a.p,{children:["A validation-module that does the schema-validation. This one is using ",(0,n.jsx)(a.a,{href:"https://github.com/epoberezkin/ajv",children:"ajv"})," as validator which is a bit faster. Better compliant to the jsonschema-standard but also has a bigger build-size."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv';\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n// wrap the validation around the main RxStorage\nconst storage = wrappedValidateAjvStorage({\n storage: getRxStorageDexie()\n});\n\nconst db = await createRxDatabase({\n name: randomCouchString(10),\n storage\n});\n"})}),"\n",(0,n.jsx)(a.h3,{id:"validate-z-schema",children:"validate-z-schema"}),"\n",(0,n.jsxs)(a.p,{children:["Both ",(0,n.jsx)(a.code,{children:"is-my-json-valid"})," and ",(0,n.jsx)(a.code,{children:"validate-ajv"})," use ",(0,n.jsx)(a.code,{children:"eval()"})," to perform validation which might not be wanted when ",(0,n.jsx)(a.code,{children:"'unsafe-eval'"})," is not allowed in Content Security Policies. This one is using ",(0,n.jsx)(a.a,{href:"https://github.com/zaggino/z-schema",children:"z-schema"})," as validator which doesn't use ",(0,n.jsx)(a.code,{children:"eval"}),"."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { wrappedValidateZSchemaStorage } from 'rxdb/plugins/validate-z-schema';\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n// wrap the validation around the main RxStorage\nconst storage = wrappedValidateZSchemaStorage({\n storage: getRxStorageDexie()\n});\n\nconst db = await createRxDatabase({\n name: randomCouchString(10),\n storage\n});\n"})}),"\n",(0,n.jsx)(a.h3,{id:"validate-is-my-json-valid",children:"validate-is-my-json-valid"}),"\n",(0,n.jsxs)(a.p,{children:[(0,n.jsx)(a.strong,{children:"WARNING"}),": The ",(0,n.jsx)(a.code,{children:"is-my-json-valid"})," validation is no longer supported until ",(0,n.jsx)(a.a,{href:"https://github.com/mafintosh/is-my-json-valid/pull/192",children:"this bug"})," is fixed."]}),"\n",(0,n.jsxs)(a.p,{children:["The ",(0,n.jsx)(a.code,{children:"validate-is-my-json-valid"})," plugin uses ",(0,n.jsx)(a.a,{href:"https://www.npmjs.com/package/is-my-json-valid",children:"is-my-json-valid"})," for schema validation."]}),"\n",(0,n.jsx)(a.pre,{children:(0,n.jsx)(a.code,{className:"language-javascript",children:"import { wrappedValidateIsMyJsonValidStorage } from 'rxdb/plugins/validate-is-my-json-valid';\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n// wrap the validation around the main RxStorage\nconst storage = wrappedValidateIsMyJsonValidStorage({\n storage: getRxStorageDexie()\n});\n\nconst db = await createRxDatabase({\n name: randomCouchString(10),\n storage\n});\n"})})]})}function c(e={}){const{wrapper:a}={...(0,i.R)(),...e.components};return a?(0,n.jsx)(a,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}},8453:(e,a,t)=>{t.d(a,{R:()=>o,x:()=>r});var n=t(6540);const i={},s=n.createContext(i);function o(e){const a=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(a):{...a,...e}}),[a,e])}function r(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),n.createElement(s.Provider,{value:a},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/a574e172.4dd0ced1.js b/docs/assets/js/a574e172.4dd0ced1.js deleted file mode 100644 index 8bbcfee0bcb..00000000000 --- a/docs/assets/js/a574e172.4dd0ced1.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[7149],{9329:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>d,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var o=n(4848),r=n(8453);const s={title:"HTTP Replication",slug:"replication-http.html",description:"Learn how to establish HTTP replication between RxDB clients and a Node.js Express server for data synchronization."},a="HTTP Replication from a custom server to RxDB clients",i={id:"replication-http",title:"HTTP Replication",description:"Learn how to establish HTTP replication between RxDB clients and a Node.js Express server for data synchronization.",source:"@site/docs/replication-http.md",sourceDirName:".",slug:"/replication-http.html",permalink:"/replication-http.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"HTTP Replication",slug:"replication-http.html",description:"Learn how to establish HTTP replication between RxDB clients and a Node.js Express server for data synchronization."},sidebar:"tutorialSidebar",previous:{title:"\u2699\ufe0f Replication Protocol",permalink:"/replication.html"},next:{title:"RxDB Server Replication",permalink:"/replication-server"}},l={},c=[{value:"Setup",id:"setup",level:2},{value:"Pull from the server to the client",id:"pull-from-the-server-to-the-client",level:2},{value:"Push from the Client to the Server",id:"push-from-the-client-to-the-server",level:2},{value:"pullStream$ for ongoing changes",id:"pullstream-for-ongoing-changes",level:2},{value:"pullStream$ RESYNC flag",id:"pullstream-resync-flag",level:3},{value:"Missing implementation details",id:"missing-implementation-details",level:2}];function h(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"http-replication-from-a-custom-server-to-rxdb-clients",children:"HTTP Replication from a custom server to RxDB clients"}),"\n",(0,o.jsxs)(t.p,{children:["While RxDB has a range of backend-specific replication plugins (like ",(0,o.jsx)(t.a,{href:"/replication-graphql.html",children:"GraphQL"})," or ",(0,o.jsx)(t.a,{href:"/replication-firestore.html",children:"Firestore"}),"), the replication is build in a way to make it very easy to replicate data from a custom server to RxDB clients."]}),"\n",(0,o.jsx)("p",{align:"center",children:(0,o.jsx)("img",{src:"./files/icons/with-gradient/replication.svg",alt:"HTTP replication",height:"60"})}),"\n",(0,o.jsxs)(t.p,{children:["Using ",(0,o.jsx)(t.strong,{children:"HTTP"})," as a transport protocol makes it simple to create a compatible backend on top of your existing infrastructure. For events that must be send from the server to to client, we can use ",(0,o.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events",children:"Server Send Events"}),"."]}),"\n",(0,o.jsx)(t.p,{children:"In this tutorial we will implement a HTTP replication between an RxDB client and a MongoDB express server. You can adapt this for any other backend database technologie like PostgreSQL or even a non-Node.js server like go or java."}),"\n",(0,o.jsxs)(t.p,{children:["To create a compatible server for replication, we will start a server and implement the correct HTTP routes and replication handlers. We need a push-handler, a pull-handler and for the ongoing changes ",(0,o.jsx)(t.code,{children:"pull.stream"})," we use ",(0,o.jsx)(t.strong,{children:"Server Send Events"}),"."]}),"\n",(0,o.jsx)(t.h2,{id:"setup",children:"Setup"}),"\n",(0,o.jsxs)(t.p,{children:["RxDB does not have a specific HTTP-replication plugin because the ",(0,o.jsx)(t.a,{href:"/replication.html",children:"replication primitives plugin"})," is simple enough to start a HTTP replication on top of it.\nWe import the ",(0,o.jsx)(t.code,{children:"replicateRxCollection"})," function and start the replication from there for a single ",(0,o.jsx)(t.a,{href:"/rx-collection.html",children:"RxCollection"}),"."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nimport { replicateRxCollection } from 'rxdb/plugins/replication';\nconst replicationState = await replicateRxCollection({\n collection: myRxCollection,\n replicationIdentifier: 'my-http-replication',\n push: { /* add settings from below */ },\n pull: { /* add settings from below */ }\n});\n"})}),"\n",(0,o.jsx)(t.p,{children:"On the server side, we start an express server that has a MongoDB connection and serves the HTTP requests of the client."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > server.ts\nimport { MongoClient } from 'mongodb';\nimport express from 'express';\nconst mongoClient = new MongoClient('mongodb://localhost:27017/');\nconst mongoConnection = await mongoClient.connect();\nconst mongoDatabase = mongoConnection.db('myDatabase');\nconst mongoCollection = await mongoDatabase.collection('myDocs');\n\nconst app = express();\napp.use(express.json());\n\n/* ... add routes from below */\n\napp.listen(80, () => {\n console.log(`Example app listening on port 80`)\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"pull-from-the-server-to-the-client",children:"Pull from the server to the client"}),"\n",(0,o.jsxs)(t.p,{children:["First we need to implement the pull handler. This is used by the RxDB replication to fetch all documents writes that happened after a given ",(0,o.jsx)(t.code,{children:"checkpoint"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.code,{children:"checkpoint"})," format is not determined by RxDB, instead the server can use any type of changepoint that can be used to iterate across document writes. Here we will just use a unix timestamp ",(0,o.jsx)(t.code,{children:"updatedAt"})," and a string ",(0,o.jsx)(t.code,{children:"id"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["On the client we add the ",(0,o.jsx)(t.code,{children:"pull.handler"})," to the replication setting. The handler request the correct server url and fetches the documents."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nconst replicationState = await replicateRxCollection({\n /* ... */\n pull: {\n async handler(checkpointOrNull, batchSize){\n const updatedAt = checkpointOrNull ? checkpointOrNull.updatedAt : 0;\n const id = checkpointOrNull ? checkpointOrNull.id : '';\n const response = await fetch(`https://localhost/pull?updatedAt=${updatedAt}&id=${id}&limit=${batchSize}`);\n const data = await response.json();\n return {\n documents: data.documents,\n checkpoint: data.checkpoint\n };\n }\n \n }\n /* ... */\n});\n"})}),"\n",(0,o.jsx)(t.p,{children:"The server responds with an array of document data based on the given checkpoint and a new checkpoint.\nAlso the server has to respect the batchSize so that RxDB knows when there are no more new documents and the server returns a non-full array."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > server.ts\nimport { lastOfArray } from 'rxdb/plugins/core';\napp.get('/pull', (req, res) => {\n const id = req.query.id;\n const updatedAt = parseInt(req.query.updatedAt, 10);\n const documents = await mongoCollection.find({\n $or: [\n /**\n * Notice that we have to compare the updatedAt AND the id field\n * because the updateAt field is not unique and when two documents have\n * the same updateAt, we can still \"sort\" them by their id.\n */\n {\n updateAt: { $gt: updatedAt }\n },\n {\n updateAt: { $eq: updatedAt }\n id: { $gt: id }\n }\n ]\n }).limit(parseInt(req.query.batchSize, 10)).toArray();\n const newCheckpoint = documents.length === 0 ? { id, updatedAt } : {\n id: lastOfArray(documents).id,\n updatedAt: lastOfArray(documents).updatedAt\n };\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ documents, checkpoint: newCheckpoint }));\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"push-from-the-client-to-the-server",children:"Push from the Client to the Server"}),"\n",(0,o.jsxs)(t.p,{children:["To send client side writes to the server, we have to implement the ",(0,o.jsx)(t.code,{children:"push.handler"}),". It gets an array of change rows as input and has to return only the conflicting documents that did not have been written to the server. Each change row contains a ",(0,o.jsx)(t.code,{children:"newDocumentState"})," and an optional ",(0,o.jsx)(t.code,{children:"assumedMasterState"}),"."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nconst replicationState = await replicateRxCollection({\n /* ... */\n push: {\n async handler(changeRows){\n const rawResponse = await fetch('https://localhost/push', {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(changeRows)\n });\n const conflictsArray = await rawResponse.json();\n return conflictsArray;\n }\n }\n /* ... */\n});\n"})}),"\n",(0,o.jsxs)(t.p,{children:["On the server we first have to detect if the ",(0,o.jsx)(t.code,{children:"assumedMasterState"}),' is correct for each row. If yes, we have to write the new document state to the database, otherwise we have to return the "real" master state in the conflict array.']}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.strong,{children:"NOTICE:"})," For simplicity in this tutorial, we do not use transactions. In reality you should run the full push function inside of a MongoDB transaction to ensure that no other process can mix up the document state while the writes are processed. Also you should call batch operations on MongoDB instead of running the operations for each change row."]}),"\n",(0,o.jsxs)(t.p,{children:["The server also creates an ",(0,o.jsx)(t.code,{children:"event"})," that is emitted to the ",(0,o.jsx)(t.code,{children:"pullStream$"})," which is later used in the ",(0,o.jsx)(t.a,{href:"#pullstream-for-ongoing-changes",children:"pull.stream$"}),"."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > server.ts\nimport { lastOfArray } from 'rxdb/plugins/core';\nimport { Subject } from 'rxjs';\n\n// used in the pull.stream$ below\nlet lastEventId = 0;\nconst pullStream$ = new Subject();\n\napp.get('/push', (req, res) => {\n const changeRows = req.body;\n const conflicts = [];\n const event = {\n id: lastEventId++,\n documents: [],\n checkpoint: null\n };\n for(const changeRow of changeRows){\n const realMasterState = mongoCollection.findOne({id: changeRow.newDocumentState.id});\n if(\n realMasterState && !changeRow.assumedMasterState ||\n (\n realMasterState && changeRow.assumedMasterState &&\n /*\n * For simplicity we detect conflicts on the server by only compare the updateAt value.\n * In reality you might want to do a more complex check or do a deep-equal comparison.\n */\n realMasterState.updatedAt !== changeRow.assumedMasterState.updatedAt\n )\n ) {\n // we have a conflict\n conflicts.push(realMasterState);\n } else {\n // no conflict -> write the document\n mongoCollection.updateOne(\n {id: changeRow.newDocumentState.id},\n changeRow.newDocumentState\n );\n event.documents.push(changeRow.newDocumentState);\n event.checkpoint = { id: changeRow.newDocumentState.id, updatedAt: changeRow.newDocumentState.updatedAt };\n }\n }\n if(event.documents.length > 0){\n myPullStream$.next(event);\n }\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify(conflicts));\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"pullstream-for-ongoing-changes",children:"pullStream$ for ongoing changes"}),"\n",(0,o.jsxs)(t.p,{children:["While the normal pull handler is used when the replication is in ",(0,o.jsx)(t.a,{href:"/replication.html#checkpoint-iteration",children:"iteration mode"}),", we also need a stream of ongoing changes when the replication is in ",(0,o.jsx)(t.a,{href:"/replication.html#event-observation",children:"event observation mode"}),".\nThe ",(0,o.jsx)(t.code,{children:"pull.stream$"})," is implemented with server send events that are send from the server to the client."]}),"\n",(0,o.jsx)(t.p,{children:"The client connects to an url and receives server-send-events that contain all ongoing writes."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nimport { Subject } from 'rxjs';\nconst myPullStream$ = new Subject();\nconst eventSource = new EventSource('http://localhost/pullStream', { withCredentials: true });\neventSource.onmessage = event => {\n const eventData = JSON.parse(event.data);\n myPullStream$.next({\n documents: eventData.documents,\n checkpoint: eventData.checkpoint\n });\n};\n\nconst replicationState = await replicateRxCollection({\n /* ... */\n pull: {\n /* ... */\n stream$: myPullStream$.asObservable()\n }\n /* ... */\n});\n"})}),"\n",(0,o.jsxs)(t.p,{children:["On the server we have to implement the ",(0,o.jsx)(t.code,{children:"pullStream"})," route and emit the events. We use the ",(0,o.jsx)(t.code,{children:"pullStream$"})," observable from ",(0,o.jsx)(t.a,{href:"#push-from-the-client-to-the-server",children:"above"})," to fetch all ongoing events and respond them to the client."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > server.ts\napp.get('/pullStream', (req, res) => {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Connection': 'keep-alive',\n 'Cache-Control': 'no-cache'\n });\n const subscription = pullStream$.subscribe(event => res.write('data: ' + JSON.stringify(event) + '\\n\\n'));\n req.on('close', () => subscription.unsubscribe());\n});\n"})}),"\n",(0,o.jsx)(t.h3,{id:"pullstream-resync-flag",children:"pullStream$ RESYNC flag"}),"\n",(0,o.jsxs)(t.p,{children:["In case the client looses the connection, the EventSource will automatically reconnect but there might have been some changes that have been missed out in the meantime. The replication has to be informed that it might have missed events by emitting a ",(0,o.jsx)(t.code,{children:"RESYNC"})," flag from the ",(0,o.jsx)(t.code,{children:"pull.stream$"}),".\nThe replication will then catch up by switching to the ",(0,o.jsx)(t.a,{href:"/replication.html#checkpoint-iteration",children:"iteration mode"})," until it is in sync with the server again."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\neventSource.onerror = () => myPullStream$.next('RESYNC');\n"})}),"\n",(0,o.jsxs)(t.p,{children:["The purpose of the ",(0,o.jsx)(t.code,{children:"RESYNC"}),' flag is to tell the client that "something might have changed" and then the client can react on that information without having to run operations in an interval.']}),"\n",(0,o.jsx)(t.p,{children:"If your backend is not capable of emitting the actual documents and checkpoint in the pull stream, you could just map all events to the RESYNC flag. This would make the replication work with a slight performance drawback:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nimport { Subject } from 'rxjs';\nconst myPullStream$ = new Subject();\nconst eventSource = new EventSource('http://localhost/pullStream', { withCredentials: true });\neventSource.onmessage = () => myPullStream$.next('RESYNC');\nconst replicationState = await replicateRxCollection({\n pull: {\n stream$: myPullStream$.asObservable()\n }\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"missing-implementation-details",children:"Missing implementation details"}),"\n",(0,o.jsx)(t.p,{children:"Here we only covered the basics of doing a HTTP replication between RxDB clients and a server. We did not cover the following aspects of the implementation:"}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsx)(t.li,{children:"Authentication: To authenticate the client on the server, you might want to send authentication headers with the HTTP requests"}),"\n",(0,o.jsxs)(t.li,{children:["Skip events on the ",(0,o.jsx)(t.code,{children:"pull.stream$"})," for the client that caused the changes to improve performance."]}),"\n"]})]})}function d(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>a,x:()=>i});var o=n(6540);const r={},s=o.createContext(r);function a(e){const t=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),o.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/a574e172.bc01cac5.js b/docs/assets/js/a574e172.bc01cac5.js new file mode 100644 index 00000000000..73c2ebb0b47 --- /dev/null +++ b/docs/assets/js/a574e172.bc01cac5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[7149],{9329:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>d,frontMatter:()=>a,metadata:()=>i,toc:()=>c});var o=n(4848),r=n(8453);const a={title:"HTTP Replication",slug:"replication-http.html",description:"Learn how to establish HTTP replication between RxDB clients and a Node.js Express server for data synchronization."},s="HTTP Replication from a custom server to RxDB clients",i={id:"replication-http",title:"HTTP Replication",description:"Learn how to establish HTTP replication between RxDB clients and a Node.js Express server for data synchronization.",source:"@site/docs/replication-http.md",sourceDirName:".",slug:"/replication-http.html",permalink:"/replication-http.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"HTTP Replication",slug:"replication-http.html",description:"Learn how to establish HTTP replication between RxDB clients and a Node.js Express server for data synchronization."},sidebar:"tutorialSidebar",previous:{title:"\u2699\ufe0f Replication Protocol",permalink:"/replication.html"},next:{title:"RxDB Server Replication",permalink:"/replication-server"}},l={},c=[{value:"Setup",id:"setup",level:2},{value:"Pull from the server to the client",id:"pull-from-the-server-to-the-client",level:2},{value:"Push from the Client to the Server",id:"push-from-the-client-to-the-server",level:2},{value:"pullStream$ for ongoing changes",id:"pullstream-for-ongoing-changes",level:2},{value:"pullStream$ RESYNC flag",id:"pullstream-resync-flag",level:3},{value:"Missing implementation details",id:"missing-implementation-details",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"http-replication-from-a-custom-server-to-rxdb-clients",children:"HTTP Replication from a custom server to RxDB clients"}),"\n",(0,o.jsxs)(t.p,{children:["While RxDB has a range of backend-specific replication plugins (like ",(0,o.jsx)(t.a,{href:"/replication-graphql.html",children:"GraphQL"})," or ",(0,o.jsx)(t.a,{href:"/replication-firestore.html",children:"Firestore"}),"), the replication is build in a way to make it very easy to replicate data from a custom server to RxDB clients."]}),"\n",(0,o.jsx)("p",{align:"center",children:(0,o.jsx)("img",{src:"./files/icons/with-gradient/replication.svg",alt:"HTTP replication",height:"60"})}),"\n",(0,o.jsxs)(t.p,{children:["Using ",(0,o.jsx)(t.strong,{children:"HTTP"})," as a transport protocol makes it simple to create a compatible backend on top of your existing infrastructure. For events that must be send from the server to to client, we can use ",(0,o.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events",children:"Server Send Events"}),"."]}),"\n",(0,o.jsx)(t.p,{children:"In this tutorial we will implement a HTTP replication between an RxDB client and a MongoDB express server. You can adapt this for any other backend database technologie like PostgreSQL or even a non-Node.js server like go or java."}),"\n",(0,o.jsxs)(t.p,{children:["To create a compatible server for replication, we will start a server and implement the correct HTTP routes and replication handlers. We need a push-handler, a pull-handler and for the ongoing changes ",(0,o.jsx)(t.code,{children:"pull.stream"})," we use ",(0,o.jsx)(t.strong,{children:"Server Send Events"}),"."]}),"\n",(0,o.jsx)(t.h2,{id:"setup",children:"Setup"}),"\n",(0,o.jsxs)(t.p,{children:["RxDB does not have a specific HTTP-replication plugin because the ",(0,o.jsx)(t.a,{href:"/replication.html",children:"replication primitives plugin"})," is simple enough to start a HTTP replication on top of it.\nWe import the ",(0,o.jsx)(t.code,{children:"replicateRxCollection"})," function and start the replication from there for a single ",(0,o.jsx)(t.a,{href:"/rx-collection.html",children:"RxCollection"}),"."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nimport { replicateRxCollection } from 'rxdb/plugins/replication';\nconst replicationState = await replicateRxCollection({\n collection: myRxCollection,\n replicationIdentifier: 'my-http-replication',\n push: { /* add settings from below */ },\n pull: { /* add settings from below */ }\n});\n"})}),"\n",(0,o.jsx)(t.p,{children:"On the server side, we start an express server that has a MongoDB connection and serves the HTTP requests of the client."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > server.ts\nimport { MongoClient } from 'mongodb';\nimport express from 'express';\nconst mongoClient = new MongoClient('mongodb://localhost:27017/');\nconst mongoConnection = await mongoClient.connect();\nconst mongoDatabase = mongoConnection.db('myDatabase');\nconst mongoCollection = await mongoDatabase.collection('myDocs');\n\nconst app = express();\napp.use(express.json());\n\n/* ... add routes from below */\n\napp.listen(80, () => {\n console.log(`Example app listening on port 80`)\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"pull-from-the-server-to-the-client",children:"Pull from the server to the client"}),"\n",(0,o.jsxs)(t.p,{children:["First we need to implement the pull handler. This is used by the RxDB replication to fetch all documents writes that happened after a given ",(0,o.jsx)(t.code,{children:"checkpoint"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.code,{children:"checkpoint"})," format is not determined by RxDB, instead the server can use any type of changepoint that can be used to iterate across document writes. Here we will just use a unix timestamp ",(0,o.jsx)(t.code,{children:"updatedAt"})," and a string ",(0,o.jsx)(t.code,{children:"id"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["On the client we add the ",(0,o.jsx)(t.code,{children:"pull.handler"})," to the replication setting. The handler request the correct server url and fetches the documents."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nconst replicationState = await replicateRxCollection({\n /* ... */\n pull: {\n async handler(checkpointOrNull, batchSize){\n const updatedAt = checkpointOrNull ? checkpointOrNull.updatedAt : 0;\n const id = checkpointOrNull ? checkpointOrNull.id : '';\n const response = await fetch(`https://localhost/pull?updatedAt=${updatedAt}&id=${id}&limit=${batchSize}`);\n const data = await response.json();\n return {\n documents: data.documents,\n checkpoint: data.checkpoint\n };\n }\n \n }\n /* ... */\n});\n"})}),"\n",(0,o.jsx)(t.p,{children:"The server responds with an array of document data based on the given checkpoint and a new checkpoint.\nAlso the server has to respect the batchSize so that RxDB knows when there are no more new documents and the server returns a non-full array."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > server.ts\nimport { lastOfArray } from 'rxdb/plugins/core';\napp.get('/pull', (req, res) => {\n const id = req.query.id;\n const updatedAt = parseInt(req.query.updatedAt, 10);\n const documents = await mongoCollection.find({\n $or: [\n /**\n * Notice that we have to compare the updatedAt AND the id field\n * because the updateAt field is not unique and when two documents have\n * the same updateAt, we can still \"sort\" them by their id.\n */\n {\n updateAt: { $gt: updatedAt }\n },\n {\n updateAt: { $eq: updatedAt }\n id: { $gt: id }\n }\n ]\n }).limit(parseInt(req.query.batchSize, 10)).toArray();\n const newCheckpoint = documents.length === 0 ? { id, updatedAt } : {\n id: lastOfArray(documents).id,\n updatedAt: lastOfArray(documents).updatedAt\n };\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ documents, checkpoint: newCheckpoint }));\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"push-from-the-client-to-the-server",children:"Push from the Client to the Server"}),"\n",(0,o.jsxs)(t.p,{children:["To send client side writes to the server, we have to implement the ",(0,o.jsx)(t.code,{children:"push.handler"}),". It gets an array of change rows as input and has to return only the conflicting documents that did not have been written to the server. Each change row contains a ",(0,o.jsx)(t.code,{children:"newDocumentState"})," and an optional ",(0,o.jsx)(t.code,{children:"assumedMasterState"}),"."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nconst replicationState = await replicateRxCollection({\n /* ... */\n push: {\n async handler(changeRows){\n const rawResponse = await fetch('https://localhost/push', {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(changeRows)\n });\n const conflictsArray = await rawResponse.json();\n return conflictsArray;\n }\n }\n /* ... */\n});\n"})}),"\n",(0,o.jsxs)(t.p,{children:["On the server we first have to detect if the ",(0,o.jsx)(t.code,{children:"assumedMasterState"}),' is correct for each row. If yes, we have to write the new document state to the database, otherwise we have to return the "real" master state in the conflict array.']}),"\n",(0,o.jsx)(t.admonition,{type:"note",children:(0,o.jsx)(t.p,{children:"For simplicity in this tutorial, we do not use transactions. In reality you should run the full push function inside of a MongoDB transaction to ensure that no other process can mix up the document state while the writes are processed. Also you should call batch operations on MongoDB instead of running the operations for each change row."})}),"\n",(0,o.jsxs)(t.p,{children:["The server also creates an ",(0,o.jsx)(t.code,{children:"event"})," that is emitted to the ",(0,o.jsx)(t.code,{children:"pullStream$"})," which is later used in the ",(0,o.jsx)(t.a,{href:"#pullstream-for-ongoing-changes",children:"pull.stream$"}),"."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > server.ts\nimport { lastOfArray } from 'rxdb/plugins/core';\nimport { Subject } from 'rxjs';\n\n// used in the pull.stream$ below\nlet lastEventId = 0;\nconst pullStream$ = new Subject();\n\napp.get('/push', (req, res) => {\n const changeRows = req.body;\n const conflicts = [];\n const event = {\n id: lastEventId++,\n documents: [],\n checkpoint: null\n };\n for(const changeRow of changeRows){\n const realMasterState = mongoCollection.findOne({id: changeRow.newDocumentState.id});\n if(\n realMasterState && !changeRow.assumedMasterState ||\n (\n realMasterState && changeRow.assumedMasterState &&\n /*\n * For simplicity we detect conflicts on the server by only compare the updateAt value.\n * In reality you might want to do a more complex check or do a deep-equal comparison.\n */\n realMasterState.updatedAt !== changeRow.assumedMasterState.updatedAt\n )\n ) {\n // we have a conflict\n conflicts.push(realMasterState);\n } else {\n // no conflict -> write the document\n mongoCollection.updateOne(\n {id: changeRow.newDocumentState.id},\n changeRow.newDocumentState\n );\n event.documents.push(changeRow.newDocumentState);\n event.checkpoint = { id: changeRow.newDocumentState.id, updatedAt: changeRow.newDocumentState.updatedAt };\n }\n }\n if(event.documents.length > 0){\n myPullStream$.next(event);\n }\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify(conflicts));\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"pullstream-for-ongoing-changes",children:"pullStream$ for ongoing changes"}),"\n",(0,o.jsxs)(t.p,{children:["While the normal pull handler is used when the replication is in ",(0,o.jsx)(t.a,{href:"/replication.html#checkpoint-iteration",children:"iteration mode"}),", we also need a stream of ongoing changes when the replication is in ",(0,o.jsx)(t.a,{href:"/replication.html#event-observation",children:"event observation mode"}),".\nThe ",(0,o.jsx)(t.code,{children:"pull.stream$"})," is implemented with server send events that are send from the server to the client."]}),"\n",(0,o.jsx)(t.p,{children:"The client connects to an url and receives server-send-events that contain all ongoing writes."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nimport { Subject } from 'rxjs';\nconst myPullStream$ = new Subject();\nconst eventSource = new EventSource('http://localhost/pullStream', { withCredentials: true });\neventSource.onmessage = event => {\n const eventData = JSON.parse(event.data);\n myPullStream$.next({\n documents: eventData.documents,\n checkpoint: eventData.checkpoint\n });\n};\n\nconst replicationState = await replicateRxCollection({\n /* ... */\n pull: {\n /* ... */\n stream$: myPullStream$.asObservable()\n }\n /* ... */\n});\n"})}),"\n",(0,o.jsxs)(t.p,{children:["On the server we have to implement the ",(0,o.jsx)(t.code,{children:"pullStream"})," route and emit the events. We use the ",(0,o.jsx)(t.code,{children:"pullStream$"})," observable from ",(0,o.jsx)(t.a,{href:"#push-from-the-client-to-the-server",children:"above"})," to fetch all ongoing events and respond them to the client."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > server.ts\napp.get('/pullStream', (req, res) => {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Connection': 'keep-alive',\n 'Cache-Control': 'no-cache'\n });\n const subscription = pullStream$.subscribe(event => res.write('data: ' + JSON.stringify(event) + '\\n\\n'));\n req.on('close', () => subscription.unsubscribe());\n});\n"})}),"\n",(0,o.jsx)(t.h3,{id:"pullstream-resync-flag",children:"pullStream$ RESYNC flag"}),"\n",(0,o.jsxs)(t.p,{children:["In case the client looses the connection, the EventSource will automatically reconnect but there might have been some changes that have been missed out in the meantime. The replication has to be informed that it might have missed events by emitting a ",(0,o.jsx)(t.code,{children:"RESYNC"})," flag from the ",(0,o.jsx)(t.code,{children:"pull.stream$"}),".\nThe replication will then catch up by switching to the ",(0,o.jsx)(t.a,{href:"/replication.html#checkpoint-iteration",children:"iteration mode"})," until it is in sync with the server again."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\neventSource.onerror = () => myPullStream$.next('RESYNC');\n"})}),"\n",(0,o.jsxs)(t.p,{children:["The purpose of the ",(0,o.jsx)(t.code,{children:"RESYNC"}),' flag is to tell the client that "something might have changed" and then the client can react on that information without having to run operations in an interval.']}),"\n",(0,o.jsx)(t.p,{children:"If your backend is not capable of emitting the actual documents and checkpoint in the pull stream, you could just map all events to the RESYNC flag. This would make the replication work with a slight performance drawback:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"// > client.ts\nimport { Subject } from 'rxjs';\nconst myPullStream$ = new Subject();\nconst eventSource = new EventSource('http://localhost/pullStream', { withCredentials: true });\neventSource.onmessage = () => myPullStream$.next('RESYNC');\nconst replicationState = await replicateRxCollection({\n pull: {\n stream$: myPullStream$.asObservable()\n }\n});\n"})}),"\n",(0,o.jsx)(t.h2,{id:"missing-implementation-details",children:"Missing implementation details"}),"\n",(0,o.jsx)(t.p,{children:"Here we only covered the basics of doing a HTTP replication between RxDB clients and a server. We did not cover the following aspects of the implementation:"}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsx)(t.li,{children:"Authentication: To authenticate the client on the server, you might want to send authentication headers with the HTTP requests"}),"\n",(0,o.jsxs)(t.li,{children:["Skip events on the ",(0,o.jsx)(t.code,{children:"pull.stream$"})," for the client that caused the changes to improve performance."]}),"\n"]})]})}function d(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>s,x:()=>i});var o=n(6540);const r={},a=o.createContext(r);function s(e){const t=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),o.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/a69eebfc.726cdfe7.js b/docs/assets/js/a69eebfc.726cdfe7.js deleted file mode 100644 index b2c9970af23..00000000000 --- a/docs/assets/js/a69eebfc.726cdfe7.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9408],{4120:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>o,default:()=>c,frontMatter:()=>s,metadata:()=>a,toc:()=>u});var i=n(4848),r=n(8453);const s={title:"Query Optimizer \ud83d\udc51",slug:"query-optimizer.html"},o="Query Optimizer",a={id:"query-optimizer",title:"Query Optimizer \ud83d\udc51",description:"The query optimizer can be used to determine which index is the best to use for a given query.",source:"@site/docs/query-optimizer.md",sourceDirName:".",slug:"/query-optimizer.html",permalink:"/query-optimizer.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Query Optimizer \ud83d\udc51",slug:"query-optimizer.html"},sidebar:"tutorialSidebar",previous:{title:"ORM",permalink:"/orm.html"},next:{title:"Logger \ud83d\udc51",permalink:"/logger.html"}},d={},u=[{value:"Usage",id:"usage",level:2},{value:"Important details",id:"important-details",level:2}];function l(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"query-optimizer",children:"Query Optimizer"}),"\n",(0,i.jsx)(t.p,{children:"The query optimizer can be used to determine which index is the best to use for a given query.\nBecause RxDB is used in client side applications, it cannot do any background checks or measurements to optimize the query plan because that would cause significant performance problems."}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"NOTICE:"})," The query optimizer is part of the ",(0,i.jsx)(t.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"})," plugin that must be purchased. It is not part of the default RxDB module."]}),"\n",(0,i.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import {\n findBestIndex\n} from 'rxdb-premium/plugins/query-optimizer';\n\nimport { \n getRxStorageIndexedDB\n} from 'rxdb-premium/plugins/indexeddb';\n\nconst bestIndexes = await findBestIndex({\n schema: myRxJsonSchema,\n /**\n * In this example we use the IndexedDB RxStorage,\n * but any other storage can be used for testing.\n */\n storage: getRxStorageIndexedDB(),\n /**\n * Multiple queries can be optimized at the same time\n * which decreases the overall runtime.\n */\n queries: {\n /**\n * Queries can be mapped by a query id,\n * here we use myFirstQuery as query id.\n */\n myFirstQuery: {\n selector: {\n age: {\n $gt: 10\n }\n },\n },\n mySecondQuery: {\n selector: {\n age: {\n $gt: 10\n },\n lastName: {\n $eq: 'Nakamoto'\n }\n },\n }\n },\n testData: [/** data for the documents. **/]\n});\n\n"})}),"\n",(0,i.jsx)(t.h2,{id:"important-details",children:"Important details"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:["This is a build time tool. You should use it to find the best indexes for your queries during ",(0,i.jsx)(t.strong,{children:"build time"}),". Then you store these results and you application can use the best indexes during ",(0,i.jsx)(t.strong,{children:"run time"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:["It makes no sense to run time optimization with a different ",(0,i.jsx)(t.code,{children:"RxStorage"})," (+settings) that what you use in production. The result of the query optimizer is heavily dependent on the RxStorage and JavaScript runtime. For example it makes no sense to run the optimization in Node.js and then use the optimized indexes in the browser."]}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:["It is very important the you use ",(0,i.jsx)(t.strong,{children:"production like"})," ",(0,i.jsx)(t.code,{children:"testData"}),". Finding the best index heavily depends on data distribution and amount of stored/queried documents. For example if you store and query users with an ",(0,i.jsx)(t.code,{children:"age"})," field, it makes no sense to just use a random number for the age because in production the ",(0,i.jsx)(t.code,{children:"age"})," of your users is not equally distributed."]}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:["The higher you set ",(0,i.jsx)(t.code,{children:"runs"}),", the more test cycles will be performed and the more ",(0,i.jsx)(t.strong,{children:"significant"})," will be the time measurements which leads to a better index selection."]}),"\n"]}),"\n"]})]})}function c(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>o,x:()=>a});var i=n(6540);const r={},s=i.createContext(r);function o(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/a69eebfc.fe82e265.js b/docs/assets/js/a69eebfc.fe82e265.js new file mode 100644 index 00000000000..f440922a551 --- /dev/null +++ b/docs/assets/js/a69eebfc.fe82e265.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9408],{4120:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>o,default:()=>c,frontMatter:()=>s,metadata:()=>a,toc:()=>u});var i=n(4848),r=n(8453);const s={title:"Query Optimizer \ud83d\udc51",slug:"query-optimizer.html"},o="Query Optimizer",a={id:"query-optimizer",title:"Query Optimizer \ud83d\udc51",description:"The query optimizer can be used to determine which index is the best to use for a given query.",source:"@site/docs/query-optimizer.md",sourceDirName:".",slug:"/query-optimizer.html",permalink:"/query-optimizer.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Query Optimizer \ud83d\udc51",slug:"query-optimizer.html"},sidebar:"tutorialSidebar",previous:{title:"ORM",permalink:"/orm.html"},next:{title:"Logger \ud83d\udc51",permalink:"/logger.html"}},d={},u=[{value:"Usage",id:"usage",level:2},{value:"Important details",id:"important-details",level:2}];function l(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"query-optimizer",children:"Query Optimizer"}),"\n",(0,i.jsx)(t.p,{children:"The query optimizer can be used to determine which index is the best to use for a given query.\nBecause RxDB is used in client side applications, it cannot do any background checks or measurements to optimize the query plan because that would cause significant performance problems."}),"\n",(0,i.jsx)(t.admonition,{type:"note",children:(0,i.jsxs)(t.p,{children:["The query optimizer is part of the ",(0,i.jsx)(t.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"})," plugin that must be purchased. It is not part of the default RxDB module."]})}),"\n",(0,i.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import {\n findBestIndex\n} from 'rxdb-premium/plugins/query-optimizer';\n\nimport { \n getRxStorageIndexedDB\n} from 'rxdb-premium/plugins/indexeddb';\n\nconst bestIndexes = await findBestIndex({\n schema: myRxJsonSchema,\n /**\n * In this example we use the IndexedDB RxStorage,\n * but any other storage can be used for testing.\n */\n storage: getRxStorageIndexedDB(),\n /**\n * Multiple queries can be optimized at the same time\n * which decreases the overall runtime.\n */\n queries: {\n /**\n * Queries can be mapped by a query id,\n * here we use myFirstQuery as query id.\n */\n myFirstQuery: {\n selector: {\n age: {\n $gt: 10\n }\n },\n },\n mySecondQuery: {\n selector: {\n age: {\n $gt: 10\n },\n lastName: {\n $eq: 'Nakamoto'\n }\n },\n }\n },\n testData: [/** data for the documents. **/]\n});\n\n"})}),"\n",(0,i.jsx)(t.h2,{id:"important-details",children:"Important details"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:["This is a build time tool. You should use it to find the best indexes for your queries during ",(0,i.jsx)(t.strong,{children:"build time"}),". Then you store these results and you application can use the best indexes during ",(0,i.jsx)(t.strong,{children:"run time"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:["It makes no sense to run time optimization with a different ",(0,i.jsx)(t.code,{children:"RxStorage"})," (+settings) that what you use in production. The result of the query optimizer is heavily dependent on the RxStorage and JavaScript runtime. For example it makes no sense to run the optimization in Node.js and then use the optimized indexes in the browser."]}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:["It is very important the you use ",(0,i.jsx)(t.strong,{children:"production like"})," ",(0,i.jsx)(t.code,{children:"testData"}),". Finding the best index heavily depends on data distribution and amount of stored/queried documents. For example if you store and query users with an ",(0,i.jsx)(t.code,{children:"age"})," field, it makes no sense to just use a random number for the age because in production the ",(0,i.jsx)(t.code,{children:"age"})," of your users is not equally distributed."]}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:["The higher you set ",(0,i.jsx)(t.code,{children:"runs"}),", the more test cycles will be performed and the more ",(0,i.jsx)(t.strong,{children:"significant"})," will be the time measurements which leads to a better index selection."]}),"\n"]}),"\n"]})]})}function c(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>o,x:()=>a});var i=n(6540);const r={},s=i.createContext(r);function o(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/aa14e6b1.6a1ab27a.js b/docs/assets/js/aa14e6b1.6a1ab27a.js new file mode 100644 index 00000000000..8226304e2b3 --- /dev/null +++ b/docs/assets/js/aa14e6b1.6a1ab27a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9824],{3268:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>o,toc:()=>c});var r=t(4848),i=t(8453);const a={title:"GraphQL Replication",slug:"replication-graphql.html"},s="Replication with GraphQL",o={id:"replication-graphql",title:"GraphQL Replication",description:"The GraphQL replication provides handlers for GraphQL to run replication with GraphQL as the transportation layer.",source:"@site/docs/replication-graphql.md",sourceDirName:".",slug:"/replication-graphql.html",permalink:"/replication-graphql.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"GraphQL Replication",slug:"replication-graphql.html"},sidebar:"tutorialSidebar",previous:{title:"RxDB Server Replication",permalink:"/replication-server"},next:{title:"Websocket Replication",permalink:"/replication-websocket.html"}},l={},c=[{value:"Usage",id:"usage",level:2},{value:"Creating a compatible GraphQL Server",id:"creating-a-compatible-graphql-server",level:3},{value:"RxDB Client",id:"rxdb-client",level:3},{value:"Pull replication",id:"pull-replication",level:4},{value:"Push replication",id:"push-replication",level:4},{value:"Pull Stream",id:"pull-stream",level:4},{value:"Transforming null to undefined in optional fields",id:"transforming-null-to-undefined-in-optional-fields",level:3},{value:"pull.responseModifier",id:"pullresponsemodifier",level:3},{value:"push.responseModifier",id:"pushresponsemodifier",level:3},{value:"Helper Functions",id:"helper-functions",level:4},{value:"RxGraphQLReplicationState",id:"rxgraphqlreplicationstate",level:3},{value:".setHeaders()",id:"setheaders",level:4},{value:"Sending Cookies",id:"sending-cookies",level:4}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",p:"p",pre:"pre",strong:"strong",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"replication-with-graphql",children:"Replication with GraphQL"}),"\n",(0,r.jsxs)(n.p,{children:["The GraphQL replication provides handlers for GraphQL to run ",(0,r.jsx)(n.a,{href:"/replication.html",children:"replication"})," with GraphQL as the transportation layer."]}),"\n",(0,r.jsxs)(n.p,{children:["The GraphQL replication is mostly used when you already have a backend that exposes a GraphQL API that can be adjusted to serve as a replication endpoint. If you do not already have a GraphQL endpoint, using the ",(0,r.jsx)(n.a,{href:"/replication-http.html",children:"HTTP replication"})," is an easier solution."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["To play around, check out the full example of the RxDB ",(0,r.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/graphql",children:"GraphQL replication with server and client"})]})}),"\n",(0,r.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsxs)(n.p,{children:["Before you use the GraphQL replication, make sure you've learned how the ",(0,r.jsx)(n.a,{href:"/replication.html",children:"RxDB replication"})," works."]}),"\n",(0,r.jsx)(n.h3,{id:"creating-a-compatible-graphql-server",children:"Creating a compatible GraphQL Server"}),"\n",(0,r.jsxs)(n.p,{children:["At the server-side, there must exist an endpoint which returns newer rows when the last ",(0,r.jsx)(n.code,{children:"checkpoint"})," is used as input. For example lets say you create a ",(0,r.jsx)(n.code,{children:"Query"})," ",(0,r.jsx)(n.code,{children:"pullHuman"})," which returns a list of document writes that happened after the given checkpoint."]}),"\n",(0,r.jsxs)(n.p,{children:["For the push-replication, you also need a ",(0,r.jsx)(n.code,{children:"Mutation"})," ",(0,r.jsx)(n.code,{children:"pushHuman"})," which lets RxDB update data of documents by sending the previous document state and the new client document state.\nAlso for being able to stream all ongoing events, we need a ",(0,r.jsx)(n.code,{children:"Subscription"})," called ",(0,r.jsx)(n.code,{children:"streamHuman"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-graphql",children:"input HumanInput {\n id: ID!,\n name: String!,\n lastName: String!,\n updatedAt: Float!,\n deleted: Boolean!\n}\ntype Human {\n id: ID!,\n name: String!,\n lastName: String!,\n updatedAt: Float!,\n deleted: Boolean!\n}\ninput Checkpoint {\n id: String!,\n updatedAt: Float!\n}\ntype HumanPullBulk {\n documents: [Human]!\n checkpoint: Checkpoint\n}\n\ntype Query {\n pullHuman(checkpoint: Checkpoint, limit: Int!): HumanPullBulk!\n}\n\ninput HumanInputPushRow {\n assumedMasterState: HeroInputPushRowT0AssumedMasterStateT0\n newDocumentState: HeroInputPushRowT0NewDocumentStateT0!\n}\n\ntype Mutation {\n # Returns a list of all conflicts\n # If no document write caused a conflict, return an empty list.\n pushHuman(rows: [HumanInputPushRow!]): [Human]\n}\n\n# headers are used to authenticate the subscriptions\n# over websockets.\ninput Headers {\n AUTH_TOKEN: String!;\n}\ntype Subscription {\n streamHuman(headers: Headers): HumanPullBulk!\n}\n\n"})}),"\n",(0,r.jsxs)(n.p,{children:["The GraphQL resolver for the ",(0,r.jsx)(n.code,{children:"pullHuman"})," would then look like:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const rootValue = {\n pullHuman: args => {\n const minId = args.checkpoint ? args.checkpoint.id : '';\n const minUpdatedAt = args.checkpoint ? args.checkpoint.updatedAt : 0;\n\n // sorted by updatedAt first and the id as second\n const sortedDocuments = documents.sort((a, b) => {\n if (a.updatedAt > b.updatedAt) return 1;\n if (a.updatedAt < b.updatedAt) return -1;\n if (a.updatedAt === b.updatedAt) {\n if (a.id > b.id) return 1;\n if (a.id < b.id) return -1;\n else return 0;\n }\n });\n\n // only return documents newer than the input document\n const filterForMinUpdatedAtAndId = sortedDocuments.filter(doc => {\n if (doc.updatedAt < minUpdatedAt) return false;\n if (doc.updatedAt > minUpdatedAt) return true;\n if (doc.updatedAt === minUpdatedAt) {\n // if updatedAt is equal, compare by id\n if (doc.id > minId) return true;\n else return false;\n }\n });\n\n // only return some documents in one batch\n const limitedDocs = filterForMinUpdatedAtAndId.slice(0, args.limit);\n\n // use the last document for the checkpoint\n const lastDoc = limitedDocs[limitedDocs.length - 1];\n const retCheckpoint = {\n id: lastDoc.id,\n updatedAt: lastDoc.updatedAt\n }\n\n return {\n documents: limitedDocs,\n checkpoint: retCheckpoint\n }\n\n return limited;\n }\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["For examples for the other resolvers, consult the ",(0,r.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/blob/master/examples/graphql/server/index.js",children:"GraphQL Example Project"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"rxdb-client",children:"RxDB Client"}),"\n",(0,r.jsx)(n.h4,{id:"pull-replication",children:"Pull replication"}),"\n",(0,r.jsxs)(n.p,{children:["For the pull-replication, you first need a ",(0,r.jsx)(n.code,{children:"pullQueryBuilder"}),". This is a function that gets the last replication ",(0,r.jsx)(n.code,{children:"checkpoint"})," and a ",(0,r.jsx)(n.code,{children:"limit"})," as input and returns an object with a GraphQL-query and its variables (or a promise that resolves to the same object). RxDB will use the query builder to construct what is later sent to the GraphQL endpoint."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const pullQueryBuilder = (checkpoint, limit) => {\n /**\n * The first pull does not have a checkpoint\n * so we fill it up with defaults\n */\n if (!checkpoint) {\n checkpoint = {\n id: '',\n updatedAt: 0\n };\n }\n const query = `query PullHuman($checkpoint: CheckpointInput, $limit: Int!) {\n pullHuman(checkpoint: $checkpoint, limit: $limit) {\n documents {\n id\n name\n age\n updatedAt\n deleted\n }\n checkpoint {\n id\n updatedAt\n }\n }\n }`;\n return {\n query,\n operationName: 'PullHuman',\n variables: {\n checkpoint,\n limit\n }\n };\n};\n"})}),"\n",(0,r.jsx)(n.p,{children:"With the queryBuilder, you can then setup the pull-replication."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"import { replicateGraphQL } from 'rxdb/plugins/replication-graphql';\nconst replicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n // urls to the GraphQL endpoints\n url: {\n http: 'http://example.com/graphql'\n },\n pull: {\n queryBuilder: pullQueryBuilder, // the queryBuilder from above\n modifier: doc => doc, // (optional) modifies all pulled documents before they are handled by RxDB\n dataPath: undefined, // (optional) specifies the object path to access the document(s). Otherwise, the first result of the response data is used.\n /**\n * Amount of documents that the remote will send in one request.\n * If the response contains less then [batchSize] documents,\n * RxDB will assume there are no more changes on the backend\n * that are not replicated.\n * This value is the same as the limit in the pullHuman() schema.\n * [default=100]\n */\n batchSize: 50\n },\n // headers which will be used in http requests against the server.\n headers: {\n Authorization: 'Bearer abcde...'\n },\n\n /**\n * Options that have been inherited from the RxReplication\n */\n deletedField: 'deleted',\n live: true,\n retryTime = 1000 * 5,\n waitForLeadership = true,\n autoStart = true,\n }\n);\n"})}),"\n",(0,r.jsx)(n.h4,{id:"push-replication",children:"Push replication"}),"\n",(0,r.jsxs)(n.p,{children:["For the push-replication, you also need a ",(0,r.jsx)(n.code,{children:"queryBuilder"}),". Here, the builder receives a changed document as input which has to be send to the server. It also returns a GraphQL-Query and its data."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const pushQueryBuilder = rows => {\n const query = `\n mutation PushHuman($writeRows: [HumanInputPushRow!]) {\n pushHuman(writeRows: $writeRows) {\n id\n name\n age\n updatedAt\n deleted\n }\n }\n `;\n const variables = {\n writeRows: rows\n };\n return {\n query,\n operationName: 'PushHuman',\n variables\n };\n};\n"})}),"\n",(0,r.jsx)(n.p,{children:"With the queryBuilder, you can then setup the push-replication."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const replicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n // urls to the GraphQL endpoints\n url: {\n http: 'http://example.com/graphql'\n },\n push: {\n queryBuilder: pushQueryBuilder, // the queryBuilder from above\n /**\n * batchSize (optional)\n * Amount of document that will be pushed to the server in a single request.\n */\n batchSize: 5,\n /**\n * modifier (optional)\n * Modifies all pushed documents before they are send to the GraphQL endpoint.\n * Returning null will skip the document.\n */\n modifier: doc => doc\n },\n headers: {\n Authorization: 'Bearer abcde...'\n },\n pull: {\n /* ... */\n },\n /* ... */\n }\n);\n"})}),"\n",(0,r.jsx)(n.h4,{id:"pull-stream",children:"Pull Stream"}),"\n",(0,r.jsxs)(n.p,{children:["To create a ",(0,r.jsx)(n.strong,{children:"realtime"})," replication, you need to create a pull stream that pulls ongoing writes from the server.\nThe pull stream gets the ",(0,r.jsx)(n.code,{children:"headers"})," of the ",(0,r.jsx)(n.code,{children:"RxReplicationState"})," as input, so that it can be authenticated on the backend."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const pullStreamQueryBuilder = (headers) => {\n const query = `subscription onStream($headers: Headers) {\n streamHero(headers: $headers) {\n documents {\n id,\n name,\n age,\n updatedAt,\n deleted\n },\n checkpoint {\n id\n updatedAt\n }\n }\n }`;\n return {\n query,\n variables: {\n headers\n }\n };\n};\n"})}),"\n",(0,r.jsxs)(n.p,{children:["With the ",(0,r.jsx)(n.code,{children:"pullStreamQueryBuilder"})," you can then start a realtime replication."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const replicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n // urls to the GraphQL endpoints\n url: {\n http: 'http://example.com/graphql',\n ws: 'ws://example.com/subscriptions' // <- The websocket has to use a different url.\n },\n push: {\n batchSize: 100,\n queryBuilder: pushQueryBuilder\n },\n headers: {\n Authorization: 'Bearer abcde...'\n },\n pull: {\n batchSize: 100,\n queryBuilder: pullQueryBuilder,\n streamQueryBuilder: pullStreamQueryBuilder,\n includeWsHeaders: false, // Includes headers as connection parameter to Websocket.\n },\n deletedField: 'deleted'\n }\n);\n"})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["If it is not possible to create a websocket server on your backend, you can use any other method of pull out the ongoing events from the backend and then you can send them into ",(0,r.jsx)(n.code,{children:"RxReplicationState.emitEvent()"}),"."]})}),"\n",(0,r.jsx)(n.h3,{id:"transforming-null-to-undefined-in-optional-fields",children:"Transforming null to undefined in optional fields"}),"\n",(0,r.jsxs)(n.p,{children:["GraphQL fills up non-existent optional values with ",(0,r.jsx)(n.code,{children:"null"})," while RxDB required them to be ",(0,r.jsx)(n.code,{children:"undefined"}),".\nTherefore, if your schema contains optional properties, you have to transform the pulled data to switch out ",(0,r.jsx)(n.code,{children:"null"})," to ",(0,r.jsx)(n.code,{children:"undefined"})]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const replicationState: RxGraphQLReplicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n url: {/* ... */},\n headers: {/* ... */},\n push: {/* ... */},\n pull: {\n queryBuilder: pullQueryBuilder,\n modifier: (doc => {\n // We have to remove optional non-existent field values\n // they are set as null by GraphQL but should be undefined\n Object.entries(doc).forEach(([k, v]) => {\n if (v === null) {\n delete doc[k];\n }\n });\n return doc;\n })\n },\n /* ... */\n }\n);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"pullresponsemodifier",children:"pull.responseModifier"}),"\n",(0,r.jsxs)(n.p,{children:["With the ",(0,r.jsx)(n.code,{children:"pull.responseModifier"})," you can modify the whole response from the GraphQL endpoint ",(0,r.jsx)(n.strong,{children:"before"})," it is processed by RxDB.\nFor example if your endpoint is not capable of returning a valid checkpoint, but instead only returns the plain document array, you can use the ",(0,r.jsx)(n.code,{children:"responseModifier"})," to aggregate the checkpoint from the returned documents."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"import {\n\n} from 'rxdb';\nconst replicationState: RxGraphQLReplicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n url: {/* ... */},\n headers: {/* ... */},\n push: {/* ... */},\n pull: {\n responseModifier: async function(\n plainResponse, // the exact response that was returned from the server\n origin, // either 'handler' if plainResponse came from the pull.handler, or 'stream' if it came from the pull.stream\n requestCheckpoint // if origin==='handler', the requestCheckpoint contains the checkpoint that was send to the backend\n ) {\n /**\n * In this example we aggregate the checkpoint from the documents array\n * that was returned from the graphql endpoint.\n */\n const docs = plainResponse;\n return {\n documents: docs,\n checkpoint: docs.length === 0 ? requestCheckpoint : {\n name: lastOfArray(docs).name,\n updatedAt: lastOfArray(docs).updatedAt\n }\n };\n }\n },\n /* ... */\n }\n);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"pushresponsemodifier",children:"push.responseModifier"}),"\n",(0,r.jsx)(n.p,{children:"It's also possible to modify the response of a push mutation. For example if your server returns more than the just conflicting docs:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-graphql",children:"type PushResponse {\n conflicts: [Human]\n conflictMessages: [ReplicationConflictMessage]\n}\n\ntype Mutation {\n # Returns a PushResponse type that contains the conflicts along with other information\n pushHuman(rows: [HumanInputPushRow!]): PushResponse!\n}\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:'import {} from "rxdb";\nconst replicationState: RxGraphQLReplicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n url: {/* ... */},\n headers: {/* ... */},\n push: {\n responseModifier: async function (plainResponse) {\n /**\n * In this example we aggregate the conflicting documents from a response object\n */\n return plainResponse.conflicts;\n },\n },\n pull: {/* ... */},\n /* ... */\n }\n);\n'})}),"\n",(0,r.jsx)(n.h4,{id:"helper-functions",children:"Helper Functions"}),"\n",(0,r.jsxs)(n.p,{children:["RxDB provides the helper functions ",(0,r.jsx)(n.code,{children:"graphQLSchemaFromRxSchema()"}),", ",(0,r.jsx)(n.code,{children:"pullQueryBuilderFromRxSchema()"}),", ",(0,r.jsx)(n.code,{children:"pullStreamBuilderFromRxSchema()"})," and ",(0,r.jsx)(n.code,{children:"pushQueryBuilderFromRxSchema()"})," that can be used to generate handlers and schemas from the ",(0,r.jsx)(n.code,{children:"RxJsonSchema"}),". To learn how to use them, please inspect the ",(0,r.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/graphql",children:"GraphQL Example"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"rxgraphqlreplicationstate",children:"RxGraphQLReplicationState"}),"\n",(0,r.jsxs)(n.p,{children:["When you call ",(0,r.jsx)(n.code,{children:"myCollection.syncGraphQL()"})," it returns a ",(0,r.jsx)(n.code,{children:"RxGraphQLReplicationState"})," which can be used to subscribe to events, for debugging or other functions. It extends the ",(0,r.jsx)(n.a,{href:"/replication.html",children:"RxReplicationState"})," with some GraphQL specific methods."]}),"\n",(0,r.jsx)(n.h4,{id:"setheaders",children:".setHeaders()"}),"\n",(0,r.jsx)(n.p,{children:"Changes the headers for the replication after it has been set up."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"replicationState.setHeaders({\n Authorization: `...`\n});\n"})}),"\n",(0,r.jsx)(n.h4,{id:"sending-cookies",children:"Sending Cookies"}),"\n",(0,r.jsxs)(n.p,{children:["The underlying fetch framework uses a ",(0,r.jsx)(n.code,{children:"same-origin"})," policy for credentials per default. That means, cookies and session data is only shared if you backend and frontend run on the same domain and port. Pass the credential parameter to ",(0,r.jsx)(n.code,{children:"include"})," cookies in requests to servers from different origins via:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"replicationState.setCredentials('include');\n"})}),"\n",(0,r.jsxs)(n.p,{children:["or directly pass it in the the ",(0,r.jsx)(n.code,{children:"syncGraphQL"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"replicateGraphQL(\n {\n collection: myRxCollection,\n /* ... */\n credentials: 'include',\n /* ... */\n }\n);\n"})}),"\n",(0,r.jsxs)(n.p,{children:["See ",(0,r.jsx)(n.a,{href:"https://fetch.spec.whatwg.org/#concept-request-credentials-mode",children:"the fetch spec"})," for more information about available options."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["To play around, check out the full example of the RxDB ",(0,r.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/graphql",children:"GraphQL replication with server and client"})]})})]})}function h(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>s,x:()=>o});var r=t(6540);const i={},a=r.createContext(i);function s(e){const n=r.useContext(a);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/aa14e6b1.8c72d55d.js b/docs/assets/js/aa14e6b1.8c72d55d.js deleted file mode 100644 index 2351bfcae54..00000000000 --- a/docs/assets/js/aa14e6b1.8c72d55d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9824],{3268:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>o,toc:()=>c});var r=t(4848),i=t(8453);const a={title:"GraphQL Replication",slug:"replication-graphql.html"},s="Replication with GraphQL",o={id:"replication-graphql",title:"GraphQL Replication",description:"The GraphQL replication provides handlers for GraphQL to run replication with GraphQL as the transportation layer.",source:"@site/docs/replication-graphql.md",sourceDirName:".",slug:"/replication-graphql.html",permalink:"/replication-graphql.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"GraphQL Replication",slug:"replication-graphql.html"},sidebar:"tutorialSidebar",previous:{title:"RxDB Server Replication",permalink:"/replication-server"},next:{title:"Websocket Replication",permalink:"/replication-websocket.html"}},l={},c=[{value:"Usage",id:"usage",level:2},{value:"Creating a compatible GraphQL Server",id:"creating-a-compatible-graphql-server",level:3},{value:"RxDB Client",id:"rxdb-client",level:3},{value:"Pull replication",id:"pull-replication",level:4},{value:"Push replication",id:"push-replication",level:4},{value:"Pull Stream",id:"pull-stream",level:4},{value:"Transforming null to undefined in optional fields",id:"transforming-null-to-undefined-in-optional-fields",level:3},{value:"pull.responseModifier",id:"pullresponsemodifier",level:3},{value:"push.responseModifier",id:"pushresponsemodifier",level:3},{value:"Helper Functions",id:"helper-functions",level:4},{value:"RxGraphQLReplicationState",id:"rxgraphqlreplicationstate",level:3},{value:".setHeaders()",id:"setheaders",level:4},{value:"Sending Cookies",id:"sending-cookies",level:4}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",p:"p",pre:"pre",strong:"strong",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"replication-with-graphql",children:"Replication with GraphQL"}),"\n",(0,r.jsxs)(n.p,{children:["The GraphQL replication provides handlers for GraphQL to run ",(0,r.jsx)(n.a,{href:"/replication.html",children:"replication"})," with GraphQL as the transportation layer."]}),"\n",(0,r.jsxs)(n.p,{children:["The GraphQL replication is mostly used when you already have a backend that exposes a GraphQL API that can be adjusted to serve as a replication endpoint. If you do not already have a GraphQL endpoint, using the ",(0,r.jsx)(n.a,{href:"/replication-http.html",children:"HTTP replication"})," is an easier solution."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"NOTICE:"})," To play around, check out the full example of the RxDB ",(0,r.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/graphql",children:"GraphQL replication with server and client"})]}),"\n",(0,r.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsxs)(n.p,{children:["Before you use the GraphQL replication, make sure you've learned how the ",(0,r.jsx)(n.a,{href:"/replication.html",children:"RxDB replication"})," works."]}),"\n",(0,r.jsx)(n.h3,{id:"creating-a-compatible-graphql-server",children:"Creating a compatible GraphQL Server"}),"\n",(0,r.jsxs)(n.p,{children:["At the server-side, there must exist an endpoint which returns newer rows when the last ",(0,r.jsx)(n.code,{children:"checkpoint"})," is used as input. For example lets say you create a ",(0,r.jsx)(n.code,{children:"Query"})," ",(0,r.jsx)(n.code,{children:"pullHuman"})," which returns a list of document writes that happened after the given checkpoint."]}),"\n",(0,r.jsxs)(n.p,{children:["For the push-replication, you also need a ",(0,r.jsx)(n.code,{children:"Mutation"})," ",(0,r.jsx)(n.code,{children:"pushHuman"})," which lets RxDB update data of documents by sending the previous document state and the new client document state.\nAlso for being able to stream all ongoing events, we need a ",(0,r.jsx)(n.code,{children:"Subscription"})," called ",(0,r.jsx)(n.code,{children:"streamHuman"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-graphql",children:"input HumanInput {\n id: ID!,\n name: String!,\n lastName: String!,\n updatedAt: Float!,\n deleted: Boolean!\n}\ntype Human {\n id: ID!,\n name: String!,\n lastName: String!,\n updatedAt: Float!,\n deleted: Boolean!\n}\ninput Checkpoint {\n id: String!,\n updatedAt: Float!\n}\ntype HumanPullBulk {\n documents: [Human]!\n checkpoint: Checkpoint\n}\n\ntype Query {\n pullHuman(checkpoint: Checkpoint, limit: Int!): HumanPullBulk!\n}\n\ninput HumanInputPushRow {\n assumedMasterState: HeroInputPushRowT0AssumedMasterStateT0\n newDocumentState: HeroInputPushRowT0NewDocumentStateT0!\n}\n\ntype Mutation {\n # Returns a list of all conflicts\n # If no document write caused a conflict, return an empty list.\n pushHuman(rows: [HumanInputPushRow!]): [Human]\n}\n\n# headers are used to authenticate the subscriptions\n# over websockets.\ninput Headers {\n AUTH_TOKEN: String!;\n}\ntype Subscription {\n streamHuman(headers: Headers): HumanPullBulk!\n}\n\n"})}),"\n",(0,r.jsxs)(n.p,{children:["The GraphQL resolver for the ",(0,r.jsx)(n.code,{children:"pullHuman"})," would then look like:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const rootValue = {\n pullHuman: args => {\n const minId = args.checkpoint ? args.checkpoint.id : '';\n const minUpdatedAt = args.checkpoint ? args.checkpoint.updatedAt : 0;\n\n // sorted by updatedAt first and the id as second\n const sortedDocuments = documents.sort((a, b) => {\n if (a.updatedAt > b.updatedAt) return 1;\n if (a.updatedAt < b.updatedAt) return -1;\n if (a.updatedAt === b.updatedAt) {\n if (a.id > b.id) return 1;\n if (a.id < b.id) return -1;\n else return 0;\n }\n });\n\n // only return documents newer than the input document\n const filterForMinUpdatedAtAndId = sortedDocuments.filter(doc => {\n if (doc.updatedAt < minUpdatedAt) return false;\n if (doc.updatedAt > minUpdatedAt) return true;\n if (doc.updatedAt === minUpdatedAt) {\n // if updatedAt is equal, compare by id\n if (doc.id > minId) return true;\n else return false;\n }\n });\n\n // only return some documents in one batch\n const limitedDocs = filterForMinUpdatedAtAndId.slice(0, args.limit);\n\n // use the last document for the checkpoint\n const lastDoc = limitedDocs[limitedDocs.length - 1];\n const retCheckpoint = {\n id: lastDoc.id,\n updatedAt: lastDoc.updatedAt\n }\n\n return {\n documents: limitedDocs,\n checkpoint: retCheckpoint\n }\n\n return limited;\n }\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["For examples for the other resolvers, consult the ",(0,r.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/blob/master/examples/graphql/server/index.js",children:"GraphQL Example Project"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"rxdb-client",children:"RxDB Client"}),"\n",(0,r.jsx)(n.h4,{id:"pull-replication",children:"Pull replication"}),"\n",(0,r.jsxs)(n.p,{children:["For the pull-replication, you first need a ",(0,r.jsx)(n.code,{children:"pullQueryBuilder"}),". This is a function that gets the last replication ",(0,r.jsx)(n.code,{children:"checkpoint"})," and a ",(0,r.jsx)(n.code,{children:"limit"})," as input and returns an object with a GraphQL-query and its variables (or a promise that resolves to the same object). RxDB will use the query builder to construct what is later sent to the GraphQL endpoint."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const pullQueryBuilder = (checkpoint, limit) => {\n /**\n * The first pull does not have a checkpoint\n * so we fill it up with defaults\n */\n if (!checkpoint) {\n checkpoint = {\n id: '',\n updatedAt: 0\n };\n }\n const query = `query PullHuman($checkpoint: CheckpointInput, $limit: Int!) {\n pullHuman(checkpoint: $checkpoint, limit: $limit) {\n documents {\n id\n name\n age\n updatedAt\n deleted\n }\n checkpoint {\n id\n updatedAt\n }\n }\n }`;\n return {\n query,\n operationName: 'PullHuman',\n variables: {\n checkpoint,\n limit\n }\n };\n};\n"})}),"\n",(0,r.jsx)(n.p,{children:"With the queryBuilder, you can then setup the pull-replication."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"import { replicateGraphQL } from 'rxdb/plugins/replication-graphql';\nconst replicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n // urls to the GraphQL endpoints\n url: {\n http: 'http://example.com/graphql'\n },\n pull: {\n queryBuilder: pullQueryBuilder, // the queryBuilder from above\n modifier: doc => doc, // (optional) modifies all pulled documents before they are handled by RxDB\n dataPath: undefined, // (optional) specifies the object path to access the document(s). Otherwise, the first result of the response data is used.\n /**\n * Amount of documents that the remote will send in one request.\n * If the response contains less then [batchSize] documents,\n * RxDB will assume there are no more changes on the backend\n * that are not replicated.\n * This value is the same as the limit in the pullHuman() schema.\n * [default=100]\n */\n batchSize: 50\n },\n // headers which will be used in http requests against the server.\n headers: {\n Authorization: 'Bearer abcde...'\n },\n\n /**\n * Options that have been inherited from the RxReplication\n */\n deletedField: 'deleted',\n live: true,\n retryTime = 1000 * 5,\n waitForLeadership = true,\n autoStart = true,\n }\n);\n"})}),"\n",(0,r.jsx)(n.h4,{id:"push-replication",children:"Push replication"}),"\n",(0,r.jsxs)(n.p,{children:["For the push-replication, you also need a ",(0,r.jsx)(n.code,{children:"queryBuilder"}),". Here, the builder receives a changed document as input which has to be send to the server. It also returns a GraphQL-Query and its data."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const pushQueryBuilder = rows => {\n const query = `\n mutation PushHuman($writeRows: [HumanInputPushRow!]) {\n pushHuman(writeRows: $writeRows) {\n id\n name\n age\n updatedAt\n deleted\n }\n }\n `;\n const variables = {\n writeRows: rows\n };\n return {\n query,\n operationName: 'PushHuman',\n variables\n };\n};\n"})}),"\n",(0,r.jsx)(n.p,{children:"With the queryBuilder, you can then setup the push-replication."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const replicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n // urls to the GraphQL endpoints\n url: {\n http: 'http://example.com/graphql'\n },\n push: {\n queryBuilder: pushQueryBuilder, // the queryBuilder from above\n /**\n * batchSize (optional)\n * Amount of document that will be pushed to the server in a single request.\n */\n batchSize: 5,\n /**\n * modifier (optional)\n * Modifies all pushed documents before they are send to the GraphQL endpoint.\n * Returning null will skip the document.\n */\n modifier: doc => doc\n },\n headers: {\n Authorization: 'Bearer abcde...'\n },\n pull: {\n /* ... */\n },\n /* ... */\n }\n);\n"})}),"\n",(0,r.jsx)(n.h4,{id:"pull-stream",children:"Pull Stream"}),"\n",(0,r.jsxs)(n.p,{children:["To create a ",(0,r.jsx)(n.strong,{children:"realtime"})," replication, you need to create a pull stream that pulls ongoing writes from the server.\nThe pull stream gets the ",(0,r.jsx)(n.code,{children:"headers"})," of the ",(0,r.jsx)(n.code,{children:"RxReplicationState"})," as input, so that it can be authenticated on the backend."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const pullStreamQueryBuilder = (headers) => {\n const query = `subscription onStream($headers: Headers) {\n streamHero(headers: $headers) {\n documents {\n id,\n name,\n age,\n updatedAt,\n deleted\n },\n checkpoint {\n id\n updatedAt\n }\n }\n }`;\n return {\n query,\n variables: {\n headers\n }\n };\n};\n"})}),"\n",(0,r.jsxs)(n.p,{children:["With the ",(0,r.jsx)(n.code,{children:"pullStreamQueryBuilder"})," you can then start a realtime replication."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const replicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n // urls to the GraphQL endpoints\n url: {\n http: 'http://example.com/graphql',\n ws: 'ws://example.com/subscriptions' // <- The websocket has to use a different url.\n },\n push: {\n batchSize: 100,\n queryBuilder: pushQueryBuilder\n },\n headers: {\n Authorization: 'Bearer abcde...'\n },\n pull: {\n batchSize: 100,\n queryBuilder: pullQueryBuilder,\n streamQueryBuilder: pullStreamQueryBuilder,\n includeWsHeaders: false, // Includes headers as connection parameter to Websocket.\n },\n deletedField: 'deleted'\n }\n);\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"NOTICE"}),": If it is not possible to create a websocket server on your backend, you can use any other method of pull out the ongoing events from the backend and then you can send them into ",(0,r.jsx)(n.code,{children:"RxReplicationState.emitEvent()"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"transforming-null-to-undefined-in-optional-fields",children:"Transforming null to undefined in optional fields"}),"\n",(0,r.jsxs)(n.p,{children:["GraphQL fills up non-existent optional values with ",(0,r.jsx)(n.code,{children:"null"})," while RxDB required them to be ",(0,r.jsx)(n.code,{children:"undefined"}),".\nTherefore, if your schema contains optional properties, you have to transform the pulled data to switch out ",(0,r.jsx)(n.code,{children:"null"})," to ",(0,r.jsx)(n.code,{children:"undefined"})]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"const replicationState: RxGraphQLReplicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n url: {/* ... */},\n headers: {/* ... */},\n push: {/* ... */},\n pull: {\n queryBuilder: pullQueryBuilder,\n modifier: (doc => {\n // We have to remove optional non-existent field values\n // they are set as null by GraphQL but should be undefined\n Object.entries(doc).forEach(([k, v]) => {\n if (v === null) {\n delete doc[k];\n }\n });\n return doc;\n })\n },\n /* ... */\n }\n);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"pullresponsemodifier",children:"pull.responseModifier"}),"\n",(0,r.jsxs)(n.p,{children:["With the ",(0,r.jsx)(n.code,{children:"pull.responseModifier"})," you can modify the whole response from the GraphQL endpoint ",(0,r.jsx)(n.strong,{children:"before"})," it is processed by RxDB.\nFor example if your endpoint is not capable of returning a valid checkpoint, but instead only returns the plain document array, you can use the ",(0,r.jsx)(n.code,{children:"responseModifier"})," to aggregate the checkpoint from the returned documents."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"import {\n\n} from 'rxdb';\nconst replicationState: RxGraphQLReplicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n url: {/* ... */},\n headers: {/* ... */},\n push: {/* ... */},\n pull: {\n responseModifier: async function(\n plainResponse, // the exact response that was returned from the server\n origin, // either 'handler' if plainResponse came from the pull.handler, or 'stream' if it came from the pull.stream\n requestCheckpoint // if origin==='handler', the requestCheckpoint contains the checkpoint that was send to the backend\n ) {\n /**\n * In this example we aggregate the checkpoint from the documents array\n * that was returned from the graphql endpoint.\n */\n const docs = plainResponse;\n return {\n documents: docs,\n checkpoint: docs.length === 0 ? requestCheckpoint : {\n name: lastOfArray(docs).name,\n updatedAt: lastOfArray(docs).updatedAt\n }\n };\n }\n },\n /* ... */\n }\n);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"pushresponsemodifier",children:"push.responseModifier"}),"\n",(0,r.jsx)(n.p,{children:"It's also possible to modify the response of a push mutation. For example if your server returns more than the just conflicting docs:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-graphql",children:"type PushResponse {\n conflicts: [Human]\n conflictMessages: [ReplicationConflictMessage]\n}\n\ntype Mutation {\n # Returns a PushResponse type that contains the conflicts along with other information\n pushHuman(rows: [HumanInputPushRow!]): PushResponse!\n}\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:'import {} from "rxdb";\nconst replicationState: RxGraphQLReplicationState = replicateGraphQL(\n {\n collection: myRxCollection,\n url: {/* ... */},\n headers: {/* ... */},\n push: {\n responseModifier: async function (plainResponse) {\n /**\n * In this example we aggregate the conflicting documents from a response object\n */\n return plainResponse.conflicts;\n },\n },\n pull: {/* ... */},\n /* ... */\n }\n);\n'})}),"\n",(0,r.jsx)(n.h4,{id:"helper-functions",children:"Helper Functions"}),"\n",(0,r.jsxs)(n.p,{children:["RxDB provides the helper functions ",(0,r.jsx)(n.code,{children:"graphQLSchemaFromRxSchema()"}),", ",(0,r.jsx)(n.code,{children:"pullQueryBuilderFromRxSchema()"}),", ",(0,r.jsx)(n.code,{children:"pullStreamBuilderFromRxSchema()"})," and ",(0,r.jsx)(n.code,{children:"pushQueryBuilderFromRxSchema()"})," that can be used to generate handlers and schemas from the ",(0,r.jsx)(n.code,{children:"RxJsonSchema"}),". To learn how to use them, please inspect the ",(0,r.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/graphql",children:"GraphQL Example"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"rxgraphqlreplicationstate",children:"RxGraphQLReplicationState"}),"\n",(0,r.jsxs)(n.p,{children:["When you call ",(0,r.jsx)(n.code,{children:"myCollection.syncGraphQL()"})," it returns a ",(0,r.jsx)(n.code,{children:"RxGraphQLReplicationState"})," which can be used to subscribe to events, for debugging or other functions. It extends the ",(0,r.jsx)(n.a,{href:"/replication.html",children:"RxReplicationState"})," with some GraphQL specific methods."]}),"\n",(0,r.jsx)(n.h4,{id:"setheaders",children:".setHeaders()"}),"\n",(0,r.jsx)(n.p,{children:"Changes the headers for the replication after it has been set up."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"replicationState.setHeaders({\n Authorization: `...`\n});\n"})}),"\n",(0,r.jsx)(n.h4,{id:"sending-cookies",children:"Sending Cookies"}),"\n",(0,r.jsxs)(n.p,{children:["The underlying fetch framework uses a ",(0,r.jsx)(n.code,{children:"same-origin"})," policy for credentials per default. That means, cookies and session data is only shared if you backend and frontend run on the same domain and port. Pass the credential parameter to ",(0,r.jsx)(n.code,{children:"include"})," cookies in requests to servers from different origins via:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"replicationState.setCredentials('include');\n"})}),"\n",(0,r.jsxs)(n.p,{children:["or directly pass it in the the ",(0,r.jsx)(n.code,{children:"syncGraphQL"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"replicateGraphQL(\n {\n collection: myRxCollection,\n /* ... */\n credentials: 'include',\n /* ... */\n }\n);\n"})}),"\n",(0,r.jsxs)(n.p,{children:["See ",(0,r.jsx)(n.a,{href:"https://fetch.spec.whatwg.org/#concept-request-credentials-mode",children:"the fetch spec"})," for more information about available options."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"NOTICE:"})," To play around, check out the full example of the RxDB ",(0,r.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/graphql",children:"GraphQL replication with server and client"})]})]})}function h(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>s,x:()=>o});var r=t(6540);const i={},a=r.createContext(i);function s(e){const n=r.useContext(a);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/ac62b32d.31119efd.js b/docs/assets/js/ac62b32d.31119efd.js deleted file mode 100644 index 8ac7756a2a8..00000000000 --- a/docs/assets/js/ac62b32d.31119efd.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9192],{9585:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>i,metadata:()=>s,toc:()=>l});var o=n(4848),r=n(8453);const i={title:"Websocket Replication",slug:"replication-websocket.html"},a="Websocket Replication",s={id:"replication-websocket",title:"Websocket Replication",description:"With the websocket replication plugin, you can spawn a websocket server from a RxDB database in Node.js and replicate with it.",source:"@site/docs/replication-websocket.md",sourceDirName:".",slug:"/replication-websocket.html",permalink:"/replication-websocket.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Websocket Replication",slug:"replication-websocket.html"},sidebar:"tutorialSidebar",previous:{title:"GraphQL Replication",permalink:"/replication-graphql.html"},next:{title:"CouchDB Replication",permalink:"/replication-couchdb.html"}},c={},l=[{value:"Starting the Websocket Server",id:"starting-the-websocket-server",level:2},{value:"Connect to the Websocket Server",id:"connect-to-the-websocket-server",level:2},{value:"Customize",id:"customize",level:2}];function p(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"websocket-replication",children:"Websocket Replication"}),"\n",(0,o.jsx)(t.p,{children:"With the websocket replication plugin, you can spawn a websocket server from a RxDB database in Node.js and replicate with it."}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.strong,{children:"NOTICE"}),": The websocket replication plugin does not have any concept for authentication or permission handling. It is designed to create an easy ",(0,o.jsx)(t.strong,{children:"server-to-server"})," replication. It is ",(0,o.jsx)(t.strong,{children:"not"})," made for client-server replication. Make a pull request if you need that feature."]}),"\n",(0,o.jsx)(t.h2,{id:"starting-the-websocket-server",children:"Starting the Websocket Server"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"import { createRxDatabase } from 'rxdb';\nimport {\n startWebsocketServer\n} from 'rxdb/plugins/replication-websocket';\n\n// create a RxDatabase like normal\nconst myDatabase = await createRxDatabase({/* ... */});\n\n// start a websocket server\nconst serverState = await startWebsocketServer({\n database: myDatabase,\n port: 1337,\n path: '/socket'\n});\n\n// stop the server\nawait serverState.close();\n"})}),"\n",(0,o.jsx)(t.h2,{id:"connect-to-the-websocket-server",children:"Connect to the Websocket Server"}),"\n",(0,o.jsx)(t.p,{children:"The replication has to be started once for each collection that you want to replicate."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"import {\n replicateWithWebsocketServer\n} from 'rxdb/plugins/replication-websocket';\n\n// start the replication\nconst replicationState = await replicateWithWebsocketServer({\n /**\n * To make the replication work,\n * the client collection name must be equal\n * to the server collection name.\n */\n collection: myRxCollection,\n url: 'ws://localhost:1337/socket'\n});\n\n// stop the replication\nawait replicationState.cancel();\n"})}),"\n",(0,o.jsx)(t.h2,{id:"customize",children:"Customize"}),"\n",(0,o.jsxs)(t.p,{children:["We use the ",(0,o.jsx)(t.a,{href:"https://www.npmjs.com/package/ws",children:"ws"})," npm library, so you can use all optional configuration provided by it.\nThis is especially important to improve performance by opting in of some optional settings."]})]})}function h(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(p,{...e})}):p(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>a,x:()=>s});var o=n(6540);const r={},i=o.createContext(r);function a(e){const t=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),o.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/ac62b32d.8332e0f8.js b/docs/assets/js/ac62b32d.8332e0f8.js new file mode 100644 index 00000000000..742105a0d61 --- /dev/null +++ b/docs/assets/js/ac62b32d.8332e0f8.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[9192],{9585:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>r,metadata:()=>s,toc:()=>l});var o=n(4848),i=n(8453);const r={title:"Websocket Replication",slug:"replication-websocket.html"},a="Websocket Replication",s={id:"replication-websocket",title:"Websocket Replication",description:"With the websocket replication plugin, you can spawn a websocket server from a RxDB database in Node.js and replicate with it.",source:"@site/docs/replication-websocket.md",sourceDirName:".",slug:"/replication-websocket.html",permalink:"/replication-websocket.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Websocket Replication",slug:"replication-websocket.html"},sidebar:"tutorialSidebar",previous:{title:"GraphQL Replication",permalink:"/replication-graphql.html"},next:{title:"CouchDB Replication",permalink:"/replication-couchdb.html"}},c={},l=[{value:"Starting the Websocket Server",id:"starting-the-websocket-server",level:2},{value:"Connect to the Websocket Server",id:"connect-to-the-websocket-server",level:2},{value:"Customize",id:"customize",level:2}];function p(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,i.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"websocket-replication",children:"Websocket Replication"}),"\n",(0,o.jsx)(t.p,{children:"With the websocket replication plugin, you can spawn a websocket server from a RxDB database in Node.js and replicate with it."}),"\n",(0,o.jsx)(t.admonition,{type:"note",children:(0,o.jsxs)(t.p,{children:["The websocket replication plugin does not have any concept for authentication or permission handling. It is designed to create an easy ",(0,o.jsx)(t.strong,{children:"server-to-server"})," replication. It is ",(0,o.jsx)(t.strong,{children:"not"})," made for client-server replication. Make a pull request if you need that feature."]})}),"\n",(0,o.jsx)(t.h2,{id:"starting-the-websocket-server",children:"Starting the Websocket Server"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"import { createRxDatabase } from 'rxdb';\nimport {\n startWebsocketServer\n} from 'rxdb/plugins/replication-websocket';\n\n// create a RxDatabase like normal\nconst myDatabase = await createRxDatabase({/* ... */});\n\n// start a websocket server\nconst serverState = await startWebsocketServer({\n database: myDatabase,\n port: 1337,\n path: '/socket'\n});\n\n// stop the server\nawait serverState.close();\n"})}),"\n",(0,o.jsx)(t.h2,{id:"connect-to-the-websocket-server",children:"Connect to the Websocket Server"}),"\n",(0,o.jsx)(t.p,{children:"The replication has to be started once for each collection that you want to replicate."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"import {\n replicateWithWebsocketServer\n} from 'rxdb/plugins/replication-websocket';\n\n// start the replication\nconst replicationState = await replicateWithWebsocketServer({\n /**\n * To make the replication work,\n * the client collection name must be equal\n * to the server collection name.\n */\n collection: myRxCollection,\n url: 'ws://localhost:1337/socket'\n});\n\n// stop the replication\nawait replicationState.cancel();\n"})}),"\n",(0,o.jsx)(t.h2,{id:"customize",children:"Customize"}),"\n",(0,o.jsxs)(t.p,{children:["We use the ",(0,o.jsx)(t.a,{href:"https://www.npmjs.com/package/ws",children:"ws"})," npm library, so you can use all optional configuration provided by it.\nThis is especially important to improve performance by opting in of some optional settings."]})]})}function h(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(p,{...e})}):p(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>a,x:()=>s});var o=n(6540);const i={},r=o.createContext(i);function a(e){const t=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),o.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/ad16b3ea.7e05cdec.js b/docs/assets/js/ad16b3ea.7e05cdec.js deleted file mode 100644 index 1876250c926..00000000000 --- a/docs/assets/js/ad16b3ea.7e05cdec.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[8674],{2462:(e,n,d)=>{d.r(n),d.d(n,{assets:()=>o,contentTitle:()=>t,default:()=>u,frontMatter:()=>a,metadata:()=>l,toc:()=>r});var s=d(4848),i=d(8453);const a={title:"Dev-Mode Plugin",slug:"dev-mode.html"},t="Dev Mode",l={id:"dev-mode",title:"Dev-Mode Plugin",description:"The dev-mode plugin adds many checks and validations to RxDB.",source:"@site/docs/dev-mode.md",sourceDirName:".",slug:"/dev-mode.html",permalink:"/dev-mode.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Dev-Mode Plugin",slug:"dev-mode.html"},sidebar:"tutorialSidebar",previous:{title:"Installation",permalink:"/install.html"},next:{title:"RxDatabase",permalink:"/rx-database.html"}},o={},r=[{value:"Usage with Node.js",id:"usage-with-nodejs",level:2},{value:"Usage with Angular",id:"usage-with-angular",level:2},{value:"Usage with webpack",id:"usage-with-webpack",level:2},{value:"Disable the dev-mode warning",id:"disable-the-dev-mode-warning",level:2}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"dev-mode",children:"Dev Mode"}),"\n",(0,s.jsx)(n.p,{children:"The dev-mode plugin adds many checks and validations to RxDB.\nThis ensures that you use the RxDB API properly and so the dev-mode plugin should always be used when\nusing RxDB in development mode."}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Adds readable error messages."}),"\n",(0,s.jsxs)(n.li,{children:["Ensures that ",(0,s.jsx)(n.code,{children:"readonly"})," JavaScript objects are not accidentally mutated."]}),"\n",(0,s.jsxs)(n.li,{children:["Adds validation check for validity of schemas, queries, ORM methods and document fields.","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"NOTICE"}),": The ",(0,s.jsx)(n.code,{children:"dev-mode"})," plugin does not perform schema checks against the data see ",(0,s.jsx)(n.a,{href:"/schema-validation.html",children:"schema validation"})," for that."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"IMPORTANT"}),": The dev-mode plugin will increase your build size and decrease the performance. It must ",(0,s.jsx)(n.strong,{children:"always"})," be used in development. You should ",(0,s.jsx)(n.strong,{children:"never"})," use it in production."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode';\naddRxPlugin(RxDBDevModePlugin);\n"})}),"\n",(0,s.jsx)(n.h2,{id:"usage-with-nodejs",children:"Usage with Node.js"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"async function createDb() {\n if (process.env.NODE_ENV !== \"production\") {\n await import('rxdb/plugins/dev-mode').then(\n module => addRxPlugin(module.RxDBDevModePlugin)\n );\n }\n const db = createRxDatabase( /* ... */ );\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"usage-with-angular",children:"Usage with Angular"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"import { isDevMode } from '@angular/core';\n\nasync function createDb() {\n if (isDevMode()){\n await import('rxdb/plugins/dev-mode').then(\n module => addRxPlugin(module.RxDBDevModePlugin)\n );\n }\n\n const db = createRxDatabase( /* ... */ );\n // ...\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"usage-with-webpack",children:"Usage with webpack"}),"\n",(0,s.jsxs)(n.p,{children:["In the ",(0,s.jsx)(n.code,{children:"webpack.config.js"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"module.exports = {\n entry: './src/index.ts',\n /* ... */\n plugins: [\n // set a global variable that can be accessed during runtime\n new webpack.DefinePlugin({ MODE: JSON.stringify(\"production\") })\n ]\n /* ... */\n};\n"})}),"\n",(0,s.jsx)(n.p,{children:"In your source code:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"declare var MODE: 'production' | 'development';\n\nasync function createDb() {\n if (MODE === 'development') {\n await import('rxdb/plugins/dev-mode').then(\n module => addRxPlugin(module.RxDBDevModePlugin)\n );\n }\n const db = createRxDatabase( /* ... */ );\n // ...\n}\n\n"})}),"\n",(0,s.jsx)(n.h2,{id:"disable-the-dev-mode-warning",children:"Disable the dev-mode warning"}),"\n",(0,s.jsxs)(n.p,{children:["When the dev-mode is enabled, it will print a ",(0,s.jsx)(n.code,{children:"console.warn()"})," message to the console so that you do not accidentally use the dev-mode in production. To disable this warning you can call the ",(0,s.jsx)(n.code,{children:"disableWarnings()"})," function."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"import { disableWarnings } from 'rxdb/plugins/dev-mode';\ndisableWarnings();\n"})})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},8453:(e,n,d)=>{d.d(n,{R:()=>t,x:()=>l});var s=d(6540);const i={},a=s.createContext(i);function t(e){const n=s.useContext(a);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:t(e.components),s.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/ad16b3ea.8a3eb3f3.js b/docs/assets/js/ad16b3ea.8a3eb3f3.js new file mode 100644 index 00000000000..880284a9279 --- /dev/null +++ b/docs/assets/js/ad16b3ea.8a3eb3f3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[8674],{2462:(e,n,d)=>{d.r(n),d.d(n,{assets:()=>l,contentTitle:()=>t,default:()=>u,frontMatter:()=>a,metadata:()=>o,toc:()=>r});var s=d(4848),i=d(8453);const a={title:"Dev-Mode Plugin",slug:"dev-mode.html"},t="Dev Mode",o={id:"dev-mode",title:"Dev-Mode Plugin",description:"The dev-mode plugin adds many checks and validations to RxDB.",source:"@site/docs/dev-mode.md",sourceDirName:".",slug:"/dev-mode.html",permalink:"/dev-mode.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Dev-Mode Plugin",slug:"dev-mode.html"},sidebar:"tutorialSidebar",previous:{title:"Installation",permalink:"/install.html"},next:{title:"RxDatabase",permalink:"/rx-database.html"}},l={},r=[{value:"Usage with Node.js",id:"usage-with-nodejs",level:2},{value:"Usage with Angular",id:"usage-with-angular",level:2},{value:"Usage with webpack",id:"usage-with-webpack",level:2},{value:"Disable the dev-mode warning",id:"disable-the-dev-mode-warning",level:2}];function c(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"dev-mode",children:"Dev Mode"}),"\n",(0,s.jsx)(n.p,{children:"The dev-mode plugin adds many checks and validations to RxDB.\nThis ensures that you use the RxDB API properly and so the dev-mode plugin should always be used when\nusing RxDB in development mode."}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Adds readable error messages."}),"\n",(0,s.jsxs)(n.li,{children:["Ensures that ",(0,s.jsx)(n.code,{children:"readonly"})," JavaScript objects are not accidentally mutated."]}),"\n",(0,s.jsxs)(n.li,{children:["Adds validation check for validity of schemas, queries, ORM methods and document fields.","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Notice that the ",(0,s.jsx)(n.code,{children:"dev-mode"})," plugin does not perform schema checks against the data see ",(0,s.jsx)(n.a,{href:"/schema-validation.html",children:"schema validation"})," for that."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsxs)(n.p,{children:["The dev-mode plugin will increase your build size and decrease the performance. It must ",(0,s.jsx)(n.strong,{children:"always"})," be used in development. You should ",(0,s.jsx)(n.strong,{children:"never"})," use it in production."]})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode';\naddRxPlugin(RxDBDevModePlugin);\n"})}),"\n",(0,s.jsx)(n.h2,{id:"usage-with-nodejs",children:"Usage with Node.js"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"async function createDb() {\n if (process.env.NODE_ENV !== \"production\") {\n await import('rxdb/plugins/dev-mode').then(\n module => addRxPlugin(module.RxDBDevModePlugin)\n );\n }\n const db = createRxDatabase( /* ... */ );\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"usage-with-angular",children:"Usage with Angular"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"import { isDevMode } from '@angular/core';\n\nasync function createDb() {\n if (isDevMode()){\n await import('rxdb/plugins/dev-mode').then(\n module => addRxPlugin(module.RxDBDevModePlugin)\n );\n }\n\n const db = createRxDatabase( /* ... */ );\n // ...\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"usage-with-webpack",children:"Usage with webpack"}),"\n",(0,s.jsxs)(n.p,{children:["In the ",(0,s.jsx)(n.code,{children:"webpack.config.js"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"module.exports = {\n entry: './src/index.ts',\n /* ... */\n plugins: [\n // set a global variable that can be accessed during runtime\n new webpack.DefinePlugin({ MODE: JSON.stringify(\"production\") })\n ]\n /* ... */\n};\n"})}),"\n",(0,s.jsx)(n.p,{children:"In your source code:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"declare var MODE: 'production' | 'development';\n\nasync function createDb() {\n if (MODE === 'development') {\n await import('rxdb/plugins/dev-mode').then(\n module => addRxPlugin(module.RxDBDevModePlugin)\n );\n }\n const db = createRxDatabase( /* ... */ );\n // ...\n}\n\n"})}),"\n",(0,s.jsx)(n.h2,{id:"disable-the-dev-mode-warning",children:"Disable the dev-mode warning"}),"\n",(0,s.jsxs)(n.p,{children:["When the dev-mode is enabled, it will print a ",(0,s.jsx)(n.code,{children:"console.warn()"})," message to the console so that you do not accidentally use the dev-mode in production. To disable this warning you can call the ",(0,s.jsx)(n.code,{children:"disableWarnings()"})," function."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"import { disableWarnings } from 'rxdb/plugins/dev-mode';\ndisableWarnings();\n"})})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},8453:(e,n,d)=>{d.d(n,{R:()=>t,x:()=>o});var s=d(6540);const i={},a=s.createContext(i);function t(e){const n=s.useContext(a);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:t(e.components),s.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/b30f4f1f.4b6d3357.js b/docs/assets/js/b30f4f1f.4b6d3357.js new file mode 100644 index 00000000000..12770e858fa --- /dev/null +++ b/docs/assets/js/b30f4f1f.4b6d3357.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3779],{5830:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>r,contentTitle:()=>h,default:()=>d,frontMatter:()=>c,metadata:()=>i,toc:()=>l});var a=n(4848),s=n(8453);const c={title:"Attachments",slug:"rx-attachment.html"},h="Attachments",i={id:"rx-attachment",title:"Attachments",description:"Attachments are binary data files that can be attachment to an RxDocument, like a file that is attached to an email.",source:"@site/docs/rx-attachment.md",sourceDirName:".",slug:"/rx-attachment.html",permalink:"/rx-attachment.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Attachments",slug:"rx-attachment.html"},sidebar:"tutorialSidebar",previous:{title:"RxQuery",permalink:"/rx-query.html"},next:{title:"\u2699\ufe0f Rxstorage Layer",permalink:"/rx-storage.html"}},r={},l=[{value:"Add the attachments plugin",id:"add-the-attachments-plugin",level:2},{value:"Enable attachments in the schema",id:"enable-attachments-in-the-schema",level:2},{value:"putAttachment()",id:"putattachment",level:2},{value:"getAttachment()",id:"getattachment",level:2},{value:"allAttachments()",id:"allattachments",level:2},{value:"allAttachments$",id:"allattachments-1",level:2},{value:"RxAttachment",id:"rxattachment",level:2},{value:"doc",id:"doc",level:3},{value:"id",id:"id",level:3},{value:"type",id:"type",level:3},{value:"length",id:"length",level:3},{value:"digest",id:"digest",level:3},{value:"rev",id:"rev",level:3},{value:"remove()",id:"remove",level:3},{value:"getData()",id:"getdata",level:2},{value:"getStringData()",id:"getstringdata",level:2}];function o(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.h1,{id:"attachments",children:"Attachments"}),"\n",(0,a.jsxs)(t.p,{children:["Attachments are binary data files that can be attachment to an ",(0,a.jsx)(t.code,{children:"RxDocument"}),", like a file that is attached to an email."]}),"\n",(0,a.jsxs)(t.p,{children:["Using attachments instead of adding the data to the normal document, ensures that you still have a good ",(0,a.jsx)(t.strong,{children:"performance"})," when querying and writing documents, even when a big amount of data, like an image file has to be stored."]}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"You can store string, binary files, images and whatever you want side by side with your documents."}),"\n",(0,a.jsx)(t.li,{children:"Deleted documents automatically loose all their attachments data."}),"\n",(0,a.jsx)(t.li,{children:"Not all replication plugins support the replication of attachments."}),"\n",(0,a.jsxs)(t.li,{children:["Attachments can be stored ",(0,a.jsx)(t.a,{href:"/encryption.html",children:"encrypted"}),"."]}),"\n"]}),"\n",(0,a.jsxs)(t.p,{children:["Internally, attachments in RxDB are stored and handled similar to how ",(0,a.jsx)(t.a,{href:"https://pouchdb.com/guides/attachments.html#how-attachments-are-stored",children:"CouchDB, PouchDB"})," does it."]}),"\n",(0,a.jsx)(t.h2,{id:"add-the-attachments-plugin",children:"Add the attachments plugin"}),"\n",(0,a.jsxs)(t.p,{children:["To enable the attachments, you have to add the ",(0,a.jsx)(t.code,{children:"attachments"})," plugin."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBAttachmentsPlugin } from 'rxdb/plugins/attachments';\naddRxPlugin(RxDBAttachmentsPlugin);\n"})}),"\n",(0,a.jsx)(t.h2,{id:"enable-attachments-in-the-schema",children:"Enable attachments in the schema"}),"\n",(0,a.jsxs)(t.p,{children:["Before you can use attachments, you have to ensure that the attachments-object is set in the schema of your ",(0,a.jsx)(t.code,{children:"RxCollection"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"\nconst mySchema = {\n version: 0,\n type: 'object',\n properties: {\n // .\n // .\n // .\n },\n attachments: {\n encrypted: true // if true, the attachment-data will be encrypted with the db-password\n }\n};\n\nconst myCollection = await myDatabase.addCollections({\n humans: {\n schema: mySchema\n }\n});\n"})}),"\n",(0,a.jsx)(t.h2,{id:"putattachment",children:"putAttachment()"}),"\n",(0,a.jsxs)(t.p,{children:["Adds an attachment to a ",(0,a.jsx)(t.code,{children:"RxDocument"}),". Returns a Promise with the new attachment."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"import { createBlob } from 'rxdb';\n\nconst attachment = await myDocument.putAttachment(\n {\n id: 'cat.txt', // (string) name of the attachment\n data: createBlob('meowmeow', 'text/plain'), // (string|Blob) data of the attachment\n type: 'text/plain' // (string) type of the attachment-data like 'image/jpeg'\n }\n);\n"})}),"\n",(0,a.jsx)(t.h2,{id:"getattachment",children:"getAttachment()"}),"\n",(0,a.jsxs)(t.p,{children:["Returns an ",(0,a.jsx)(t.code,{children:"RxAttachment"})," by its id. Returns ",(0,a.jsx)(t.code,{children:"null"})," when the attachment does not exist."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachment = myDocument.getAttachment('cat.jpg');\n"})}),"\n",(0,a.jsx)(t.h2,{id:"allattachments",children:"allAttachments()"}),"\n",(0,a.jsxs)(t.p,{children:["Returns an array of all attachments of the ",(0,a.jsx)(t.code,{children:"RxDocument"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachments = myDocument.allAttachments();\n"})}),"\n",(0,a.jsx)(t.h2,{id:"allattachments-1",children:"allAttachments$"}),"\n",(0,a.jsx)(t.p,{children:"Gets an Observable which emits a stream of all attachments from the document. Re-emits each time an attachment gets added or removed from the RxDocument."}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const all = [];\nmyDocument.allAttachments$.subscribe(\n attachments => all = attachments\n);\n"})}),"\n",(0,a.jsx)(t.h2,{id:"rxattachment",children:"RxAttachment"}),"\n",(0,a.jsxs)(t.p,{children:["The attachments of RxDB are represented by the type ",(0,a.jsx)(t.code,{children:"RxAttachment"})," which has the following attributes/methods."]}),"\n",(0,a.jsx)(t.h3,{id:"doc",children:"doc"}),"\n",(0,a.jsxs)(t.p,{children:["The ",(0,a.jsx)(t.code,{children:"RxDocument"})," which the attachment is assigned to."]}),"\n",(0,a.jsx)(t.h3,{id:"id",children:"id"}),"\n",(0,a.jsxs)(t.p,{children:["The id as ",(0,a.jsx)(t.code,{children:"string"})," of the attachment."]}),"\n",(0,a.jsx)(t.h3,{id:"type",children:"type"}),"\n",(0,a.jsxs)(t.p,{children:["The type as ",(0,a.jsx)(t.code,{children:"string"})," of the attachment."]}),"\n",(0,a.jsx)(t.h3,{id:"length",children:"length"}),"\n",(0,a.jsxs)(t.p,{children:["The length of the data of the attachment as ",(0,a.jsx)(t.code,{children:"number"}),"."]}),"\n",(0,a.jsx)(t.h3,{id:"digest",children:"digest"}),"\n",(0,a.jsxs)(t.p,{children:["The hash of the attachments data as ",(0,a.jsx)(t.code,{children:"string"}),"."]}),"\n",(0,a.jsx)(t.admonition,{type:"note",children:(0,a.jsx)(t.p,{children:"The digest is NOT calculated by RxDB, instead it is calculated by the RxStorage. The only guarantee is that the digest will change when the attachments data changes."})}),"\n",(0,a.jsx)(t.h3,{id:"rev",children:"rev"}),"\n",(0,a.jsxs)(t.p,{children:["The revision-number of the attachment as ",(0,a.jsx)(t.code,{children:"number"}),"."]}),"\n",(0,a.jsx)(t.h3,{id:"remove",children:"remove()"}),"\n",(0,a.jsx)(t.p,{children:"Removes the attachment. Returns a Promise that resolves when done."}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachment = myDocument.getAttachment('cat.jpg');\nawait attachment.remove();\n"})}),"\n",(0,a.jsx)(t.h2,{id:"getdata",children:"getData()"}),"\n",(0,a.jsxs)(t.p,{children:["Returns a Promise which resolves the attachment's data as ",(0,a.jsx)(t.code,{children:"Blob"}),". (async)"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachment = myDocument.getAttachment('cat.jpg');\nconst blob = await attachment.getData();\n"})}),"\n",(0,a.jsx)(t.h2,{id:"getstringdata",children:"getStringData()"}),"\n",(0,a.jsxs)(t.p,{children:["Returns a Promise which resolves the attachment's data as ",(0,a.jsx)(t.code,{children:"string"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachment = await myDocument.getAttachment('cat.jpg');\nconst data = await attachment.getStringData();\n"})}),"\n",(0,a.jsx)(t.h1,{id:"attachment-compression",children:"Attachment compression"}),"\n",(0,a.jsxs)(t.p,{children:["Storing many attachments can be a problem when the disc space of the device is exceeded.\nTherefore it can make sense to compress the attachments before storing them in the ",(0,a.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"}),".\nWith the ",(0,a.jsx)(t.code,{children:"attachments-compression"})," plugin you can compress the attachments data on write and decompress it on reads.\nThis happens internally and will now change on how you use the api. The compression is run with the ",(0,a.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Compression_Streams_API",children:"Compression Streams API"})," which is only supported on ",(0,a.jsx)(t.a,{href:"https://caniuse.com/?search=compressionstream",children:"newer browsers"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"import {\n wrappedAttachmentsCompressionStorage\n} from 'rxdb/plugins/attachments-compression';\n\nimport { getRxStorageIndexedDB } from 'rxdb-premium/plugins/storage-indexeddb';\n\n// create a wrapped storage with attachment-compression.\nconst storageWithAttachmentsCompression = wrappedAttachmentsCompressionStorage({\n storage: getRxStorageIndexedDB()\n});\n\nconst db = await createRxDatabase({\n name: 'mydatabase',\n storage: storageWithAttachmentsCompression\n});\n\n\n// set the compression mode at the schema level\nconst mySchema = {\n version: 0,\n type: 'object',\n properties: {\n // .\n // .\n // .\n },\n attachments: {\n compression: 'deflate' // <- Specify the compression mode here. OneOf ['deflate', 'gzip']\n }\n};\n\n/* ... create your collections as usual and store attachments in them. */\n\n"})})]})}function d(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(o,{...e})}):o(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>h,x:()=>i});var a=n(6540);const s={},c=a.createContext(s);function h(e){const t=a.useContext(c);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:h(e.components),a.createElement(c.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/b30f4f1f.7980d8e8.js b/docs/assets/js/b30f4f1f.7980d8e8.js deleted file mode 100644 index 6b4bbedba5c..00000000000 --- a/docs/assets/js/b30f4f1f.7980d8e8.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3779],{5830:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>r,contentTitle:()=>h,default:()=>d,frontMatter:()=>c,metadata:()=>i,toc:()=>l});var a=n(4848),s=n(8453);const c={title:"Attachments",slug:"rx-attachment.html"},h="Attachments",i={id:"rx-attachment",title:"Attachments",description:"Attachments are binary data files that can be attachment to an RxDocument, like a file that is attached to an email.",source:"@site/docs/rx-attachment.md",sourceDirName:".",slug:"/rx-attachment.html",permalink:"/rx-attachment.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"Attachments",slug:"rx-attachment.html"},sidebar:"tutorialSidebar",previous:{title:"RxQuery",permalink:"/rx-query.html"},next:{title:"\u2699\ufe0f Rxstorage Layer",permalink:"/rx-storage.html"}},r={},l=[{value:"Add the attachments plugin",id:"add-the-attachments-plugin",level:2},{value:"Enable attachments in the schema",id:"enable-attachments-in-the-schema",level:2},{value:"putAttachment()",id:"putattachment",level:2},{value:"getAttachment()",id:"getattachment",level:2},{value:"allAttachments()",id:"allattachments",level:2},{value:"allAttachments$",id:"allattachments-1",level:2},{value:"RxAttachment",id:"rxattachment",level:2},{value:"doc",id:"doc",level:3},{value:"id",id:"id",level:3},{value:"type",id:"type",level:3},{value:"length",id:"length",level:3},{value:"digest",id:"digest",level:3},{value:"rev",id:"rev",level:3},{value:"remove()",id:"remove",level:3},{value:"getData()",id:"getdata",level:2},{value:"getStringData()",id:"getstringdata",level:2}];function o(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.h1,{id:"attachments",children:"Attachments"}),"\n",(0,a.jsxs)(t.p,{children:["Attachments are binary data files that can be attachment to an ",(0,a.jsx)(t.code,{children:"RxDocument"}),", like a file that is attached to an email."]}),"\n",(0,a.jsxs)(t.p,{children:["Using attachments instead of adding the data to the normal document, ensures that you still have a good ",(0,a.jsx)(t.strong,{children:"performance"})," when querying and writing documents, even when a big amount of data, like an image file has to be stored."]}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"You can store string, binary files, images and whatever you want side by side with your documents."}),"\n",(0,a.jsx)(t.li,{children:"Deleted documents automatically loose all their attachments data."}),"\n",(0,a.jsx)(t.li,{children:"Not all replication plugins support the replication of attachments."}),"\n",(0,a.jsxs)(t.li,{children:["Attachments can be stored ",(0,a.jsx)(t.a,{href:"/encryption.html",children:"encrypted"}),"."]}),"\n"]}),"\n",(0,a.jsxs)(t.p,{children:["Internally, attachments in RxDB are stored and handled similar to how ",(0,a.jsx)(t.a,{href:"https://pouchdb.com/guides/attachments.html#how-attachments-are-stored",children:"CouchDB, PouchDB"})," does it."]}),"\n",(0,a.jsx)(t.h2,{id:"add-the-attachments-plugin",children:"Add the attachments plugin"}),"\n",(0,a.jsxs)(t.p,{children:["To enable the attachments, you have to add the ",(0,a.jsx)(t.code,{children:"attachments"})," plugin."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBAttachmentsPlugin } from 'rxdb/plugins/attachments';\naddRxPlugin(RxDBAttachmentsPlugin);\n"})}),"\n",(0,a.jsx)(t.h2,{id:"enable-attachments-in-the-schema",children:"Enable attachments in the schema"}),"\n",(0,a.jsxs)(t.p,{children:["Before you can use attachments, you have to ensure that the attachments-object is set in the schema of your ",(0,a.jsx)(t.code,{children:"RxCollection"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"\nconst mySchema = {\n version: 0,\n type: 'object',\n properties: {\n // .\n // .\n // .\n },\n attachments: {\n encrypted: true // if true, the attachment-data will be encrypted with the db-password\n }\n};\n\nconst myCollection = await myDatabase.addCollections({\n humans: {\n schema: mySchema\n }\n});\n"})}),"\n",(0,a.jsx)(t.h2,{id:"putattachment",children:"putAttachment()"}),"\n",(0,a.jsxs)(t.p,{children:["Adds an attachment to a ",(0,a.jsx)(t.code,{children:"RxDocument"}),". Returns a Promise with the new attachment."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"import { createBlob } from 'rxdb';\n\nconst attachment = await myDocument.putAttachment(\n {\n id: 'cat.txt', // (string) name of the attachment\n data: createBlob('meowmeow', 'text/plain'), // (string|Blob) data of the attachment\n type: 'text/plain' // (string) type of the attachment-data like 'image/jpeg'\n }\n);\n"})}),"\n",(0,a.jsx)(t.h2,{id:"getattachment",children:"getAttachment()"}),"\n",(0,a.jsxs)(t.p,{children:["Returns an ",(0,a.jsx)(t.code,{children:"RxAttachment"})," by its id. Returns ",(0,a.jsx)(t.code,{children:"null"})," when the attachment does not exist."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachment = myDocument.getAttachment('cat.jpg');\n"})}),"\n",(0,a.jsx)(t.h2,{id:"allattachments",children:"allAttachments()"}),"\n",(0,a.jsxs)(t.p,{children:["Returns an array of all attachments of the ",(0,a.jsx)(t.code,{children:"RxDocument"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachments = myDocument.allAttachments();\n"})}),"\n",(0,a.jsx)(t.h2,{id:"allattachments-1",children:"allAttachments$"}),"\n",(0,a.jsx)(t.p,{children:"Gets an Observable which emits a stream of all attachments from the document. Re-emits each time an attachment gets added or removed from the RxDocument."}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const all = [];\nmyDocument.allAttachments$.subscribe(\n attachments => all = attachments\n);\n"})}),"\n",(0,a.jsx)(t.h2,{id:"rxattachment",children:"RxAttachment"}),"\n",(0,a.jsxs)(t.p,{children:["The attachments of RxDB are represented by the type ",(0,a.jsx)(t.code,{children:"RxAttachment"})," which has the following attributes/methods."]}),"\n",(0,a.jsx)(t.h3,{id:"doc",children:"doc"}),"\n",(0,a.jsxs)(t.p,{children:["The ",(0,a.jsx)(t.code,{children:"RxDocument"})," which the attachment is assigned to."]}),"\n",(0,a.jsx)(t.h3,{id:"id",children:"id"}),"\n",(0,a.jsxs)(t.p,{children:["The id as ",(0,a.jsx)(t.code,{children:"string"})," of the attachment."]}),"\n",(0,a.jsx)(t.h3,{id:"type",children:"type"}),"\n",(0,a.jsxs)(t.p,{children:["The type as ",(0,a.jsx)(t.code,{children:"string"})," of the attachment."]}),"\n",(0,a.jsx)(t.h3,{id:"length",children:"length"}),"\n",(0,a.jsxs)(t.p,{children:["The length of the data of the attachment as ",(0,a.jsx)(t.code,{children:"number"}),"."]}),"\n",(0,a.jsx)(t.h3,{id:"digest",children:"digest"}),"\n",(0,a.jsxs)(t.p,{children:["The hash of the attachments data as ",(0,a.jsx)(t.code,{children:"string"}),"."]}),"\n",(0,a.jsx)(t.p,{children:"NOTICE: The digest is NOT calculated by RxDB, instead it is calculated by the RxStorage. The only guarantee is that the digest will change when the attachments data changes."}),"\n",(0,a.jsx)(t.h3,{id:"rev",children:"rev"}),"\n",(0,a.jsxs)(t.p,{children:["The revision-number of the attachment as ",(0,a.jsx)(t.code,{children:"number"}),"."]}),"\n",(0,a.jsx)(t.h3,{id:"remove",children:"remove()"}),"\n",(0,a.jsx)(t.p,{children:"Removes the attachment. Returns a Promise that resolves when done."}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachment = myDocument.getAttachment('cat.jpg');\nawait attachment.remove();\n"})}),"\n",(0,a.jsx)(t.h2,{id:"getdata",children:"getData()"}),"\n",(0,a.jsxs)(t.p,{children:["Returns a Promise which resolves the attachment's data as ",(0,a.jsx)(t.code,{children:"Blob"}),". (async)"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachment = myDocument.getAttachment('cat.jpg');\nconst blob = await attachment.getData();\n"})}),"\n",(0,a.jsx)(t.h2,{id:"getstringdata",children:"getStringData()"}),"\n",(0,a.jsxs)(t.p,{children:["Returns a Promise which resolves the attachment's data as ",(0,a.jsx)(t.code,{children:"string"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-javascript",children:"const attachment = await myDocument.getAttachment('cat.jpg');\nconst data = await attachment.getStringData();\n"})}),"\n",(0,a.jsx)(t.h1,{id:"attachment-compression",children:"Attachment compression"}),"\n",(0,a.jsxs)(t.p,{children:["Storing many attachments can be a problem when the disc space of the device is exceeded.\nTherefore it can make sense to compress the attachments before storing them in the ",(0,a.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"}),".\nWith the ",(0,a.jsx)(t.code,{children:"attachments-compression"})," plugin you can compress the attachments data on write and decompress it on reads.\nThis happens internally and will now change on how you use the api. The compression is run with the ",(0,a.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Compression_Streams_API",children:"Compression Streams API"})," which is only supported on ",(0,a.jsx)(t.a,{href:"https://caniuse.com/?search=compressionstream",children:"newer browsers"}),"."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"import {\n wrappedAttachmentsCompressionStorage\n} from 'rxdb/plugins/attachments-compression';\n\nimport { getRxStorageIndexedDB } from 'rxdb-premium/plugins/storage-indexeddb';\n\n// create a wrapped storage with attachment-compression.\nconst storageWithAttachmentsCompression = wrappedAttachmentsCompressionStorage({\n storage: getRxStorageIndexedDB()\n});\n\nconst db = await createRxDatabase({\n name: 'mydatabase',\n storage: storageWithAttachmentsCompression\n});\n\n\n// set the compression mode at the schema level\nconst mySchema = {\n version: 0,\n type: 'object',\n properties: {\n // .\n // .\n // .\n },\n attachments: {\n compression: 'deflate' // <- Specify the compression mode here. OneOf ['deflate', 'gzip']\n }\n};\n\n/* ... create your collections as usual and store attachments in them. */\n\n"})})]})}function d(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(o,{...e})}):o(e)}},8453:(e,t,n)=>{n.d(t,{R:()=>h,x:()=>i});var a=n(6540);const s={},c=a.createContext(s);function h(e){const t=a.useContext(c);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:h(e.components),a.createElement(c.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/badcd764.2deaee16.js b/docs/assets/js/badcd764.2deaee16.js new file mode 100644 index 00000000000..b9224c7f04d --- /dev/null +++ b/docs/assets/js/badcd764.2deaee16.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[8318],{6042:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>r,metadata:()=>o,toc:()=>c});var i=a(4848),n=a(8453);const r={title:"RxDB as a Database in a Flutter Application",slug:"flutter-database.html"},s="RxDB as a Database in a Flutter Application",o={id:"articles/flutter-database",title:"RxDB as a Database in a Flutter Application",description:"In the world of mobile application development, Flutter has gained significant popularity due to its cross-platform capabilities and rich UI framework. When it comes to building feature-rich Flutter applications, the choice of a robust and efficient database is crucial. In this article, we will explore RxDB as a database solution for Flutter applications. We'll delve into the core features of RxDB, its benefits over other database options, and how to integrate it into a Flutter app.",source:"@site/docs/articles/flutter-database.md",sourceDirName:"articles",slug:"/articles/flutter-database.html",permalink:"/articles/flutter-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDB as a Database in a Flutter Application",slug:"flutter-database.html"},sidebar:"tutorialSidebar",previous:{title:"Using RxDB as an Embedded Database",permalink:"/articles/embedded-database.html"},next:{title:"RxDB JavaScript Frontend Database - Efficient Data Storage in Frontend Applications",permalink:"/articles/frontend-database.html"}},l={},c=[{value:"Overview of Flutter Mobile Applications",id:"overview-of-flutter-mobile-applications",level:3},{value:"Importance of Databases in Flutter Applications",id:"importance-of-databases-in-flutter-applications",level:3},{value:"Introducing RxDB as a Database Solution",id:"introducing-rxdb-as-a-database-solution",level:3},{value:"Getting Started with RxDB",id:"getting-started-with-rxdb",level:2},{value:"What is RxDB?",id:"what-is-rxdb",level:3},{value:"Reactive Data Handling",id:"reactive-data-handling",level:3},{value:"Offline-First Approach",id:"offline-first-approach",level:3},{value:"Data Replication",id:"data-replication",level:3},{value:"Observable Queries",id:"observable-queries",level:3},{value:"RxDB vs. Other Flutter Database Options",id:"rxdb-vs-other-flutter-database-options",level:3},{value:"Using RxDB in a Flutter Application",id:"using-rxdb-in-a-flutter-application",level:2},{value:"How RxDB can run in Flutter",id:"how-rxdb-can-run-in-flutter",level:2},{value:"Different RxStorage layers for RxDB",id:"different-rxstorage-layers-for-rxdb",level:3},{value:"Synchronizing Data with RxDB between Clients and Servers",id:"synchronizing-data-with-rxdb-between-clients-and-servers",level:2},{value:"Offline-First Approach",id:"offline-first-approach-1",level:3},{value:"RxDB Replication Plugins",id:"rxdb-replication-plugins",level:3},{value:"Advanced RxDB Features and Techniques",id:"advanced-rxdb-features-and-techniques",level:2},{value:"Indexing and Performance Optimization",id:"indexing-and-performance-optimization",level:3},{value:"Encryption of Local Data",id:"encryption-of-local-data",level:3},{value:"Change Streams and Event Handling",id:"change-streams-and-event-handling",level:3},{value:"JSON Key Compression",id:"json-key-compression",level:3},{value:"Conclusion",id:"conclusion",level:2}];function d(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,n.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"rxdb-as-a-database-in-a-flutter-application",children:"RxDB as a Database in a Flutter Application"}),"\n",(0,i.jsxs)(t.p,{children:["In the world of mobile application development, Flutter has gained significant popularity due to its cross-platform capabilities and rich UI framework. When it comes to building feature-rich Flutter applications, the choice of a robust and efficient database is crucial. In this article, we will explore ",(0,i.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," as a database solution for Flutter applications. We'll delve into the core features of RxDB, its benefits over other database options, and how to integrate it into a Flutter app."]}),"\n",(0,i.jsx)(t.admonition,{type:"note",children:(0,i.jsxs)(t.p,{children:["You can find the source code for an example RxDB Flutter Application ",(0,i.jsx)(t.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/flutter",children:"at the github repo"})]})}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("a",{href:"https://rxdb.info/",children:(0,i.jsx)("img",{src:"../files/logo/rxdb_javascript_database.svg",alt:"RxDB Flutter Database",width:"220"})})}),"\n",(0,i.jsx)(t.h3,{id:"overview-of-flutter-mobile-applications",children:"Overview of Flutter Mobile Applications"}),"\n",(0,i.jsxs)(t.p,{children:["Flutter is an open-source UI software development kit created by Google that allows developers to build high-performance ",(0,i.jsx)(t.a,{href:"/articles/mobile-database.html",children:"mobile"})," applications for iOS and Android platforms using a single codebase. Flutter's framework provides a wide range of widgets and tools that enable developers to create visually appealing and responsive applications."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("img",{src:"../files/icons/flutter.svg",alt:"Flutter",width:"60"})}),"\n",(0,i.jsx)(t.h3,{id:"importance-of-databases-in-flutter-applications",children:"Importance of Databases in Flutter Applications"}),"\n",(0,i.jsx)(t.p,{children:"Databases play a vital role in Flutter applications by providing a persistent and reliable storage solution for storing and retrieving data. Whether it's user profiles, app settings, or complex data structures, a database helps in efficiently managing and organizing the application's data. Choosing the right database for a Flutter application can significantly impact the performance, scalability, and user experience of the app."}),"\n",(0,i.jsx)(t.h3,{id:"introducing-rxdb-as-a-database-solution",children:"Introducing RxDB as a Database Solution"}),"\n",(0,i.jsx)(t.p,{children:"RxDB is a powerful NoSQL database solution that is designed to work seamlessly with JavaScript-based frameworks, such as Flutter. It stands for Reactive Database and offers a variety of features that make it an excellent choice for building Flutter applications. RxDB combines the simplicity of JavaScript's document-based database model with the reactive programming paradigm, enabling developers to build real-time and offline-first applications with ease."}),"\n",(0,i.jsx)(t.h2,{id:"getting-started-with-rxdb",children:"Getting Started with RxDB"}),"\n",(0,i.jsx)(t.p,{children:"To understand how RxDB can be utilized in a Flutter application, let's explore its core features and advantages."}),"\n",(0,i.jsx)(t.h3,{id:"what-is-rxdb",children:"What is RxDB?"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," is a client-side database built on top of IndexedDB, which is a low-level ",(0,i.jsx)(t.a,{href:"/articles/browser-database.html",children:"browser-based database"})," API. It provides a simple and intuitive API for performing CRUD operations (Create, Read, Update, Delete) on documents. RxDB's underlying architecture allows for efficient handling of data synchronization between multiple clients and servers."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("a",{href:"https://rxdb.info/",children:(0,i.jsx)("img",{src:"../files/logo/rxdb_javascript_database.svg",alt:"RxDB Flutter Database",width:"220"})})}),"\n",(0,i.jsx)(t.h3,{id:"reactive-data-handling",children:"Reactive Data Handling"}),"\n",(0,i.jsx)(t.p,{children:"One of the key strengths of RxDB is its reactive data handling. It leverages the power of Observables, a concept from reactive programming, to automatically update the UI in response to data changes. With RxDB, developers can define queries and subscribe to their results, ensuring that the UI is always in sync with the database."}),"\n",(0,i.jsx)(t.h3,{id:"offline-first-approach",children:"Offline-First Approach"}),"\n",(0,i.jsx)(t.p,{children:"RxDB follows an offline-first approach, making it ideal for building Flutter applications that need to function even without an internet connection. It allows data to be stored locally and seamlessly synchronizes it with the server when a connection is available. This ensures that users can access and interact with their data regardless of network availability."}),"\n",(0,i.jsx)(t.h3,{id:"data-replication",children:"Data Replication"}),"\n",(0,i.jsx)(t.p,{children:"Data replication is a critical aspect of building distributed applications. RxDB provides robust replication capabilities that enable synchronization of data between different clients and servers. With its replication plugins, RxDB simplifies the process of setting up real-time data synchronization, ensuring consistency across all connected devices."}),"\n",(0,i.jsx)(t.h3,{id:"observable-queries",children:"Observable Queries"}),"\n",(0,i.jsx)(t.p,{children:"RxDB introduces the concept of observable queries, which are queries that automatically update when the underlying data changes. This feature is particularly useful for keeping the UI up to date with the latest data. By subscribing to an observable query, developers can receive real-time updates and reflect them in the user interface without manual intervention."}),"\n",(0,i.jsx)(t.h3,{id:"rxdb-vs-other-flutter-database-options",children:"RxDB vs. Other Flutter Database Options"}),"\n",(0,i.jsx)(t.p,{children:"When considering database options for Flutter applications, developers often come across alternatives such as SQLite or LokiJS. While these databases have their merits, RxDB offers several advantages over them. RxDB's seamless integration with Flutter, its offline-first approach, reactive data handling, and built-in data replication make it a compelling choice for building feature-rich and scalable Flutter applications."}),"\n",(0,i.jsx)(t.h2,{id:"using-rxdb-in-a-flutter-application",children:"Using RxDB in a Flutter Application"}),"\n",(0,i.jsx)(t.p,{children:"Now that we understand the core features of RxDB, let's explore how to integrate it into a Flutter application."}),"\n",(0,i.jsx)(t.h2,{id:"how-rxdb-can-run-in-flutter",children:"How RxDB can run in Flutter"}),"\n",(0,i.jsxs)(t.p,{children:["RxDB is written in TypeScript and compiled to JavaScript. To run it in a Flutter application, the ",(0,i.jsx)(t.code,{children:"flutter_qjs"})," library is used to spawn a QuickJS JavaScript runtime. RxDB itself runs in that runtime and communicates with the flutter dart runtime. To store data persistent, the ",(0,i.jsx)(t.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"})," is used together with a custom storage adapter that persists the database inside of the ",(0,i.jsx)(t.code,{children:"shared_preferences"})," data."]}),"\n",(0,i.jsx)(t.p,{children:"To use RxDB, you have to create a compatible JavaScript file that creates your RxDatabase and starts some connectors which are used by Flutter to communicate with the JavaScript RxDB database via setFlutterRxDatabaseConnector()."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-javascript",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport {\n getRxStorageLoki\n} from 'rxdb/plugins/storage-lokijs';\nimport {\n setFlutterRxDatabaseConnector,\n getLokijsAdapterFlutter\n} from 'rxdb/plugins/flutter';\n\n// do all database creation stuff in this method.\nasync function createDB(databaseName) {\n // create the RxDatabase\n const db = await createRxDatabase({\n // the database.name is variable so we can change it on the flutter side\n name: databaseName,\n storage: getRxStorageLoki({\n adapter: getLokijsAdapterFlutter()\n }),\n multiInstance: false\n });\n await db.addCollections({\n heroes: {\n schema: {\n version: 0,\n primaryKey: 'id',\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100\n },\n name: {\n type: 'string',\n maxLength: 100\n },\n color: {\n type: 'string',\n maxLength: 30\n }\n },\n indexes: ['name'],\n required: ['id', 'name', 'color']\n }\n }\n });\n return db;\n}\n\n// start the connector so that flutter can communicate with the JavaScript process\nsetFlutterRxDatabaseConnector(\n createDB\n);\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Before you can use the JavaScript code, you have to bundle it into a single .js file. In this example we do that with webpack in a npm script here which bundles everything into the ",(0,i.jsx)(t.code,{children:"javascript/dist/index.js"})," file."]}),"\n",(0,i.jsx)(t.p,{children:"To allow Flutter to access that file during runtime, add it to the assets inside of your pubspec.yaml:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-yaml",children:"flutter:\n assets:\n - javascript/dist/index.js\n"})}),"\n",(0,i.jsx)(t.p,{children:"Also you need to install RxDB in your flutter part of the application. First you have to use the rxdb dart package as a flutter dependency. Currently the package is not published at the dart pub.dev. Instead you have to install it from the local filesystem inside of your RxDB npm installation."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-yaml",children:"# inside of pubspec.yaml\ndependencies:\n rxdb:\n path: path/to/your/node_modules/rxdb/src/plugins/flutter/dart\n"})}),"\n",(0,i.jsx)(t.p,{children:"Afterwards you can import the rxdb library in your dart code and connect to the JavaScript process from there. For reference, check out the lib/main.dart file."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:'import \'package:rxdb/rxdb.dart\';\n\n// start the javascript process and connect to the database\nRxDatabase database = await getRxDatabase("javascript/dist/index.js", databaseName);\n\n// get a collection\nRxCollection collection = database.getCollection(\'heroes\');\n\n// insert a document\nRxDocument document = await collection.insert({\n "id": "zflutter-${DateTime.now()}",\n "name": nameController.text,\n "color": colorController.text\n});\n\n// create a query\nRxQuery query = RxDatabaseState.collection.find();\n\n// create list to store query results\nList> documents = [];\n\n// subscribe to a query\nquery.$().listen((results) {\n setState(() {\n documents = results;\n });\n});\n'})}),"\n",(0,i.jsx)(t.h3,{id:"different-rxstorage-layers-for-rxdb",children:"Different RxStorage layers for RxDB"}),"\n",(0,i.jsx)(t.p,{children:"RxDB offers multiple storage options, known as RxStorage layers, to store data locally. These options include:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"}),": LokiJS is an in-memory database that can be used as a ",(0,i.jsx)(t.a,{href:"/articles/browser-storage.html",children:"storage"})," layer for RxDB. It provides fast and efficient in-memory data management capabilities."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"}),": SQLite is a popular and widely used ",(0,i.jsx)(t.a,{href:"/articles/embedded-database.html",children:"embedded database"})," that offers robust storage capabilities. RxDB utilizes SQLite as a storage layer to persist data on the device."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/rx-storage-memory.html",children:"Memory RxStorage"}),": As the name suggests, Memory RxStorage stores data ",(0,i.jsx)(t.a,{href:"/articles/in-memory-nosql-database.html",children:"in memory"}),". While this option does not provide persistence, it can be useful for temporary or cache-based data storage.\nBy choosing the appropriate RxStorage layer based on the specific requirements of the application, developers can optimize performance and storage efficiency."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"synchronizing-data-with-rxdb-between-clients-and-servers",children:"Synchronizing Data with RxDB between Clients and Servers"}),"\n",(0,i.jsx)(t.p,{children:"One of the key strengths of RxDB is its ability to synchronize data between multiple clients and servers seamlessly. Let's explore how this synchronization can be achieved."}),"\n",(0,i.jsx)(t.h3,{id:"offline-first-approach-1",children:"Offline-First Approach"}),"\n",(0,i.jsx)(t.p,{children:"RxDB's offline-first approach ensures that data can be accessed and modified even when there is no internet connection. Changes made offline are automatically synchronized with the server once a connection is reestablished. This ensures data consistency across all devices, providing a seamless user experience."}),"\n",(0,i.jsx)(t.h3,{id:"rxdb-replication-plugins",children:"RxDB Replication Plugins"}),"\n",(0,i.jsxs)(t.p,{children:["RxDB provides replication plugins that simplify the process of setting up data ",(0,i.jsx)(t.a,{href:"/replication.html",children:"synchronization between clients and servers"}),". These plugins offer various synchronization strategies, such as one-way replication, two-way replication, and conflict resolution mechanisms. By configuring the appropriate replication plugin, developers can easily establish real-time data synchronization in their Flutter applications."]}),"\n",(0,i.jsx)(t.h2,{id:"advanced-rxdb-features-and-techniques",children:"Advanced RxDB Features and Techniques"}),"\n",(0,i.jsx)(t.p,{children:"RxDB offers a range of advanced features and techniques that enhance its functionality and performance. Let's explore a few of these features:"}),"\n",(0,i.jsx)(t.h3,{id:"indexing-and-performance-optimization",children:"Indexing and Performance Optimization"}),"\n",(0,i.jsx)(t.p,{children:"Indexing is a technique used to optimize query performance by creating indexes on specific fields. RxDB allows developers to define indexes on document fields, improving the efficiency of queries and data retrieval."}),"\n",(0,i.jsx)(t.h3,{id:"encryption-of-local-data",children:"Encryption of Local Data"}),"\n",(0,i.jsxs)(t.p,{children:["To ensure data privacy and security, RxDB supports ",(0,i.jsx)(t.a,{href:"/encryption.html",children:"encryption of local data"}),". By encrypting the data stored on the device, developers can protect sensitive information and prevent unauthorized access."]}),"\n",(0,i.jsx)(t.h3,{id:"change-streams-and-event-handling",children:"Change Streams and Event Handling"}),"\n",(0,i.jsx)(t.p,{children:"RxDB provides change streams, which emit events whenever data changes occur. By leveraging change streams, developers can implement custom event handling logic, such as updating the UI or triggering background processes, in response to specific data changes."}),"\n",(0,i.jsx)(t.h3,{id:"json-key-compression",children:"JSON Key Compression"}),"\n",(0,i.jsxs)(t.p,{children:["To minimize storage requirements and optimize performance, RxDB offers ",(0,i.jsx)(t.a,{href:"/key-compression.html",children:"JSON key compression"}),". This feature reduces the size of keys used in the database, resulting in more efficient storage and improved query performance."]}),"\n",(0,i.jsx)(t.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,i.jsx)(t.p,{children:"RxDB offers a powerful and flexible database solution for Flutter applications. With its offline-first approach, real-time data synchronization, and reactive data handling capabilities, RxDB simplifies the development of feature-rich and scalable Flutter applications. By integrating RxDB into your Flutter projects, you can leverage its advanced features and techniques to build responsive and data-driven applications that provide an exceptional user experience."}),"\n",(0,i.jsx)(t.admonition,{type:"note",children:(0,i.jsxs)(t.p,{children:["You can find the source code for an example RxDB Flutter Application ",(0,i.jsx)(t.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/flutter",children:"at the github repo"})]})})]})}function h(e={}){const{wrapper:t}={...(0,n.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8453:(e,t,a)=>{a.d(t,{R:()=>s,x:()=>o});var i=a(6540);const n={},r=i.createContext(n);function s(e){const t=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:s(e.components),i.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/badcd764.3e849703.js b/docs/assets/js/badcd764.3e849703.js deleted file mode 100644 index 06451992d42..00000000000 --- a/docs/assets/js/badcd764.3e849703.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[8318],{6042:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>r,metadata:()=>o,toc:()=>c});var i=a(4848),n=a(8453);const r={title:"RxDB as a Database in a Flutter Application",slug:"flutter-database.html"},s="RxDB as a Database in a Flutter Application",o={id:"articles/flutter-database",title:"RxDB as a Database in a Flutter Application",description:"In the world of mobile application development, Flutter has gained significant popularity due to its cross-platform capabilities and rich UI framework. When it comes to building feature-rich Flutter applications, the choice of a robust and efficient database is crucial. In this article, we will explore RxDB as a database solution for Flutter applications. We'll delve into the core features of RxDB, its benefits over other database options, and how to integrate it into a Flutter app.",source:"@site/docs/articles/flutter-database.md",sourceDirName:"articles",slug:"/articles/flutter-database.html",permalink:"/articles/flutter-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDB as a Database in a Flutter Application",slug:"flutter-database.html"},sidebar:"tutorialSidebar",previous:{title:"Using RxDB as an Embedded Database",permalink:"/articles/embedded-database.html"},next:{title:"RxDB JavaScript Frontend Database - Efficient Data Storage in Frontend Applications",permalink:"/articles/frontend-database.html"}},l={},c=[{value:"NOTICE: You can find the source code for an example RxDB Flutter Application at the github repo",id:"notice-you-can-find-the-source-code-for-an-example-rxdb-flutter-application-at-the-github-repo",level:2},{value:"Overview of Flutter Mobile Applications",id:"overview-of-flutter-mobile-applications",level:3},{value:"Importance of Databases in Flutter Applications",id:"importance-of-databases-in-flutter-applications",level:3},{value:"Introducing RxDB as a Database Solution",id:"introducing-rxdb-as-a-database-solution",level:3},{value:"Getting Started with RxDB",id:"getting-started-with-rxdb",level:2},{value:"What is RxDB?",id:"what-is-rxdb",level:3},{value:"Reactive Data Handling",id:"reactive-data-handling",level:3},{value:"Offline-First Approach",id:"offline-first-approach",level:3},{value:"Data Replication",id:"data-replication",level:3},{value:"Observable Queries",id:"observable-queries",level:3},{value:"RxDB vs. Other Flutter Database Options",id:"rxdb-vs-other-flutter-database-options",level:3},{value:"Using RxDB in a Flutter Application",id:"using-rxdb-in-a-flutter-application",level:2},{value:"How RxDB can run in Flutter",id:"how-rxdb-can-run-in-flutter",level:2},{value:"Different RxStorage layers for RxDB",id:"different-rxstorage-layers-for-rxdb",level:3},{value:"Synchronizing Data with RxDB between Clients and Servers",id:"synchronizing-data-with-rxdb-between-clients-and-servers",level:2},{value:"Offline-First Approach",id:"offline-first-approach-1",level:3},{value:"RxDB Replication Plugins",id:"rxdb-replication-plugins",level:3},{value:"Advanced RxDB Features and Techniques",id:"advanced-rxdb-features-and-techniques",level:2},{value:"Indexing and Performance Optimization",id:"indexing-and-performance-optimization",level:3},{value:"Encryption of Local Data",id:"encryption-of-local-data",level:3},{value:"Change Streams and Event Handling",id:"change-streams-and-event-handling",level:3},{value:"JSON Key Compression",id:"json-key-compression",level:3},{value:"Conclusion",id:"conclusion",level:2},{value:"NOTICE: You can find the source code for an example RxDB Flutter Application at the github repo",id:"notice-you-can-find-the-source-code-for-an-example-rxdb-flutter-application-at-the-github-repo-1",level:2}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,n.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"rxdb-as-a-database-in-a-flutter-application",children:"RxDB as a Database in a Flutter Application"}),"\n",(0,i.jsxs)(t.p,{children:["In the world of mobile application development, Flutter has gained significant popularity due to its cross-platform capabilities and rich UI framework. When it comes to building feature-rich Flutter applications, the choice of a robust and efficient database is crucial. In this article, we will explore ",(0,i.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," as a database solution for Flutter applications. We'll delve into the core features of RxDB, its benefits over other database options, and how to integrate it into a Flutter app."]}),"\n",(0,i.jsxs)(t.h2,{id:"notice-you-can-find-the-source-code-for-an-example-rxdb-flutter-application-at-the-github-repo",children:["NOTICE: You can find the source code for an example RxDB Flutter Application ",(0,i.jsx)(t.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/flutter",children:"at the github repo"})]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("a",{href:"https://rxdb.info/",children:(0,i.jsx)("img",{src:"../files/logo/rxdb_javascript_database.svg",alt:"RxDB Flutter Database",width:"220"})})}),"\n",(0,i.jsx)(t.h3,{id:"overview-of-flutter-mobile-applications",children:"Overview of Flutter Mobile Applications"}),"\n",(0,i.jsxs)(t.p,{children:["Flutter is an open-source UI software development kit created by Google that allows developers to build high-performance ",(0,i.jsx)(t.a,{href:"/articles/mobile-database.html",children:"mobile"})," applications for iOS and Android platforms using a single codebase. Flutter's framework provides a wide range of widgets and tools that enable developers to create visually appealing and responsive applications."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("img",{src:"../files/icons/flutter.svg",alt:"Flutter",width:"60"})}),"\n",(0,i.jsx)(t.h3,{id:"importance-of-databases-in-flutter-applications",children:"Importance of Databases in Flutter Applications"}),"\n",(0,i.jsx)(t.p,{children:"Databases play a vital role in Flutter applications by providing a persistent and reliable storage solution for storing and retrieving data. Whether it's user profiles, app settings, or complex data structures, a database helps in efficiently managing and organizing the application's data. Choosing the right database for a Flutter application can significantly impact the performance, scalability, and user experience of the app."}),"\n",(0,i.jsx)(t.h3,{id:"introducing-rxdb-as-a-database-solution",children:"Introducing RxDB as a Database Solution"}),"\n",(0,i.jsx)(t.p,{children:"RxDB is a powerful NoSQL database solution that is designed to work seamlessly with JavaScript-based frameworks, such as Flutter. It stands for Reactive Database and offers a variety of features that make it an excellent choice for building Flutter applications. RxDB combines the simplicity of JavaScript's document-based database model with the reactive programming paradigm, enabling developers to build real-time and offline-first applications with ease."}),"\n",(0,i.jsx)(t.h2,{id:"getting-started-with-rxdb",children:"Getting Started with RxDB"}),"\n",(0,i.jsx)(t.p,{children:"To understand how RxDB can be utilized in a Flutter application, let's explore its core features and advantages."}),"\n",(0,i.jsx)(t.h3,{id:"what-is-rxdb",children:"What is RxDB?"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," is a client-side database built on top of IndexedDB, which is a low-level ",(0,i.jsx)(t.a,{href:"/articles/browser-database.html",children:"browser-based database"})," API. It provides a simple and intuitive API for performing CRUD operations (Create, Read, Update, Delete) on documents. RxDB's underlying architecture allows for efficient handling of data synchronization between multiple clients and servers."]}),"\n",(0,i.jsx)("center",{children:(0,i.jsx)("a",{href:"https://rxdb.info/",children:(0,i.jsx)("img",{src:"../files/logo/rxdb_javascript_database.svg",alt:"RxDB Flutter Database",width:"220"})})}),"\n",(0,i.jsx)(t.h3,{id:"reactive-data-handling",children:"Reactive Data Handling"}),"\n",(0,i.jsx)(t.p,{children:"One of the key strengths of RxDB is its reactive data handling. It leverages the power of Observables, a concept from reactive programming, to automatically update the UI in response to data changes. With RxDB, developers can define queries and subscribe to their results, ensuring that the UI is always in sync with the database."}),"\n",(0,i.jsx)(t.h3,{id:"offline-first-approach",children:"Offline-First Approach"}),"\n",(0,i.jsx)(t.p,{children:"RxDB follows an offline-first approach, making it ideal for building Flutter applications that need to function even without an internet connection. It allows data to be stored locally and seamlessly synchronizes it with the server when a connection is available. This ensures that users can access and interact with their data regardless of network availability."}),"\n",(0,i.jsx)(t.h3,{id:"data-replication",children:"Data Replication"}),"\n",(0,i.jsx)(t.p,{children:"Data replication is a critical aspect of building distributed applications. RxDB provides robust replication capabilities that enable synchronization of data between different clients and servers. With its replication plugins, RxDB simplifies the process of setting up real-time data synchronization, ensuring consistency across all connected devices."}),"\n",(0,i.jsx)(t.h3,{id:"observable-queries",children:"Observable Queries"}),"\n",(0,i.jsx)(t.p,{children:"RxDB introduces the concept of observable queries, which are queries that automatically update when the underlying data changes. This feature is particularly useful for keeping the UI up to date with the latest data. By subscribing to an observable query, developers can receive real-time updates and reflect them in the user interface without manual intervention."}),"\n",(0,i.jsx)(t.h3,{id:"rxdb-vs-other-flutter-database-options",children:"RxDB vs. Other Flutter Database Options"}),"\n",(0,i.jsx)(t.p,{children:"When considering database options for Flutter applications, developers often come across alternatives such as SQLite or LokiJS. While these databases have their merits, RxDB offers several advantages over them. RxDB's seamless integration with Flutter, its offline-first approach, reactive data handling, and built-in data replication make it a compelling choice for building feature-rich and scalable Flutter applications."}),"\n",(0,i.jsx)(t.h2,{id:"using-rxdb-in-a-flutter-application",children:"Using RxDB in a Flutter Application"}),"\n",(0,i.jsx)(t.p,{children:"Now that we understand the core features of RxDB, let's explore how to integrate it into a Flutter application."}),"\n",(0,i.jsx)(t.h2,{id:"how-rxdb-can-run-in-flutter",children:"How RxDB can run in Flutter"}),"\n",(0,i.jsxs)(t.p,{children:["RxDB is written in TypeScript and compiled to JavaScript. To run it in a Flutter application, the ",(0,i.jsx)(t.code,{children:"flutter_qjs"})," library is used to spawn a QuickJS JavaScript runtime. RxDB itself runs in that runtime and communicates with the flutter dart runtime. To store data persistent, the ",(0,i.jsx)(t.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"})," is used together with a custom storage adapter that persists the database inside of the ",(0,i.jsx)(t.code,{children:"shared_preferences"})," data."]}),"\n",(0,i.jsx)(t.p,{children:"To use RxDB, you have to create a compatible JavaScript file that creates your RxDatabase and starts some connectors which are used by Flutter to communicate with the JavaScript RxDB database via setFlutterRxDatabaseConnector()."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-javascript",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport {\n getRxStorageLoki\n} from 'rxdb/plugins/storage-lokijs';\nimport {\n setFlutterRxDatabaseConnector,\n getLokijsAdapterFlutter\n} from 'rxdb/plugins/flutter';\n\n// do all database creation stuff in this method.\nasync function createDB(databaseName) {\n // create the RxDatabase\n const db = await createRxDatabase({\n // the database.name is variable so we can change it on the flutter side\n name: databaseName,\n storage: getRxStorageLoki({\n adapter: getLokijsAdapterFlutter()\n }),\n multiInstance: false\n });\n await db.addCollections({\n heroes: {\n schema: {\n version: 0,\n primaryKey: 'id',\n type: 'object',\n properties: {\n id: {\n type: 'string',\n maxLength: 100\n },\n name: {\n type: 'string',\n maxLength: 100\n },\n color: {\n type: 'string',\n maxLength: 30\n }\n },\n indexes: ['name'],\n required: ['id', 'name', 'color']\n }\n }\n });\n return db;\n}\n\n// start the connector so that flutter can communicate with the JavaScript process\nsetFlutterRxDatabaseConnector(\n createDB\n);\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Before you can use the JavaScript code, you have to bundle it into a single .js file. In this example we do that with webpack in a npm script here which bundles everything into the ",(0,i.jsx)(t.code,{children:"javascript/dist/index.js"})," file."]}),"\n",(0,i.jsx)(t.p,{children:"To allow Flutter to access that file during runtime, add it to the assets inside of your pubspec.yaml:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-yaml",children:"flutter:\n assets:\n - javascript/dist/index.js\n"})}),"\n",(0,i.jsx)(t.p,{children:"Also you need to install RxDB in your flutter part of the application. First you have to use the rxdb dart package as a flutter dependency. Currently the package is not published at the dart pub.dev. Instead you have to install it from the local filesystem inside of your RxDB npm installation."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-yaml",children:"# inside of pubspec.yaml\ndependencies:\n rxdb:\n path: path/to/your/node_modules/rxdb/src/plugins/flutter/dart\n"})}),"\n",(0,i.jsx)(t.p,{children:"Afterwards you can import the rxdb library in your dart code and connect to the JavaScript process from there. For reference, check out the lib/main.dart file."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:'import \'package:rxdb/rxdb.dart\';\n\n// start the javascript process and connect to the database\nRxDatabase database = await getRxDatabase("javascript/dist/index.js", databaseName);\n\n// get a collection\nRxCollection collection = database.getCollection(\'heroes\');\n\n// insert a document\nRxDocument document = await collection.insert({\n "id": "zflutter-${DateTime.now()}",\n "name": nameController.text,\n "color": colorController.text\n});\n\n// create a query\nRxQuery query = RxDatabaseState.collection.find();\n\n// create list to store query results\nList> documents = [];\n\n// subscribe to a query\nquery.$().listen((results) {\n setState(() {\n documents = results;\n });\n});\n'})}),"\n",(0,i.jsx)(t.h3,{id:"different-rxstorage-layers-for-rxdb",children:"Different RxStorage layers for RxDB"}),"\n",(0,i.jsx)(t.p,{children:"RxDB offers multiple storage options, known as RxStorage layers, to store data locally. These options include:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"}),": LokiJS is an in-memory database that can be used as a ",(0,i.jsx)(t.a,{href:"/articles/browser-storage.html",children:"storage"})," layer for RxDB. It provides fast and efficient in-memory data management capabilities."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"}),": SQLite is a popular and widely used ",(0,i.jsx)(t.a,{href:"/articles/embedded-database.html",children:"embedded database"})," that offers robust storage capabilities. RxDB utilizes SQLite as a storage layer to persist data on the device."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/rx-storage-memory.html",children:"Memory RxStorage"}),": As the name suggests, Memory RxStorage stores data ",(0,i.jsx)(t.a,{href:"/articles/in-memory-nosql-database.html",children:"in memory"}),". While this option does not provide persistence, it can be useful for temporary or cache-based data storage.\nBy choosing the appropriate RxStorage layer based on the specific requirements of the application, developers can optimize performance and storage efficiency."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"synchronizing-data-with-rxdb-between-clients-and-servers",children:"Synchronizing Data with RxDB between Clients and Servers"}),"\n",(0,i.jsx)(t.p,{children:"One of the key strengths of RxDB is its ability to synchronize data between multiple clients and servers seamlessly. Let's explore how this synchronization can be achieved."}),"\n",(0,i.jsx)(t.h3,{id:"offline-first-approach-1",children:"Offline-First Approach"}),"\n",(0,i.jsx)(t.p,{children:"RxDB's offline-first approach ensures that data can be accessed and modified even when there is no internet connection. Changes made offline are automatically synchronized with the server once a connection is reestablished. This ensures data consistency across all devices, providing a seamless user experience."}),"\n",(0,i.jsx)(t.h3,{id:"rxdb-replication-plugins",children:"RxDB Replication Plugins"}),"\n",(0,i.jsxs)(t.p,{children:["RxDB provides replication plugins that simplify the process of setting up data ",(0,i.jsx)(t.a,{href:"/replication.html",children:"synchronization between clients and servers"}),". These plugins offer various synchronization strategies, such as one-way replication, two-way replication, and conflict resolution mechanisms. By configuring the appropriate replication plugin, developers can easily establish real-time data synchronization in their Flutter applications."]}),"\n",(0,i.jsx)(t.h2,{id:"advanced-rxdb-features-and-techniques",children:"Advanced RxDB Features and Techniques"}),"\n",(0,i.jsx)(t.p,{children:"RxDB offers a range of advanced features and techniques that enhance its functionality and performance. Let's explore a few of these features:"}),"\n",(0,i.jsx)(t.h3,{id:"indexing-and-performance-optimization",children:"Indexing and Performance Optimization"}),"\n",(0,i.jsx)(t.p,{children:"Indexing is a technique used to optimize query performance by creating indexes on specific fields. RxDB allows developers to define indexes on document fields, improving the efficiency of queries and data retrieval."}),"\n",(0,i.jsx)(t.h3,{id:"encryption-of-local-data",children:"Encryption of Local Data"}),"\n",(0,i.jsxs)(t.p,{children:["To ensure data privacy and security, RxDB supports ",(0,i.jsx)(t.a,{href:"/encryption.html",children:"encryption of local data"}),". By encrypting the data stored on the device, developers can protect sensitive information and prevent unauthorized access."]}),"\n",(0,i.jsx)(t.h3,{id:"change-streams-and-event-handling",children:"Change Streams and Event Handling"}),"\n",(0,i.jsx)(t.p,{children:"RxDB provides change streams, which emit events whenever data changes occur. By leveraging change streams, developers can implement custom event handling logic, such as updating the UI or triggering background processes, in response to specific data changes."}),"\n",(0,i.jsx)(t.h3,{id:"json-key-compression",children:"JSON Key Compression"}),"\n",(0,i.jsxs)(t.p,{children:["To minimize storage requirements and optimize performance, RxDB offers ",(0,i.jsx)(t.a,{href:"/key-compression.html",children:"JSON key compression"}),". This feature reduces the size of keys used in the database, resulting in more efficient storage and improved query performance."]}),"\n",(0,i.jsx)(t.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,i.jsx)(t.p,{children:"RxDB offers a powerful and flexible database solution for Flutter applications. With its offline-first approach, real-time data synchronization, and reactive data handling capabilities, RxDB simplifies the development of feature-rich and scalable Flutter applications. By integrating RxDB into your Flutter projects, you can leverage its advanced features and techniques to build responsive and data-driven applications that provide an exceptional user experience."}),"\n",(0,i.jsxs)(t.h2,{id:"notice-you-can-find-the-source-code-for-an-example-rxdb-flutter-application-at-the-github-repo-1",children:["NOTICE: You can find the source code for an example RxDB Flutter Application ",(0,i.jsx)(t.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/flutter",children:"at the github repo"})]})]})}function h(e={}){const{wrapper:t}={...(0,n.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8453:(e,t,a)=>{a.d(t,{R:()=>s,x:()=>o});var i=a(6540);const n={},r=i.createContext(n);function s(e){const t=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:s(e.components),i.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/cbbe8f0a.b9b99155.js b/docs/assets/js/cbbe8f0a.b9b99155.js new file mode 100644 index 00000000000..a237ac2402a --- /dev/null +++ b/docs/assets/js/cbbe8f0a.b9b99155.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3852],{8783:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>r,contentTitle:()=>l,default:()=>h,frontMatter:()=>s,metadata:()=>c,toc:()=>a});var o=t(4848),i=t(8453);const s={title:"RxCollection",slug:"rx-collection.html"},l="RxCollection",c={id:"rx-collection",title:"RxCollection",description:"A collection stores documents of the same type.",source:"@site/docs/rx-collection.md",sourceDirName:".",slug:"/rx-collection.html",permalink:"/rx-collection.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxCollection",slug:"rx-collection.html"},sidebar:"tutorialSidebar",previous:{title:"RxSchema",permalink:"/rx-schema.html"},next:{title:"RxDocument",permalink:"/rx-document.html"}},r={},a=[{value:"Creating a Collection",id:"creating-a-collection",level:2},{value:"name",id:"name",level:3},{value:"schema",id:"schema",level:3},{value:"ORM-functions",id:"orm-functions",level:3},{value:"Migration",id:"migration",level:3},{value:"Get a collection from the database",id:"get-a-collection-from-the-database",level:2},{value:"Functions",id:"functions",level:2},{value:"Observe $",id:"observe-",level:3},{value:"insert()",id:"insert",level:3},{value:"bulkInsert()",id:"bulkinsert",level:3},{value:"bulkRemove()",id:"bulkremove",level:3},{value:"upsert()",id:"upsert",level:3},{value:"bulkUpsert()",id:"bulkupsert",level:3},{value:"incrementalUpsert()",id:"incrementalupsert",level:3},{value:"find()",id:"find",level:3},{value:"findOne()",id:"findone",level:3},{value:"findByIds()",id:"findbyids",level:3},{value:"exportJSON()",id:"exportjson",level:3},{value:"importJSON()",id:"importjson",level:3},{value:"remove()",id:"remove",level:3},{value:"destroy()",id:"destroy",level:3},{value:"isRxCollection",id:"isrxcollection",level:3},{value:"FAQ",id:"faq",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,i.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"rxcollection",children:"RxCollection"}),"\n",(0,o.jsx)(n.p,{children:"A collection stores documents of the same type."}),"\n",(0,o.jsx)(n.h2,{id:"creating-a-collection",children:"Creating a Collection"}),"\n",(0,o.jsxs)(n.p,{children:["To create one or more collections you need a RxDatabase object which has the ",(0,o.jsx)(n.code,{children:".addCollections()"}),"-method. Every collection needs a collection name and a valid ",(0,o.jsx)(n.code,{children:"RxJsonSchema"}),". Other attributes are optional."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const myCollections = await myDatabase.addCollections({\n // key = collectionName\n humans: {\n schema: mySchema,\n statics: {}, // (optional) ORM-functions for this collection\n methods: {}, // (optional) ORM-functions for documents\n attachments: {}, // (optional) ORM-functions for attachments\n options: {}, // (optional) Custom parameters that might be used in plugins\n migrationStrategies: {}, // (optional)\n autoMigrate: true, // (optional) [default=true]\n cacheReplacementPolicy: function(){}, // (optional) custom cache replacement policy\n conflictHandler: function(){} // (optional) a custom conflict handler can be used\n },\n // you can create multiple collections at once\n animals: {\n // ...\n }\n});\n"})}),"\n",(0,o.jsx)(n.h3,{id:"name",children:"name"}),"\n",(0,o.jsxs)(n.p,{children:["The name uniquely identifies the collection and should be used to refine the collection in the database. Two different collections in the same database can never have the same name. Collection names must match the following regex: ",(0,o.jsx)(n.code,{children:"^[a-z][a-z0-9]*$"}),"."]}),"\n",(0,o.jsx)(n.h3,{id:"schema",children:"schema"}),"\n",(0,o.jsxs)(n.p,{children:["The schema defines how the documents of the collection are structured. RxDB uses a schema format, similar to ",(0,o.jsx)(n.a,{href:"https://json-schema.org/",children:"JSON schema"}),". Read more about the RxDB schema format ",(0,o.jsx)(n.a,{href:"/rx-schema.html",children:"here"}),"."]}),"\n",(0,o.jsx)(n.h3,{id:"orm-functions",children:"ORM-functions"}),"\n",(0,o.jsxs)(n.p,{children:["With the parameters ",(0,o.jsx)(n.code,{children:"statics"}),", ",(0,o.jsx)(n.code,{children:"methods"})," and ",(0,o.jsx)(n.code,{children:"attachments"}),", you can define ORM-functions that are applied to each of these objects that belong to this collection. See ",(0,o.jsx)(n.a,{href:"/orm.html",children:"ORM/DRM"}),"."]}),"\n",(0,o.jsx)(n.h3,{id:"migration",children:"Migration"}),"\n",(0,o.jsxs)(n.p,{children:["With the parameters ",(0,o.jsx)(n.code,{children:"migrationStrategies"})," and ",(0,o.jsx)(n.code,{children:"autoMigrate"})," you can specify how migration between different schema-versions should be done. ",(0,o.jsx)(n.a,{href:"/migration-schema.html",children:"See Migration"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"get-a-collection-from-the-database",children:"Get a collection from the database"}),"\n",(0,o.jsx)(n.p,{children:"To get an existing collection from the database, call the collection name directly on the database:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"// newly created collection\nconst collections = await db.addCollections({\n heroes: {\n schema: mySchema\n }\n});\nconst collection2 = db.heroes;\nconsole.log(collections.heroes === collection2); //> true\n"})}),"\n",(0,o.jsx)(n.h2,{id:"functions",children:"Functions"}),"\n",(0,o.jsx)(n.h3,{id:"observe-",children:"Observe $"}),"\n",(0,o.jsxs)(n.p,{children:["Calling this will return an ",(0,o.jsx)(n.a,{href:"http://reactivex.io/rxjs/manual/overview.html#observable",children:"rxjs-Observable"})," which streams every change to data of this collection."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"myCollection.$.subscribe(changeEvent => console.dir(changeEvent));\n\n// you can also observe single event-types with insert$ update$ remove$\nmyCollection.insert$.subscribe(changeEvent => console.dir(changeEvent));\nmyCollection.update$.subscribe(changeEvent => console.dir(changeEvent));\nmyCollection.remove$.subscribe(changeEvent => console.dir(changeEvent));\n\n"})}),"\n",(0,o.jsx)(n.h3,{id:"insert",children:"insert()"}),"\n",(0,o.jsx)(n.p,{children:"Use this to insert new documents into the database. The collection will validate the schema and automatically encrypt any encrypted fields. Returns the new RxDocument."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const doc = await myCollection.insert({\n name: 'foo',\n lastname: 'bar'\n});\n"})}),"\n",(0,o.jsx)(n.h3,{id:"bulkinsert",children:"bulkInsert()"}),"\n",(0,o.jsxs)(n.p,{children:["When you have to insert many documents at once, use bulk insert. This is much faster than calling ",(0,o.jsx)(n.code,{children:".insert()"})," multiple times.\nReturns an object with a ",(0,o.jsx)(n.code,{children:"success"}),"- and ",(0,o.jsx)(n.code,{children:"error"}),"-array."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const result = await myCollection.bulkInsert([{\n name: 'foo1',\n lastname: 'bar1'\n},\n{\n name: 'foo2',\n lastname: 'bar2'\n}]);\n\n// > {\n// success: [RxDocument, RxDocument],\n// error: []\n// }\n"})}),"\n",(0,o.jsx)(n.admonition,{type:"note",children:(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.code,{children:"bulkInsert"})," will not fail on update conflicts and you cannot expect that on failure the other documents are not inserted. Also the call to ",(0,o.jsx)(n.code,{children:"bulkInsert()"})," it will not throw if a single document errors because of validation errors. Instead it will return the error in the ",(0,o.jsx)(n.code,{children:".error"})," property of the returned object."]})}),"\n",(0,o.jsx)(n.h3,{id:"bulkremove",children:"bulkRemove()"}),"\n",(0,o.jsxs)(n.p,{children:["When you want to remove many documents at once, use bulk remove. Returns an object with a ",(0,o.jsx)(n.code,{children:"success"}),"- and ",(0,o.jsx)(n.code,{children:"error"}),"-array."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const result = await myCollection.bulkRemove([\n 'primary1',\n 'primary2'\n]);\n\n// > {\n// success: [RxDocument, RxDocument],\n// error: []\n// }\n"})}),"\n",(0,o.jsx)(n.h3,{id:"upsert",children:"upsert()"}),"\n",(0,o.jsx)(n.p,{children:"Inserts the document if it does not exist within the collection, otherwise it will overwrite it. Returns the new or overwritten RxDocument."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const doc = await myCollection.upsert({\n name: 'foo',\n lastname: 'bar2'\n});\n"})}),"\n",(0,o.jsx)(n.h3,{id:"bulkupsert",children:"bulkUpsert()"}),"\n",(0,o.jsxs)(n.p,{children:["Same as ",(0,o.jsx)(n.code,{children:"upsert()"})," but runs over multiple documents. Improves performance compared to running many ",(0,o.jsx)(n.code,{children:"upsert()"})," calls.\nReturns an ",(0,o.jsx)(n.code,{children:"error"})," and a ",(0,o.jsx)(n.code,{children:"success"})," array."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const docs = await myCollection.bulkUpsert([\n {\n name: 'foo',\n lastname: 'bar2'\n },\n {\n name: 'bar',\n lastname: 'foo2'\n }\n]);\n/**\n * {\n * success: [RxDocument, RxDocument]\n * error: [],\n * }\n */\n"})}),"\n",(0,o.jsx)(n.h3,{id:"incrementalupsert",children:"incrementalUpsert()"}),"\n",(0,o.jsxs)(n.p,{children:["When you run many upsert operations on the same RxDocument in a very short timespan, you might get a ",(0,o.jsx)(n.code,{children:"409 Conflict"})," error.\nThis means that you tried to run a ",(0,o.jsx)(n.code,{children:".upsert()"})," on the document, while the previous upsert operation was still running.\nTo prevent these types of errors, you can run incremental upsert operations.\nThe behavior is similar to ",(0,o.jsx)(n.a,{href:"/rx-document.html#incrementalModify",children:"RxDocument.incrementalModify"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const docData = {\n name: 'Bob', // primary\n lastName: 'Kelso'\n};\n\nmyCollection.upsert(docData);\nmyCollection.upsert(docData);\n// -> throws because of parallel update to the same document\n\nmyCollection.incrementalUpsert(docData);\nmyCollection.incrementalUpsert(docData);\nmyCollection.incrementalUpsert(docData);\n\n// wait until last upsert finished\nawait myCollection.incrementalUpsert(docData);\n// -> works\n"})}),"\n",(0,o.jsx)(n.h3,{id:"find",children:"find()"}),"\n",(0,o.jsxs)(n.p,{children:["To find documents in your collection, use this method. ",(0,o.jsx)(n.a,{href:"/rx-query.html#find",children:"See RxQuery.find()"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find all that are older than 18\nconst olderDocuments = await myCollection\n .find()\n .where('age')\n .gt(18)\n .exec(); // execute\n"})}),"\n",(0,o.jsx)(n.h3,{id:"findone",children:"findOne()"}),"\n",(0,o.jsx)(n.p,{children:"This does basically what find() does, but it returns only a single document. You can pass a primary value to find a single document more easily."}),"\n",(0,o.jsxs)(n.p,{children:["To find documents in your collection, use this method. ",(0,o.jsx)(n.a,{href:"/rx-query.html#findOne",children:"See RxQuery.find()"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// get document with name:foobar\nmyCollection.findOne({\n selector: {\n name: 'foo'\n }\n}).exec().then(doc => console.dir(doc));\n\n// get document by primary, functionally identical to above query\nmyCollection.findOne('foo')\n .exec().then(doc => console.dir(doc));\n"})}),"\n",(0,o.jsx)(n.h3,{id:"findbyids",children:"findByIds()"}),"\n",(0,o.jsxs)(n.p,{children:["Find many documents by their id (primary value). This has a way better performance than running multiple ",(0,o.jsx)(n.code,{children:"findOne()"})," or a ",(0,o.jsx)(n.code,{children:"find()"})," with a big ",(0,o.jsx)(n.code,{children:"$or"})," selector."]}),"\n",(0,o.jsxs)(n.p,{children:["Returns a ",(0,o.jsx)(n.code,{children:"Map"})," where the primary key of the document is mapped to the document. Documents that do not exist or are deleted, will not be inside of the returned Map."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const ids = [\n 'alice',\n 'bob',\n /* ... */\n];\nconst docsMap = await myCollection.findByIds(ids);\n\nconsole.dir(docsMap); // Map(2)\n"})}),"\n",(0,o.jsx)(n.admonition,{type:"note",children:(0,o.jsxs)(n.p,{children:["The ",(0,o.jsx)(n.code,{children:"Map"})," returned by ",(0,o.jsx)(n.code,{children:"findByIds"})," is not guaranteed to return elements in the same order as the list of ids passed to it."]})}),"\n",(0,o.jsx)(n.h3,{id:"exportjson",children:"exportJSON()"}),"\n",(0,o.jsx)(n.p,{children:"Use this function to create a json export from every document in the collection."}),"\n",(0,o.jsxs)(n.p,{children:["Before ",(0,o.jsx)(n.code,{children:"exportJSON()"})," and ",(0,o.jsx)(n.code,{children:"importJSON()"})," can be used, you have to add the ",(0,o.jsx)(n.code,{children:"json-dump"})," plugin."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBJsonDumpPlugin } from 'rxdb/plugins/json-dump';\naddRxPlugin(RxDBJsonDumpPlugin);\n"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"myCollection.exportJSON()\n .then(json => console.dir(json));\n"})}),"\n",(0,o.jsx)(n.h3,{id:"importjson",children:"importJSON()"}),"\n",(0,o.jsx)(n.p,{children:"To import the json dump into your collection, use this function."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// import the dump to the database\nmyCollection.importJSON(json)\n .then(() => console.log('done'));\n"})}),"\n",(0,o.jsx)(n.p,{children:"Note that importing will fire events for each inserted document."}),"\n",(0,o.jsx)(n.h3,{id:"remove",children:"remove()"}),"\n",(0,o.jsx)(n.p,{children:"Removes all known data of the collection and its previous versions.\nThis removes the documents, the schemas, and older schemaVersions."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"await myCollection.remove();\n// collection is now removed and can be re-created\n"})}),"\n",(0,o.jsx)(n.h3,{id:"destroy",children:"destroy()"}),"\n",(0,o.jsx)(n.p,{children:"Destroys the collection's object instance. This is to free up memory and stop all observers and replications."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"await myCollection.destroy();\n"})}),"\n",(0,o.jsx)(n.h3,{id:"isrxcollection",children:"isRxCollection"}),"\n",(0,o.jsx)(n.p,{children:"Returns true if the given object is an instance of RxCollection. Returns false if not."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const is = isRxCollection(myObj);\n"})}),"\n",(0,o.jsx)(n.h2,{id:"faq",children:"FAQ"}),"\n",(0,o.jsxs)(t,{children:[(0,o.jsx)("summary",{children:"When I reload the browser window, will my collections still be in the database?"}),(0,o.jsx)("div",{children:(0,o.jsxs)(n.p,{children:["No, the javascript instance of the collections will not automatically load into the database on page reloads.\nYou have to call the ",(0,o.jsx)(n.code,{children:"addCollections()"})," method each time you create your database. This will create the JavaScript object instance of the RxCollection so that you can use it in the RxDatabase. The persisted data will be automatically in your RxCollection each time you create it."]})})]})]})}function h(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>l,x:()=>c});var o=t(6540);const i={},s=o.createContext(i);function l(e){const n=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:l(e.components),o.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/cbbe8f0a.eb6667a5.js b/docs/assets/js/cbbe8f0a.eb6667a5.js deleted file mode 100644 index 037cb8cf55b..00000000000 --- a/docs/assets/js/cbbe8f0a.eb6667a5.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[3852],{8783:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>r,contentTitle:()=>l,default:()=>h,frontMatter:()=>s,metadata:()=>c,toc:()=>a});var o=t(4848),i=t(8453);const s={title:"RxCollection",slug:"rx-collection.html"},l="RxCollection",c={id:"rx-collection",title:"RxCollection",description:"A collection stores documents of the same type.",source:"@site/docs/rx-collection.md",sourceDirName:".",slug:"/rx-collection.html",permalink:"/rx-collection.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxCollection",slug:"rx-collection.html"},sidebar:"tutorialSidebar",previous:{title:"RxSchema",permalink:"/rx-schema.html"},next:{title:"RxDocument",permalink:"/rx-document.html"}},r={},a=[{value:"Creating a Collection",id:"creating-a-collection",level:2},{value:"name",id:"name",level:3},{value:"schema",id:"schema",level:3},{value:"ORM-functions",id:"orm-functions",level:3},{value:"Migration",id:"migration",level:3},{value:"Get a collection from the database",id:"get-a-collection-from-the-database",level:2},{value:"Functions",id:"functions",level:2},{value:"Observe $",id:"observe-",level:3},{value:"insert()",id:"insert",level:3},{value:"bulkInsert()",id:"bulkinsert",level:3},{value:"bulkRemove()",id:"bulkremove",level:3},{value:"upsert()",id:"upsert",level:3},{value:"bulkUpsert()",id:"bulkupsert",level:3},{value:"incrementalUpsert()",id:"incrementalupsert",level:3},{value:"find()",id:"find",level:3},{value:"findOne()",id:"findone",level:3},{value:"findByIds()",id:"findbyids",level:3},{value:"exportJSON()",id:"exportjson",level:3},{value:"importJSON()",id:"importjson",level:3},{value:"remove()",id:"remove",level:3},{value:"destroy()",id:"destroy",level:3},{value:"isRxCollection",id:"isrxcollection",level:3},{value:"FAQ",id:"faq",level:2}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,i.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"rxcollection",children:"RxCollection"}),"\n",(0,o.jsx)(n.p,{children:"A collection stores documents of the same type."}),"\n",(0,o.jsx)(n.h2,{id:"creating-a-collection",children:"Creating a Collection"}),"\n",(0,o.jsxs)(n.p,{children:["To create one or more collections you need a RxDatabase object which has the ",(0,o.jsx)(n.code,{children:".addCollections()"}),"-method. Every collection needs a collection name and a valid ",(0,o.jsx)(n.code,{children:"RxJsonSchema"}),". Other attributes are optional."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const myCollections = await myDatabase.addCollections({\n // key = collectionName\n humans: {\n schema: mySchema,\n statics: {}, // (optional) ORM-functions for this collection\n methods: {}, // (optional) ORM-functions for documents\n attachments: {}, // (optional) ORM-functions for attachments\n options: {}, // (optional) Custom parameters that might be used in plugins\n migrationStrategies: {}, // (optional)\n autoMigrate: true, // (optional) [default=true]\n cacheReplacementPolicy: function(){}, // (optional) custom cache replacement policy\n conflictHandler: function(){} // (optional) a custom conflict handler can be used\n },\n // you can create multiple collections at once\n animals: {\n // ...\n }\n});\n"})}),"\n",(0,o.jsx)(n.h3,{id:"name",children:"name"}),"\n",(0,o.jsxs)(n.p,{children:["The name uniquely identifies the collection and should be used to refine the collection in the database. Two different collections in the same database can never have the same name. Collection names must match the following regex: ",(0,o.jsx)(n.code,{children:"^[a-z][a-z0-9]*$"}),"."]}),"\n",(0,o.jsx)(n.h3,{id:"schema",children:"schema"}),"\n",(0,o.jsxs)(n.p,{children:["The schema defines how the documents of the collection are structured. RxDB uses a schema format, similar to ",(0,o.jsx)(n.a,{href:"https://json-schema.org/",children:"JSON schema"}),". Read more about the RxDB schema format ",(0,o.jsx)(n.a,{href:"/rx-schema.html",children:"here"}),"."]}),"\n",(0,o.jsx)(n.h3,{id:"orm-functions",children:"ORM-functions"}),"\n",(0,o.jsxs)(n.p,{children:["With the parameters ",(0,o.jsx)(n.code,{children:"statics"}),", ",(0,o.jsx)(n.code,{children:"methods"})," and ",(0,o.jsx)(n.code,{children:"attachments"}),", you can define ORM-functions that are applied to each of these objects that belong to this collection. See ",(0,o.jsx)(n.a,{href:"/orm.html",children:"ORM/DRM"}),"."]}),"\n",(0,o.jsx)(n.h3,{id:"migration",children:"Migration"}),"\n",(0,o.jsxs)(n.p,{children:["With the parameters ",(0,o.jsx)(n.code,{children:"migrationStrategies"})," and ",(0,o.jsx)(n.code,{children:"autoMigrate"})," you can specify how migration between different schema-versions should be done. ",(0,o.jsx)(n.a,{href:"/migration-schema.html",children:"See Migration"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"get-a-collection-from-the-database",children:"Get a collection from the database"}),"\n",(0,o.jsx)(n.p,{children:"To get an existing collection from the database, call the collection name directly on the database:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"// newly created collection\nconst collections = await db.addCollections({\n heroes: {\n schema: mySchema\n }\n});\nconst collection2 = db.heroes;\nconsole.log(collections.heroes === collection2); //> true\n"})}),"\n",(0,o.jsx)(n.h2,{id:"functions",children:"Functions"}),"\n",(0,o.jsx)(n.h3,{id:"observe-",children:"Observe $"}),"\n",(0,o.jsxs)(n.p,{children:["Calling this will return an ",(0,o.jsx)(n.a,{href:"http://reactivex.io/rxjs/manual/overview.html#observable",children:"rxjs-Observable"})," which streams every change to data of this collection."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"myCollection.$.subscribe(changeEvent => console.dir(changeEvent));\n\n// you can also observe single event-types with insert$ update$ remove$\nmyCollection.insert$.subscribe(changeEvent => console.dir(changeEvent));\nmyCollection.update$.subscribe(changeEvent => console.dir(changeEvent));\nmyCollection.remove$.subscribe(changeEvent => console.dir(changeEvent));\n\n"})}),"\n",(0,o.jsx)(n.h3,{id:"insert",children:"insert()"}),"\n",(0,o.jsx)(n.p,{children:"Use this to insert new documents into the database. The collection will validate the schema and automatically encrypt any encrypted fields. Returns the new RxDocument."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const doc = await myCollection.insert({\n name: 'foo',\n lastname: 'bar'\n});\n"})}),"\n",(0,o.jsx)(n.h3,{id:"bulkinsert",children:"bulkInsert()"}),"\n",(0,o.jsxs)(n.p,{children:["When you have to insert many documents at once, use bulk insert. This is much faster than calling ",(0,o.jsx)(n.code,{children:".insert()"})," multiple times.\nReturns an object with a ",(0,o.jsx)(n.code,{children:"success"}),"- and ",(0,o.jsx)(n.code,{children:"error"}),"-array."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const result = await myCollection.bulkInsert([{\n name: 'foo1',\n lastname: 'bar1'\n},\n{\n name: 'foo2',\n lastname: 'bar2'\n}]);\n\n// > {\n// success: [RxDocument, RxDocument],\n// error: []\n// }\n"})}),"\n",(0,o.jsxs)(n.p,{children:["NOTICE: ",(0,o.jsx)(n.code,{children:"bulkInsert"})," will not fail on update conflicts and you cannot expect that on failure the other documents are not inserted. Also the call to ",(0,o.jsx)(n.code,{children:"bulkInsert()"})," it will not throw if a single document errors because of validation errors. Instead it will return the error in the ",(0,o.jsx)(n.code,{children:".error"})," property of the returned object."]}),"\n",(0,o.jsx)(n.h3,{id:"bulkremove",children:"bulkRemove()"}),"\n",(0,o.jsxs)(n.p,{children:["When you want to remove many documents at once, use bulk remove. Returns an object with a ",(0,o.jsx)(n.code,{children:"success"}),"- and ",(0,o.jsx)(n.code,{children:"error"}),"-array."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const result = await myCollection.bulkRemove([\n 'primary1',\n 'primary2'\n]);\n\n// > {\n// success: [RxDocument, RxDocument],\n// error: []\n// }\n"})}),"\n",(0,o.jsx)(n.h3,{id:"upsert",children:"upsert()"}),"\n",(0,o.jsx)(n.p,{children:"Inserts the document if it does not exist within the collection, otherwise it will overwrite it. Returns the new or overwritten RxDocument."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const doc = await myCollection.upsert({\n name: 'foo',\n lastname: 'bar2'\n});\n"})}),"\n",(0,o.jsx)(n.h3,{id:"bulkupsert",children:"bulkUpsert()"}),"\n",(0,o.jsxs)(n.p,{children:["Same as ",(0,o.jsx)(n.code,{children:"upsert()"})," but runs over multiple documents. Improves performance compared to running many ",(0,o.jsx)(n.code,{children:"upsert()"})," calls.\nReturns an ",(0,o.jsx)(n.code,{children:"error"})," and a ",(0,o.jsx)(n.code,{children:"success"})," array."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const docs = await myCollection.bulkUpsert([\n {\n name: 'foo',\n lastname: 'bar2'\n },\n {\n name: 'bar',\n lastname: 'foo2'\n }\n]);\n/**\n * {\n * success: [RxDocument, RxDocument]\n * error: [],\n * }\n */\n"})}),"\n",(0,o.jsx)(n.h3,{id:"incrementalupsert",children:"incrementalUpsert()"}),"\n",(0,o.jsxs)(n.p,{children:["When you run many upsert operations on the same RxDocument in a very short timespan, you might get a ",(0,o.jsx)(n.code,{children:"409 Conflict"})," error.\nThis means that you tried to run a ",(0,o.jsx)(n.code,{children:".upsert()"})," on the document, while the previous upsert operation was still running.\nTo prevent these types of errors, you can run incremental upsert operations.\nThe behavior is similar to ",(0,o.jsx)(n.a,{href:"/rx-document.html#incrementalModify",children:"RxDocument.incrementalModify"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const docData = {\n name: 'Bob', // primary\n lastName: 'Kelso'\n};\n\nmyCollection.upsert(docData);\nmyCollection.upsert(docData);\n// -> throws because of parallel update to the same document\n\nmyCollection.incrementalUpsert(docData);\nmyCollection.incrementalUpsert(docData);\nmyCollection.incrementalUpsert(docData);\n\n// wait until last upsert finished\nawait myCollection.incrementalUpsert(docData);\n// -> works\n"})}),"\n",(0,o.jsx)(n.h3,{id:"find",children:"find()"}),"\n",(0,o.jsxs)(n.p,{children:["To find documents in your collection, use this method. ",(0,o.jsx)(n.a,{href:"/rx-query.html#find",children:"See RxQuery.find()"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// find all that are older than 18\nconst olderDocuments = await myCollection\n .find()\n .where('age')\n .gt(18)\n .exec(); // execute\n"})}),"\n",(0,o.jsx)(n.h3,{id:"findone",children:"findOne()"}),"\n",(0,o.jsx)(n.p,{children:"This does basically what find() does, but it returns only a single document. You can pass a primary value to find a single document more easily."}),"\n",(0,o.jsxs)(n.p,{children:["To find documents in your collection, use this method. ",(0,o.jsx)(n.a,{href:"/rx-query.html#findOne",children:"See RxQuery.find()"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// get document with name:foobar\nmyCollection.findOne({\n selector: {\n name: 'foo'\n }\n}).exec().then(doc => console.dir(doc));\n\n// get document by primary, functionally identical to above query\nmyCollection.findOne('foo')\n .exec().then(doc => console.dir(doc));\n"})}),"\n",(0,o.jsx)(n.h3,{id:"findbyids",children:"findByIds()"}),"\n",(0,o.jsxs)(n.p,{children:["Find many documents by their id (primary value). This has a way better performance than running multiple ",(0,o.jsx)(n.code,{children:"findOne()"})," or a ",(0,o.jsx)(n.code,{children:"find()"})," with a big ",(0,o.jsx)(n.code,{children:"$or"})," selector."]}),"\n",(0,o.jsxs)(n.p,{children:["Returns a ",(0,o.jsx)(n.code,{children:"Map"})," where the primary key of the document is mapped to the document. Documents that do not exist or are deleted, will not be inside of the returned Map."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const ids = [\n 'alice',\n 'bob',\n /* ... */\n];\nconst docsMap = await myCollection.findByIds(ids);\n\nconsole.dir(docsMap); // Map(2)\n"})}),"\n",(0,o.jsxs)(n.p,{children:["NOTICE: The ",(0,o.jsx)(n.code,{children:"Map"})," returned by ",(0,o.jsx)(n.code,{children:"findByIds"})," is not guaranteed to return elements in the same order as the list of ids passed to it."]}),"\n",(0,o.jsx)(n.h3,{id:"exportjson",children:"exportJSON()"}),"\n",(0,o.jsx)(n.p,{children:"Use this function to create a json export from every document in the collection."}),"\n",(0,o.jsxs)(n.p,{children:["Before ",(0,o.jsx)(n.code,{children:"exportJSON()"})," and ",(0,o.jsx)(n.code,{children:"importJSON()"})," can be used, you have to add the ",(0,o.jsx)(n.code,{children:"json-dump"})," plugin."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"import { addRxPlugin } from 'rxdb';\nimport { RxDBJsonDumpPlugin } from 'rxdb/plugins/json-dump';\naddRxPlugin(RxDBJsonDumpPlugin);\n"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"myCollection.exportJSON()\n .then(json => console.dir(json));\n"})}),"\n",(0,o.jsx)(n.h3,{id:"importjson",children:"importJSON()"}),"\n",(0,o.jsx)(n.p,{children:"To import the json dump into your collection, use this function."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// import the dump to the database\nmyCollection.importJSON(json)\n .then(() => console.log('done'));\n"})}),"\n",(0,o.jsx)(n.p,{children:"Note that importing will fire events for each inserted document."}),"\n",(0,o.jsx)(n.h3,{id:"remove",children:"remove()"}),"\n",(0,o.jsx)(n.p,{children:"Removes all known data of the collection and its previous versions.\nThis removes the documents, the schemas, and older schemaVersions."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"await myCollection.remove();\n// collection is now removed and can be re-created\n"})}),"\n",(0,o.jsx)(n.h3,{id:"destroy",children:"destroy()"}),"\n",(0,o.jsx)(n.p,{children:"Destroys the collection's object instance. This is to free up memory and stop all observers and replications."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"await myCollection.destroy();\n"})}),"\n",(0,o.jsx)(n.h3,{id:"isrxcollection",children:"isRxCollection"}),"\n",(0,o.jsx)(n.p,{children:"Returns true if the given object is an instance of RxCollection. Returns false if not."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const is = isRxCollection(myObj);\n"})}),"\n",(0,o.jsx)(n.h2,{id:"faq",children:"FAQ"}),"\n",(0,o.jsxs)(t,{children:[(0,o.jsx)("summary",{children:"When I reload the browser window, will my collections still be in the database?"}),(0,o.jsx)("div",{children:(0,o.jsxs)(n.p,{children:["No, the javascript instance of the collections will not automatically load into the database on page reloads.\nYou have to call the ",(0,o.jsx)(n.code,{children:"addCollections()"})," method each time you create your database. This will create the JavaScript object instance of the RxCollection so that you can use it in the RxDatabase. The persisted data will be automatically in your RxCollection each time you create it."]})})]})]})}function h(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>l,x:()=>c});var o=t(6540);const i={},s=o.createContext(i);function l(e){const n=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:l(e.components),o.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/dbde2ffe.60ce37ce.js b/docs/assets/js/dbde2ffe.60ce37ce.js new file mode 100644 index 00000000000..05016554b56 --- /dev/null +++ b/docs/assets/js/dbde2ffe.60ce37ce.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6543],{118:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>i,contentTitle:()=>l,default:()=>u,frontMatter:()=>s,metadata:()=>c,toc:()=>r});var a=t(4848),o=t(8453);const s={title:"RxDocument",slug:"rx-document.html"},l="RxDocument",c={id:"rx-document",title:"RxDocument",description:"A document is a single object which is stored in a collection. It can be compared to a single record in a relational database table. You get an RxDocument either as return on inserts, or as result-set of queries.",source:"@site/docs/rx-document.md",sourceDirName:".",slug:"/rx-document.html",permalink:"/rx-document.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDocument",slug:"rx-document.html"},sidebar:"tutorialSidebar",previous:{title:"RxCollection",permalink:"/rx-collection.html"},next:{title:"RxQuery",permalink:"/rx-query.html"}},i={},r=[{value:"insert",id:"insert",level:2},{value:"find",id:"find",level:2},{value:"Functions",id:"functions",level:2},{value:"get()",id:"get",level:3},{value:"get$()",id:"get-1",level:3},{value:"proxy-get",id:"proxy-get",level:3},{value:"update()",id:"update",level:3},{value:"modify()",id:"modify",level:3},{value:"patch()",id:"patch",level:3},{value:"Prevent conflicts with the incremental methods",id:"prevent-conflicts-with-the-incremental-methods",level:3},{value:"getLatest()",id:"getlatest",level:3},{value:"Observe $",id:"observe-",level:3},{value:"remove()",id:"remove",level:3},{value:"deleted$",id:"deleted",level:3},{value:"get deleted",id:"get-deleted",level:3},{value:"toJSON()",id:"tojson",level:3},{value:"toMutableJSON()",id:"tomutablejson",level:3},{value:"isRxDocument",id:"isrxdocument",level:3}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,o.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.h1,{id:"rxdocument",children:"RxDocument"}),"\n",(0,a.jsxs)(n.p,{children:["A document is a single object which is stored in a collection. It can be compared to a single record in a relational database table. You get an ",(0,a.jsx)(n.code,{children:"RxDocument"})," either as return on inserts, or as result-set of queries."]}),"\n",(0,a.jsx)(n.h2,{id:"insert",children:"insert"}),"\n",(0,a.jsx)(n.p,{children:"To insert a document into a collection, you have to call the collection's .insert()-function."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"myCollection.insert({\n name: 'foo',\n lastname: 'bar'\n});\n"})}),"\n",(0,a.jsx)(n.h2,{id:"find",children:"find"}),"\n",(0,a.jsxs)(n.p,{children:["To find documents in a collection, you have to call the collection's .find()-function. ",(0,a.jsx)(n.a,{href:"/rx-query.html",children:"See RxQuery"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"myCollection.find().exec() // <- find all documents\n .then(documents => console.dir(documents));\n"})}),"\n",(0,a.jsx)(n.h2,{id:"functions",children:"Functions"}),"\n",(0,a.jsx)(n.h3,{id:"get",children:"get()"}),"\n",(0,a.jsx)(n.p,{children:"This will get a single field of the document. If the field is encrypted, it will be automatically decrypted before returning."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"var name = myDocument.get('name'); // returns the name\n"})}),"\n",(0,a.jsx)(n.h3,{id:"get-1",children:"get$()"}),"\n",(0,a.jsx)(n.p,{children:"This function returns an observable of the given paths-value.\nThe current value of this path will be emitted each time the document changes."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"// get the live-updating value of 'name'\nvar isName;\nmyDocument.get$('name')\n .subscribe(newName => {\n isName = newName;\n });\n\nawait myDocument.incrementalPatch({name: 'foobar2'});\nconsole.dir(isName); // isName is now 'foobar2'\n"})}),"\n",(0,a.jsx)(n.h3,{id:"proxy-get",children:"proxy-get"}),"\n",(0,a.jsxs)(n.p,{children:["All properties of a ",(0,a.jsx)(n.code,{children:"RxDocument"})," are assigned as getters so you can also directly access values instead of using the get()-function."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:" // Identical to myDocument.get('name');\n var name = myDocument.name;\n // Can also get nested values.\n var nestedValue = myDocument.whatever.nestedfield;\n\n // Also usable with observables:\n myDocument.firstName$.subscribe(newName => console.log('name is: ' + newName));\n // > 'name is: Stefe'\n await myDocument.incrementalPatch({firstName: 'Steve'});\n // > 'name is: Steve'\n"})}),"\n",(0,a.jsx)(n.h3,{id:"update",children:"update()"}),"\n",(0,a.jsxs)(n.p,{children:["Updates the document based on the ",(0,a.jsx)(n.a,{href:"https://docs.mongodb.com/manual/reference/operator/update-field/",children:"mongo-update-syntax"}),", based on the ",(0,a.jsx)(n.a,{href:"https://github.com/kofrasa/mingo#updating-documents",children:"mingo library"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"\n/**\n * If not done before, you have to add the update plugin.\n */\nimport { addRxPlugin } from 'rxdb';\nimport { RxDBUpdatePlugin } from 'rxdb/plugins/update';\naddRxPlugin(RxDBUpdatePlugin);\n\nawait myDocument.update({\n $inc: {\n age: 1 // increases age by 1\n },\n $set: {\n firstName: 'foobar' // sets firstName to foobar\n }\n});\n"})}),"\n",(0,a.jsx)(n.h3,{id:"modify",children:"modify()"}),"\n",(0,a.jsx)(n.p,{children:"Updates a documents data based on a function that mutates the current data and returns the new value."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"\nconst changeFunction = (oldData) => {\n oldData.age = oldData.age + 1;\n oldData.name = 'foooobarNew';\n return oldData;\n}\nawait myDocument.modify(changeFunction);\nconsole.log(myDocument.name); // 'foooobarNew'\n"})}),"\n",(0,a.jsx)(n.h3,{id:"patch",children:"patch()"}),"\n",(0,a.jsx)(n.p,{children:"Overwrites the given attributes over the documents data."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"await myDocument.patch({\n name: 'Steve',\n age: undefined // setting an attribute to undefined will remove it\n});\nconsole.log(myDocument.name); // 'Steve'\n"})}),"\n",(0,a.jsx)(n.h3,{id:"prevent-conflicts-with-the-incremental-methods",children:"Prevent conflicts with the incremental methods"}),"\n",(0,a.jsxs)(n.p,{children:["Making a normal change to the non-latest version of a ",(0,a.jsx)(n.code,{children:"RxDocument"})," will lead to a ",(0,a.jsx)(n.code,{children:"409 CONFLICT"})," error because RxDB\nuses ",(0,a.jsx)(n.a,{href:"/transactions-conflicts-revisions.html",children:"revision checks"})," instead of transactions."]}),"\n",(0,a.jsxs)(n.p,{children:["To make a change to a document, no matter what the current state is, you can use the ",(0,a.jsx)(n.code,{children:"incremental"})," methods:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"// update\nawait myDocument.incrementalUpdate({\n $inc: {\n age: 1 // increases age by 1\n }\n});\n\n// modify\nawait myDocument.incrementalModify(docData => {\n docData.age = docData.age + 1;\n return docData;\n});\n\n// patch\nawait myDocument.incrementalPatch({\n age: 100\n});\n\n// remove\nawait myDocument.incrementalRemove({\n age: 100\n});\n"})}),"\n",(0,a.jsx)(n.h3,{id:"getlatest",children:"getLatest()"}),"\n",(0,a.jsxs)(n.p,{children:["Returns the latest known state of the ",(0,a.jsx)(n.code,{children:"RxDocument"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const myDocument = await myCollection.findOne('foobar').exec();\nconst docAfterEdit = await myDocument.incrementalPatch({\n age: 10\n});\nconst latestDoc = myDocument.getLatest();\nconsole.log(docAfterEdit === latestDoc); // > true\n"})}),"\n",(0,a.jsx)(n.h3,{id:"observe-",children:"Observe $"}),"\n",(0,a.jsxs)(n.p,{children:["Calling this will return an ",(0,a.jsx)(n.a,{href:"http://reactivex.io/rxjs/manual/overview.html#observable",children:"rxjs-Observable"})," which the current newest state of the RxDocument."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"// get all changeEvents\nmyDocument.$\n .subscribe(currentRxDocument => console.dir(currentRxDocument));\n"})}),"\n",(0,a.jsx)(n.h3,{id:"remove",children:"remove()"}),"\n",(0,a.jsxs)(n.p,{children:["This removes the document from the collection. Notice that this will not purge the document from the store but set ",(0,a.jsx)(n.code,{children:"_deleted:true"})," so that it will be no longer returned on queries.\nTo fully purge a document, use the ",(0,a.jsx)(n.a,{href:"/cleanup.html",children:"cleanup plugin"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"myDocument.remove();\n"})}),"\n",(0,a.jsx)(n.h3,{id:"deleted",children:"deleted$"}),"\n",(0,a.jsx)(n.p,{children:"Emits a boolean value, depending on whether the RxDocument is deleted or not."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"let lastState = null;\nmyDocument.deleted$.subscribe(state => lastState = state);\n\nconsole.log(lastState);\n// false\n\nawait myDocument.remove();\n\nconsole.log(lastState);\n// true\n"})}),"\n",(0,a.jsx)(n.h3,{id:"get-deleted",children:"get deleted"}),"\n",(0,a.jsxs)(n.p,{children:["A getter to get the current value of ",(0,a.jsx)(n.code,{children:"deleted$"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"console.log(myDocument.deleted);\n// false\n\nawait myDocument.remove();\n\nconsole.log(myDocument.deleted);\n// true\n"})}),"\n",(0,a.jsx)(n.h3,{id:"tojson",children:"toJSON()"}),"\n",(0,a.jsxs)(n.p,{children:["Returns the document's data as plain json object. This will return an ",(0,a.jsx)(n.strong,{children:"immutable"})," object. To get something that can be modified, use ",(0,a.jsx)(n.code,{children:"toMutableJSON()"})," instead."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const json = myDocument.toJSON();\nconsole.dir(json);\n/* { passportId: 'h1rg9ugdd30o',\n firstName: 'Carolina',\n lastName: 'Gibson',\n age: 33 ...\n*/\n"})}),"\n",(0,a.jsxs)(n.p,{children:["You can also set ",(0,a.jsx)(n.code,{children:"withMetaFields: true"})," to get additional meta fields like the revision, attachments or the deleted flag."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const json = myDocument.toJSON(true);\nconsole.dir(json);\n/* { passportId: 'h1rg9ugdd30o',\n firstName: 'Carolina',\n lastName: 'Gibson',\n _deleted: false,\n _attachments: { ... },\n _rev: '1-aklsdjfhaklsdjhf...'\n*/\n"})}),"\n",(0,a.jsx)(n.h3,{id:"tomutablejson",children:"toMutableJSON()"}),"\n",(0,a.jsxs)(n.p,{children:["Same as ",(0,a.jsx)(n.code,{children:"toJSON()"})," but returns a deep cloned object that can be mutated afterwards.\nRemember that deep cloning is performance expensive and should only be done when necessary."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const json = myDocument.toMutableJSON();\njson.firstName = 'Alice'; // The returned document can be mutated\n"})}),"\n",(0,a.jsx)(n.admonition,{title:"All methods of RxDocument are bound to the instance",type:"note",children:(0,a.jsxs)(n.p,{children:["When you get a method from a ",(0,a.jsx)(n.code,{children:"RxDocument"}),", the method is automatically bound to the documents instance. This means you do not have to use things like ",(0,a.jsx)(n.code,{children:"myMethod.bind(myDocument)"})," like you would do in jsx."]})}),"\n",(0,a.jsx)(n.h3,{id:"isrxdocument",children:"isRxDocument"}),"\n",(0,a.jsx)(n.p,{children:"Returns true if the given object is an instance of RxDocument. Returns false if not."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const is = isRxDocument(myObj);\n"})})]})}function u(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>l,x:()=>c});var a=t(6540);const o={},s=a.createContext(o);function l(e){const n=a.useContext(s);return a.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:l(e.components),a.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/dbde2ffe.efe0bd63.js b/docs/assets/js/dbde2ffe.efe0bd63.js deleted file mode 100644 index 12c0072849f..00000000000 --- a/docs/assets/js/dbde2ffe.efe0bd63.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6543],{118:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>i,contentTitle:()=>l,default:()=>u,frontMatter:()=>s,metadata:()=>c,toc:()=>r});var a=t(4848),o=t(8453);const s={title:"RxDocument",slug:"rx-document.html"},l="RxDocument",c={id:"rx-document",title:"RxDocument",description:"A document is a single object which is stored in a collection. It can be compared to a single record in a relational database table. You get an RxDocument either as return on inserts, or as result-set of queries.",source:"@site/docs/rx-document.md",sourceDirName:".",slug:"/rx-document.html",permalink:"/rx-document.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDocument",slug:"rx-document.html"},sidebar:"tutorialSidebar",previous:{title:"RxCollection",permalink:"/rx-collection.html"},next:{title:"RxQuery",permalink:"/rx-query.html"}},i={},r=[{value:"insert",id:"insert",level:2},{value:"find",id:"find",level:2},{value:"Functions",id:"functions",level:2},{value:"get()",id:"get",level:3},{value:"get$()",id:"get-1",level:3},{value:"proxy-get",id:"proxy-get",level:3},{value:"update()",id:"update",level:3},{value:"modify()",id:"modify",level:3},{value:"patch()",id:"patch",level:3},{value:"Prevent conflicts with the incremental methods",id:"prevent-conflicts-with-the-incremental-methods",level:3},{value:"getLatest()",id:"getlatest",level:3},{value:"Observe $",id:"observe-",level:3},{value:"remove()",id:"remove",level:3},{value:"deleted$",id:"deleted",level:3},{value:"get deleted",id:"get-deleted",level:3},{value:"toJSON()",id:"tojson",level:3},{value:"toMutableJSON()",id:"tomutablejson",level:3},{value:"NOTICE: All methods of RxDocument are bound to the instance",id:"notice-all-methods-of-rxdocument-are-bound-to-the-instance",level:2},{value:"isRxDocument",id:"isrxdocument",level:3}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,o.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.h1,{id:"rxdocument",children:"RxDocument"}),"\n",(0,a.jsxs)(n.p,{children:["A document is a single object which is stored in a collection. It can be compared to a single record in a relational database table. You get an ",(0,a.jsx)(n.code,{children:"RxDocument"})," either as return on inserts, or as result-set of queries."]}),"\n",(0,a.jsx)(n.h2,{id:"insert",children:"insert"}),"\n",(0,a.jsx)(n.p,{children:"To insert a document into a collection, you have to call the collection's .insert()-function."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"myCollection.insert({\n name: 'foo',\n lastname: 'bar'\n});\n"})}),"\n",(0,a.jsx)(n.h2,{id:"find",children:"find"}),"\n",(0,a.jsxs)(n.p,{children:["To find documents in a collection, you have to call the collection's .find()-function. ",(0,a.jsx)(n.a,{href:"/rx-query.html",children:"See RxQuery"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"myCollection.find().exec() // <- find all documents\n .then(documents => console.dir(documents));\n"})}),"\n",(0,a.jsx)(n.h2,{id:"functions",children:"Functions"}),"\n",(0,a.jsx)(n.h3,{id:"get",children:"get()"}),"\n",(0,a.jsx)(n.p,{children:"This will get a single field of the document. If the field is encrypted, it will be automatically decrypted before returning."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"var name = myDocument.get('name'); // returns the name\n"})}),"\n",(0,a.jsx)(n.h3,{id:"get-1",children:"get$()"}),"\n",(0,a.jsx)(n.p,{children:"This function returns an observable of the given paths-value.\nThe current value of this path will be emitted each time the document changes."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"// get the live-updating value of 'name'\nvar isName;\nmyDocument.get$('name')\n .subscribe(newName => {\n isName = newName;\n });\n\nawait myDocument.incrementalPatch({name: 'foobar2'});\nconsole.dir(isName); // isName is now 'foobar2'\n"})}),"\n",(0,a.jsx)(n.h3,{id:"proxy-get",children:"proxy-get"}),"\n",(0,a.jsxs)(n.p,{children:["All properties of a ",(0,a.jsx)(n.code,{children:"RxDocument"})," are assigned as getters so you can also directly access values instead of using the get()-function."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:" // Identical to myDocument.get('name');\n var name = myDocument.name;\n // Can also get nested values.\n var nestedValue = myDocument.whatever.nestedfield;\n\n // Also usable with observables:\n myDocument.firstName$.subscribe(newName => console.log('name is: ' + newName));\n // > 'name is: Stefe'\n await myDocument.incrementalPatch({firstName: 'Steve'});\n // > 'name is: Steve'\n"})}),"\n",(0,a.jsx)(n.h3,{id:"update",children:"update()"}),"\n",(0,a.jsxs)(n.p,{children:["Updates the document based on the ",(0,a.jsx)(n.a,{href:"https://docs.mongodb.com/manual/reference/operator/update-field/",children:"mongo-update-syntax"}),", based on the ",(0,a.jsx)(n.a,{href:"https://github.com/kofrasa/mingo#updating-documents",children:"mingo library"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"\n/**\n * If not done before, you have to add the update plugin.\n */\nimport { addRxPlugin } from 'rxdb';\nimport { RxDBUpdatePlugin } from 'rxdb/plugins/update';\naddRxPlugin(RxDBUpdatePlugin);\n\nawait myDocument.update({\n $inc: {\n age: 1 // increases age by 1\n },\n $set: {\n firstName: 'foobar' // sets firstName to foobar\n }\n});\n"})}),"\n",(0,a.jsx)(n.h3,{id:"modify",children:"modify()"}),"\n",(0,a.jsx)(n.p,{children:"Updates a documents data based on a function that mutates the current data and returns the new value."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"\nconst changeFunction = (oldData) => {\n oldData.age = oldData.age + 1;\n oldData.name = 'foooobarNew';\n return oldData;\n}\nawait myDocument.modify(changeFunction);\nconsole.log(myDocument.name); // 'foooobarNew'\n"})}),"\n",(0,a.jsx)(n.h3,{id:"patch",children:"patch()"}),"\n",(0,a.jsx)(n.p,{children:"Overwrites the given attributes over the documents data."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"await myDocument.patch({\n name: 'Steve',\n age: undefined // setting an attribute to undefined will remove it\n});\nconsole.log(myDocument.name); // 'Steve'\n"})}),"\n",(0,a.jsx)(n.h3,{id:"prevent-conflicts-with-the-incremental-methods",children:"Prevent conflicts with the incremental methods"}),"\n",(0,a.jsxs)(n.p,{children:["Making a normal change to the non-latest version of a ",(0,a.jsx)(n.code,{children:"RxDocument"})," will lead to a ",(0,a.jsx)(n.code,{children:"409 CONFLICT"})," error because RxDB\nuses ",(0,a.jsx)(n.a,{href:"/transactions-conflicts-revisions.html",children:"revision checks"})," instead of transactions."]}),"\n",(0,a.jsxs)(n.p,{children:["To make a change to a document, no matter what the current state is, you can use the ",(0,a.jsx)(n.code,{children:"incremental"})," methods:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"// update\nawait myDocument.incrementalUpdate({\n $inc: {\n age: 1 // increases age by 1\n }\n});\n\n// modify\nawait myDocument.incrementalModify(docData => {\n docData.age = docData.age + 1;\n return docData;\n});\n\n// patch\nawait myDocument.incrementalPatch({\n age: 100\n});\n\n// remove\nawait myDocument.incrementalRemove({\n age: 100\n});\n"})}),"\n",(0,a.jsx)(n.h3,{id:"getlatest",children:"getLatest()"}),"\n",(0,a.jsxs)(n.p,{children:["Returns the latest known state of the ",(0,a.jsx)(n.code,{children:"RxDocument"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const myDocument = await myCollection.findOne('foobar').exec();\nconst docAfterEdit = await myDocument.incrementalPatch({\n age: 10\n});\nconst latestDoc = myDocument.getLatest();\nconsole.log(docAfterEdit === latestDoc); // > true\n"})}),"\n",(0,a.jsx)(n.h3,{id:"observe-",children:"Observe $"}),"\n",(0,a.jsxs)(n.p,{children:["Calling this will return an ",(0,a.jsx)(n.a,{href:"http://reactivex.io/rxjs/manual/overview.html#observable",children:"rxjs-Observable"})," which the current newest state of the RxDocument."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"// get all changeEvents\nmyDocument.$\n .subscribe(currentRxDocument => console.dir(currentRxDocument));\n"})}),"\n",(0,a.jsx)(n.h3,{id:"remove",children:"remove()"}),"\n",(0,a.jsxs)(n.p,{children:["This removes the document from the collection. Notice that this will not purge the document from the store but set ",(0,a.jsx)(n.code,{children:"_deleted:true"})," so that it will be no longer returned on queries.\nTo fully purge a document, use the ",(0,a.jsx)(n.a,{href:"/cleanup.html",children:"cleanup plugin"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"myDocument.remove();\n"})}),"\n",(0,a.jsx)(n.h3,{id:"deleted",children:"deleted$"}),"\n",(0,a.jsx)(n.p,{children:"Emits a boolean value, depending on whether the RxDocument is deleted or not."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"let lastState = null;\nmyDocument.deleted$.subscribe(state => lastState = state);\n\nconsole.log(lastState);\n// false\n\nawait myDocument.remove();\n\nconsole.log(lastState);\n// true\n"})}),"\n",(0,a.jsx)(n.h3,{id:"get-deleted",children:"get deleted"}),"\n",(0,a.jsxs)(n.p,{children:["A getter to get the current value of ",(0,a.jsx)(n.code,{children:"deleted$"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"console.log(myDocument.deleted);\n// false\n\nawait myDocument.remove();\n\nconsole.log(myDocument.deleted);\n// true\n"})}),"\n",(0,a.jsx)(n.h3,{id:"tojson",children:"toJSON()"}),"\n",(0,a.jsxs)(n.p,{children:["Returns the document's data as plain json object. This will return an ",(0,a.jsx)(n.strong,{children:"immutable"})," object. To get something that can be modified, use ",(0,a.jsx)(n.code,{children:"toMutableJSON()"})," instead."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const json = myDocument.toJSON();\nconsole.dir(json);\n/* { passportId: 'h1rg9ugdd30o',\n firstName: 'Carolina',\n lastName: 'Gibson',\n age: 33 ...\n*/\n"})}),"\n",(0,a.jsxs)(n.p,{children:["You can also set ",(0,a.jsx)(n.code,{children:"withMetaFields: true"})," to get additional meta fields like the revision, attachments or the deleted flag."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const json = myDocument.toJSON(true);\nconsole.dir(json);\n/* { passportId: 'h1rg9ugdd30o',\n firstName: 'Carolina',\n lastName: 'Gibson',\n _deleted: false,\n _attachments: { ... },\n _rev: '1-aklsdjfhaklsdjhf...'\n*/\n"})}),"\n",(0,a.jsx)(n.h3,{id:"tomutablejson",children:"toMutableJSON()"}),"\n",(0,a.jsxs)(n.p,{children:["Same as ",(0,a.jsx)(n.code,{children:"toJSON()"})," but returns a deep cloned object that can be mutated afterwards.\nRemember that deep cloning is performance expensive and should only be done when necessary."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const json = myDocument.toMutableJSON();\njson.firstName = 'Alice'; // The returned document can be mutated\n"})}),"\n",(0,a.jsx)(n.h2,{id:"notice-all-methods-of-rxdocument-are-bound-to-the-instance",children:"NOTICE: All methods of RxDocument are bound to the instance"}),"\n",(0,a.jsxs)(n.p,{children:["When you get a method from a ",(0,a.jsx)(n.code,{children:"RxDocument"}),", the method is automatically bound to the documents instance. This means you do not have to use things like ",(0,a.jsx)(n.code,{children:"myMethod.bind(myDocument)"})," like you would do in jsx."]}),"\n",(0,a.jsx)(n.h3,{id:"isrxdocument",children:"isRxDocument"}),"\n",(0,a.jsx)(n.p,{children:"Returns true if the given object is an instance of RxDocument. Returns false if not."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-js",children:"const is = isRxDocument(myObj);\n"})})]})}function u(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>l,x:()=>c});var a=t(6540);const o={},s=a.createContext(o);function l(e){const n=a.useContext(s);return a.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:l(e.components),a.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/e24529eb.552bcd35.js b/docs/assets/js/e24529eb.552bcd35.js deleted file mode 100644 index 2bfc58da7ee..00000000000 --- a/docs/assets/js/e24529eb.552bcd35.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6797],{9263:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>d,frontMatter:()=>n,metadata:()=>s,toc:()=>c});var o=a(4848),r=a(8453);const n={title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",slug:"rx-storage-localstorage-meta-optimizer.html"},i="RxStorage Localstorage Meta Optimizer",s={id:"rx-storage-localstorage-meta-optimizer",title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",description:"The RxStorage Localstorage Meta Optimizer is a wrapper around any other RxStorage. The wrapper uses the original RxStorage for normal collection documents. But to optimize the initial page load time, it uses localstorage to store the plain key-value metadata that RxDB needs to create databases and collections. This plugin can only be used in browsers.",source:"@site/docs/rx-storage-localstorage-meta-optimizer.md",sourceDirName:".",slug:"/rx-storage-localstorage-meta-optimizer.html",permalink:"/rx-storage-localstorage-meta-optimizer.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",slug:"rx-storage-localstorage-meta-optimizer.html"},sidebar:"tutorialSidebar",previous:{title:"Sharding RxStorage \ud83d\udc51",permalink:"/rx-storage-sharding.html"},next:{title:"Electron Plugin",permalink:"/electron.html"}},l={},c=[{value:"Usage",id:"usage",level:2}];function g(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"rxstorage-localstorage-meta-optimizer",children:"RxStorage Localstorage Meta Optimizer"}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"})," Localstorage Meta Optimizer is a wrapper around any other RxStorage. The wrapper uses the original RxStorage for normal collection documents. But to optimize the initial page load time, it uses ",(0,o.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage?retiredLocale=de",children:"localstorage"})," to store the plain key-value metadata that RxDB needs to create databases and collections. This plugin can only be used in browsers."]}),"\n",(0,o.jsx)(t.p,{children:"Depending on your database usage and the collection amount, this can save about 200 milliseconds on the initial pageload. It is recommended to use this when you create more then 4 RxCollections."}),"\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.strong,{children:"NOTICE:"})," This plugin is part of ",(0,o.jsx)(t.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]}),"\n",(0,o.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,o.jsx)(t.p,{children:"The meta optimizer gets wrapped around any other RxStorage. It will then automatically detect if an RxDB internal storage instance is created, and replace that with a localstorage based instance."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"import {\n getLocalstorageMetaOptimizerRxStorage\n} from 'rxdb-premium/plugins/storage-localstorage-meta-optimizer';\n\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n\n/**\n * First wrap the original RxStorage with the optimizer.\n */\nconst optimizedRxStorage = getLocalstorageMetaOptimizerRxStorage({\n\n /**\n * Here we use the dexie.js RxStorage,\n * it is also possible to use any other RxStorage instead.\n */\n storage: getRxStorageDexie()\n});\n\n/**\n * Create the RxDatabase with the wrapped RxStorage. \n */\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: optimizedRxStorage\n});\n\n"})})]})}function d(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(g,{...e})}):g(e)}},8453:(e,t,a)=>{a.d(t,{R:()=>i,x:()=>s});var o=a(6540);const r={},n=o.createContext(r);function i(e){const t=o.useContext(n);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),o.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/e24529eb.6c06ea72.js b/docs/assets/js/e24529eb.6c06ea72.js new file mode 100644 index 00000000000..76852b72348 --- /dev/null +++ b/docs/assets/js/e24529eb.6c06ea72.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6797],{9263:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>d,frontMatter:()=>n,metadata:()=>s,toc:()=>c});var o=a(4848),r=a(8453);const n={title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",slug:"rx-storage-localstorage-meta-optimizer.html"},i="RxStorage Localstorage Meta Optimizer",s={id:"rx-storage-localstorage-meta-optimizer",title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",description:"The RxStorage Localstorage Meta Optimizer is a wrapper around any other RxStorage. The wrapper uses the original RxStorage for normal collection documents. But to optimize the initial page load time, it uses localstorage to store the plain key-value metadata that RxDB needs to create databases and collections. This plugin can only be used in browsers.",source:"@site/docs/rx-storage-localstorage-meta-optimizer.md",sourceDirName:".",slug:"/rx-storage-localstorage-meta-optimizer.html",permalink:"/rx-storage-localstorage-meta-optimizer.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxStorage Localstorage Meta Optimizer \ud83d\udc51",slug:"rx-storage-localstorage-meta-optimizer.html"},sidebar:"tutorialSidebar",previous:{title:"Sharding RxStorage \ud83d\udc51",permalink:"/rx-storage-sharding.html"},next:{title:"Electron Plugin",permalink:"/electron.html"}},l={},c=[{value:"Usage",id:"usage",level:2}];function g(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"rxstorage-localstorage-meta-optimizer",children:"RxStorage Localstorage Meta Optimizer"}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.a,{href:"/rx-storage.html",children:"RxStorage"})," Localstorage Meta Optimizer is a wrapper around any other RxStorage. The wrapper uses the original RxStorage for normal collection documents. But to optimize the initial page load time, it uses ",(0,o.jsx)(t.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage?retiredLocale=de",children:"localstorage"})," to store the plain key-value metadata that RxDB needs to create databases and collections. This plugin can only be used in browsers."]}),"\n",(0,o.jsx)(t.p,{children:"Depending on your database usage and the collection amount, this can save about 200 milliseconds on the initial pageload. It is recommended to use this when you create more then 4 RxCollections."}),"\n",(0,o.jsx)(t.admonition,{title:"Premium",type:"note",children:(0,o.jsxs)(t.p,{children:["This plugin is part of ",(0,o.jsx)(t.a,{href:"/premium",children:"RxDB Premium \ud83d\udc51"}),". It is not part of the default RxDB module."]})}),"\n",(0,o.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,o.jsx)(t.p,{children:"The meta optimizer gets wrapped around any other RxStorage. It will then automatically detect if an RxDB internal storage instance is created, and replace that with a localstorage based instance."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-ts",children:"import {\n getLocalstorageMetaOptimizerRxStorage\n} from 'rxdb-premium/plugins/storage-localstorage-meta-optimizer';\n\nimport { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';\n\n\n/**\n * First wrap the original RxStorage with the optimizer.\n */\nconst optimizedRxStorage = getLocalstorageMetaOptimizerRxStorage({\n\n /**\n * Here we use the dexie.js RxStorage,\n * it is also possible to use any other RxStorage instead.\n */\n storage: getRxStorageDexie()\n});\n\n/**\n * Create the RxDatabase with the wrapped RxStorage. \n */\nconst database = await createRxDatabase({\n name: 'mydatabase',\n storage: optimizedRxStorage\n});\n\n"})})]})}function d(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(g,{...e})}):g(e)}},8453:(e,t,a)=>{a.d(t,{R:()=>i,x:()=>s});var o=a(6540);const r={},n=o.createContext(r);function i(e){const t=o.useContext(n);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),o.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/ebace26e.2909f1fb.js b/docs/assets/js/ebace26e.2909f1fb.js new file mode 100644 index 00000000000..3d5706ddd56 --- /dev/null +++ b/docs/assets/js/ebace26e.2909f1fb.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[4028],{7016:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>r,default:()=>c,frontMatter:()=>a,metadata:()=>i,toc:()=>h});var o=t(4848),s=t(8453);const a={title:"RxDB 10.0.0",slug:"10.0.0.html",description:"RxDB Major Release 10.0.0"},r="10.0.0",i={id:"releases/10.0.0",title:"RxDB 10.0.0",description:"RxDB Major Release 10.0.0",source:"@site/docs/releases/10.0.0.md",sourceDirName:"releases",slug:"/releases/10.0.0.html",permalink:"/releases/10.0.0.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDB 10.0.0",slug:"10.0.0.html",description:"RxDB Major Release 10.0.0"},sidebar:"tutorialSidebar",previous:{title:"RxDB 11.0.0",permalink:"/releases/11.0.0.html"},next:{title:"RxDB 9.0.0",permalink:"/releases/9.0.0.html"}},d={},h=[{value:"The main thing first",id:"the-main-thing-first",level:2},{value:"Other breaking changes",id:"other-breaking-changes",level:2},{value:"Primary key is required",id:"primary-key-is-required",level:3},{value:"Attachment data must be Blob or Buffer",id:"attachment-data-must-be-blob-or-buffer",level:3},{value:"Outgoing data is now readonly and deep-frozen",id:"outgoing-data-is-now-readonly-and-deep-frozen",level:3},{value:"The in-memory plugin does no longer work.",id:"the-in-memory-plugin-does-no-longer-work",level:3},{value:"What else is a breaking change?",id:"what-else-is-a-breaking-change",level:2},{value:"New features",id:"new-features",level:2},{value:"Composite primary key",id:"composite-primary-key",level:3},{value:"For the future",id:"for-the-future",level:2},{value:"You can help!",id:"you-can-help",level:2},{value:"Discuss!",id:"discuss",level:2}];function l(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"1000",children:"10.0.0"}),"\n",(0,o.jsxs)(n.p,{children:["One year after version ",(0,o.jsx)(n.code,{children:"9.0.0"})," we now have RxDB version ",(0,o.jsx)(n.code,{children:"10.0.0"}),".\nThe main goal of version 10 was to change things that make RxDB ready for the future."]}),"\n",(0,o.jsx)(n.p,{children:"Notice that I use major releases to bundle stuff that breaks the RxDB usage in your project, not to add new features."}),"\n",(0,o.jsx)(n.h2,{id:"the-main-thing-first",children:"The main thing first"}),"\n",(0,o.jsx)(n.p,{children:"In the past, RxDB was build around Pouchdb. Before I started making RxDB I tried to solve the problems of my current project with other existing databases out there. I evaluated all of them and then started using Pouchdb and added many features via plugin. Then I realised it will be easier to create a separate project that wraps around Pouchdb, that was RxDB. Back then pouchdb was the most major browser database out there and it was well maintained and had a big community.\nBut in the last 5 years, things have changed. A big part of the RxDB users do not use couchdb in the backend and do not need the couchdb replication.\nTherefore they do not really need the overhead with revision handling that slows down the performance of pouchdb. Also there where many other problems with using pouchdb. It is not actively developed, many bugs are not fixed and no new features get added. Also there are many unsolved problems like how to finally delete document data or how to replicate more then 6 databases at the same time, how to use replication without attachments data, and so on..."}),"\n",(0,o.jsxs)(n.p,{children:["So for this release, I abstracted all parts that we use from pouchdb into the ",(0,o.jsx)(n.code,{children:"RxStorage"})," interface. RxDB works on top of any implementation of the ",(0,o.jsx)(n.code,{children:"RxStorage"})," interface. This means it is now possible to use RxDB together with other underlying storages like SQLite, PostgreSQL, Minimongo, MongoDB, and so on, as long as someone writes the ",(0,o.jsx)(n.code,{children:"RxStorage"})," class for it."]}),"\n",(0,o.jsxs)(n.p,{children:["This means, to create a ",(0,o.jsx)(n.code,{children:"RxDatabase"})," you have to pass the storage class instead of pouchdb specific settings:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"\n// import pouchdb specific stuff and add pouchdb adapters\nimport {\n addPouchPlugin,\n getRxStoragePouch\n} from 'rxdb/plugins/pouchdb';\n\n// IMPORTANT: Do not use addRxPlugin to add pouchdb adapter, instead use addPouchPlugin\naddPouchPlugin(require('pouchdb-adapter-memory'));\n\nimport {\n addRxPlugin,\n createRxDatabase,\n randomCouchString,\n} from 'rxdb/plugins/core';\n\n// create the database with the storage creator.\nconst db = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('memory'),\n});\n\n"})}),"\n",(0,o.jsxs)(n.p,{children:["To access the internal ",(0,o.jsx)(n.code,{children:"pouch"})," instance of a collection, you have to go over the ",(0,o.jsx)(n.code,{children:"storageInstance"}),":"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"const pouch = myRxCollection.storageInstance.internals.pouch;\n"})}),"\n",(0,o.jsx)(n.h2,{id:"other-breaking-changes",children:"Other breaking changes"}),"\n",(0,o.jsx)(n.h3,{id:"primary-key-is-required",children:"Primary key is required"}),"\n",(0,o.jsxs)(n.p,{children:["In the past, using a primary key was optional. When no primary key was defined, RxDB filled up the ",(0,o.jsx)(n.code,{children:"_id"})," field with an uuid-like string which was then used as primary. When I researched on github how people use RxDB, I found out that many use a secondary index for what should be the primary key.\nAlso having the primary key optional, caused much confusing when using RxDB with typescript."]}),"\n",(0,o.jsxs)(n.p,{children:["So now the primary key MUST be set when creating a schema for RxDB.\nAlso the primary key is defined with the ",(0,o.jsx)(n.code,{children:"primaryKey"})," property at the top level of the schema. This ensures that typescript will complain if no ",(0,o.jsx)(n.code,{children:"primaryKey"})," is defined."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"\n// when using the type `RxJsonSchema` the `DocType` is now required\nconst mySchema: RxJsonSchema = {\n version: 0,\n primaryKey: 'passportId',\n type: 'object',\n properties: {\n passportId: {\n type: 'string'\n }\n },\n // primaryKey is always required\n required: ['passportId']\n}\n\n"})}),"\n",(0,o.jsx)(n.h3,{id:"attachment-data-must-be-blob-or-buffer",children:"Attachment data must be Blob or Buffer"}),"\n",(0,o.jsxs)(n.p,{children:["In the past, an ",(0,o.jsx)(n.code,{children:"RxAttachment"})," could be stored with ",(0,o.jsx)(n.code,{children:"Blob"}),", ",(0,o.jsx)(n.code,{children:"Buffer"})," and ",(0,o.jsx)(n.code,{children:"string"})," data. If a ",(0,o.jsx)(n.code,{children:"string"})," was passed, pouchdb internally transformed the data to a ",(0,o.jsx)(n.code,{children:"Blob"})," or ",(0,o.jsx)(n.code,{children:"Buffer"}),", depending on in which environment it is running.\nThis behavior caused much trouble and weird edge cases because of how the data is transformed from and to ",(0,o.jsx)(n.code,{children:"string"}),".\nSo now you can only store ",(0,o.jsx)(n.code,{children:"Blob"})," or ",(0,o.jsx)(n.code,{children:"Buffer"})," as attachment data. ",(0,o.jsx)(n.code,{children:"string"})," is no longer allowed. You can still transform a string to a Blob or Buffer by yourself and then store it."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"import { blobBufferUtil } from 'rxdb';\n\nconst attachment = await myDocument.putAttachment(\n {\n id: 'cat.txt',\n data: blobBufferUtil.createBlobBuffer('miau', 'text/plain')\n type: 'text/plain'\n }\n);\n\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Also ",(0,o.jsx)(n.code,{children:"putAttachment()"})," now defaults to ",(0,o.jsx)(n.code,{children:"skipIfSame=true"}),". This means when you write attachment data that already is exactly the same in the database, no write will be done."]}),"\n",(0,o.jsx)(n.h3,{id:"outgoing-data-is-now-readonly-and-deep-frozen",children:"Outgoing data is now readonly and deep-frozen"}),"\n",(0,o.jsx)(n.p,{children:"RxDB often uses outgoing data also in the internals. For example the result of a query is not only send to the user, but also used inside of RxDB's query-change-detection. To ensure that mutation of the outgoing data is not changing internal stuff, which would cause strange bugs, outgoing data was always deep-cloned before handing it out to the user. This is a common practice on many javascript libraries."}),"\n",(0,o.jsxs)(n.p,{children:["The problem is that deep-cloning big objects can be very CPU/Memory expensive. So instead of doing a deep-clone, RxDB does now assume that outgoing data is ",(0,o.jsx)(n.strong,{children:"immutable"}),". If the users wants to modify that data, it has be be deep-cloned by the user.\nTo ensure immutability, RxDB runs a ",(0,o.jsx)(n.a,{href:"https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze",children:"deep-freeze"})," in the dev-mode (about same expensive as deep clone). Also typescript will throw a build-time error because we use ",(0,o.jsx)(n.code,{children:"ReadonlyArray"})," and ",(0,o.jsx)(n.code,{children:"readonly"})," to define outgoing data immutable. In production-mode, there will be nothing besides typescript that ensures immutability to have best performance."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"const data = myRxDocument.toJSON();\ndata.foo = bar; // This does NOT work!\n\n// instead clone the data before changing it\nimport { clone } from 'rxjs';\nconst clonedData = clone(data);\ndata.foo = bar; // This works!\n"})}),"\n",(0,o.jsx)(n.h3,{id:"the-in-memory-plugin-does-no-longer-work",children:"The in-memory plugin does no longer work."}),"\n",(0,o.jsxs)(n.p,{children:["The in-memory plugin was used to spawn in-memory collections on top of a normal ",(0,o.jsx)(n.code,{children:"RxCollection"}),". The benefit is to have the data replicated into the memory of the javascript runtime, which allows for faster queries."]}),"\n",(0,o.jsxs)(n.p,{children:["After doing many tests and observations, I found out that the in-memory plugin was slow. Really slow, even slower then just using the indexeddb adapter in the browser. You can reproduce my observations at the event-reduce testpage. Here you can see that random-writes+query are slower on the ",(0,o.jsx)(n.a,{href:"https://pubkey.github.io/event-reduce/?tech=pouchdb:memory",children:"memory-adapter"})," then on ",(0,o.jsx)(n.a,{href:"https://pubkey.github.io/event-reduce/?tech=pouchdb:indexeddb",children:"indexeddb"}),".\nThe reason for this are the big abstraction layers. Pouchdb uses the adapter system. The memory adapter uses the ",(0,o.jsx)(n.a,{href:"https://github.com/Level/levelup",children:"leveldown abstraction layer"}),". Each write/read goes to the ",(0,o.jsx)(n.a,{href:"https://github.com/Level/memdown",children:"memdown module"}),"."]}),"\n",(0,o.jsxs)(n.p,{children:["So the in-memory plugin is not working for now. In the future it will be reimplemented in a custom memory based ",(0,o.jsx)(n.code,{children:"RxStorage"})," class."]}),"\n",(0,o.jsx)(n.admonition,{type:"note",children:(0,o.jsxs)(n.p,{children:["You can of course still use the pouchdb ",(0,o.jsx)(n.code,{children:"memory"})," adapter as usual. It is not affected by this change."]})}),"\n",(0,o.jsx)(n.h2,{id:"what-else-is-a-breaking-change",children:"What else is a breaking change?"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["Removed the deprecated ",(0,o.jsx)(n.code,{children:"atomicSet()"}),", use ",(0,o.jsx)(n.code,{children:"atomicPatch()"})," instead."]}),"\n",(0,o.jsxs)(n.li,{children:["Removed the deprecated ",(0,o.jsx)(n.code,{children:"RxDatabase.collection()"})," use ",(0,o.jsx)(n.code,{children:"RxDatabase().addCollections()"})," instead."]}),"\n",(0,o.jsxs)(n.li,{children:["Removed plugin hook ",(0,o.jsx)(n.code,{children:"preCreatePouchDb"})," because it is no longer needed."]}),"\n",(0,o.jsxs)(n.li,{children:["Removed the ",(0,o.jsx)(n.code,{children:"watch-for-changes"})," plugin. We now overwrite pouchdbs ",(0,o.jsx)(n.code,{children:"bulkDocs"})," method to generate events. This is faster and more reliable."]}),"\n",(0,o.jsxs)(n.li,{children:["Removed the ",(0,o.jsx)(n.code,{children:"adapter-check"})," plugin. (The function ",(0,o.jsx)(n.code,{children:"adapterCheck"})," is move to the pouchdb plugin)."]}),"\n",(0,o.jsxs)(n.li,{children:["Calling ",(0,o.jsx)(n.code,{children:"RxDatabase.server()"})," now returns a promise that resolves when the server is started up."]}),"\n",(0,o.jsxs)(n.li,{children:["Changed the defaults of ",(0,o.jsx)(n.code,{children:"PouchDBExpressServerOptions"})," from the ",(0,o.jsx)(n.code,{children:"server()"})," method, by default we now store logs in the ",(0,o.jsx)(n.code,{children:"tmp"})," folder and the config is in memory."]}),"\n",(0,o.jsxs)(n.li,{children:["Renamed ",(0,o.jsx)(n.code,{children:"replication"}),"-plugin to ",(0,o.jsx)(n.code,{children:"replication-couchdb"})," to be more consistent in naming like with ",(0,o.jsx)(n.code,{children:"replication-graphql"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["For the same reason, renamed ",(0,o.jsx)(n.code,{children:"RxCollection().sync()"})," to ",(0,o.jsx)(n.code,{children:"RxCollection().syncCouchDB()"})]}),"\n"]}),"\n"]}),"\n",(0,o.jsxs)(n.li,{children:["Renamed the functions of the json import/export plugin to be less confusing.","\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"dump()"})," is now ",(0,o.jsx)(n.code,{children:"exportJSON()"})]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"importDump()"})," is now ",(0,o.jsx)(n.code,{children:"importJSON()"})]}),"\n"]}),"\n"]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"RxCollection"})," uses a separate pouchdb instance for local documents, so that they can persist during migrations."]}),"\n",(0,o.jsxs)(n.li,{children:["A JsonSchema must have the ",(0,o.jsx)(n.code,{children:"required"})," array at the top level and it must contain the primary key."]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"new-features",children:"New features"}),"\n",(0,o.jsx)(n.h3,{id:"composite-primary-key",children:"Composite primary key"}),"\n",(0,o.jsx)(n.p,{children:"You can now use a composite primary key for the schema where you can join different properties of the document data to create a primary key."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"const mySchema = {\n keyCompression: true, // set this to true, to enable the keyCompression\n version: 0,\n title: 'human schema with composite primary',\n primaryKey: {\n // where should the composed string be stored\n key: 'id',\n // fields that will be used to create the composed key\n fields: [\n 'firstName',\n 'lastName'\n ],\n // separator which is used to concat the fields values.\n separator: '|'\n }\n type: 'object',\n properties: {\n id: {\n type: 'string'\n },\n firstName: {\n type: 'string'\n },\n lastName: {\n type: 'string'\n }\n },\n required: [\n 'id', \n 'firstName',\n 'lastName'\n ]\n};\n"})}),"\n",(0,o.jsx)(n.h2,{id:"for-the-future",children:"For the future"}),"\n",(0,o.jsx)(n.p,{children:"With these changes, RxDB is now ready for the future plans:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["I want to replace the ",(0,o.jsx)(n.code,{children:"revision"})," handling of documents with conflict resolution strategies that can always directly resolve conflicts instead of maintaining the revision tree."]}),"\n",(0,o.jsxs)(n.li,{children:["Implement different implementations for ",(0,o.jsx)(n.code,{children:"RxStorage"}),". I will first work on a memory based version. I am in good hope that the community will create other implementations depending on their needs."]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"you-can-help",children:"You can help!"}),"\n",(0,o.jsxs)(n.p,{children:["There are many things that can be done by ",(0,o.jsx)(n.strong,{children:"you"})," to improve RxDB:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["Check the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/blob/master/orga/BACKLOG.md",children:"BACKLOG"})," for features that would be great to have."]}),"\n",(0,o.jsxs)(n.li,{children:["Check the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/blob/master/orga/before-next-major.md",children:"breaking backlog"})," for breaking changes that must be implemented in the future but where I did not had the time yet."]}),"\n",(0,o.jsxs)(n.li,{children:["Check the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/search?q=TODO",children:"TODOs"})," in the code. There are many small improvements that can be done for performance and build size."]}),"\n",(0,o.jsx)(n.li,{children:"Review the code and add tests. I am only a single dude with a laptop. My code is not perfect and much small improvements can be done when people review the code and help me to clarify undefined behaviors."}),"\n",(0,o.jsx)(n.li,{children:"Improve the documentation. In the last user survey many users told me that the documentation is not good enough. But I reviewed the docs and could not find clear flaws. The problem is that I am way to deep into RxDB so that I am not able to understand which documentation a newcomer to the project needs. Likely I assume too much knowledge or focus writing about the wrong parts."}),"\n",(0,o.jsxs)(n.li,{children:["Update the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples",children:"example projects"})," many of them are outdated and need updates."]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"discuss",children:"Discuss!"}),"\n",(0,o.jsxs)(n.p,{children:["Please ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/issues/3279",children:"discuss here"}),"."]})]})}function c(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>i});var o=t(6540);const s={},a=o.createContext(s);function r(e){const n=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),o.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/ebace26e.83186a15.js b/docs/assets/js/ebace26e.83186a15.js deleted file mode 100644 index 3c6a01dc355..00000000000 --- a/docs/assets/js/ebace26e.83186a15.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[4028],{7016:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>c,frontMatter:()=>r,metadata:()=>i,toc:()=>h});var o=t(4848),s=t(8453);const r={title:"RxDB 10.0.0",slug:"10.0.0.html",description:"RxDB Major Release 10.0.0"},a="10.0.0",i={id:"releases/10.0.0",title:"RxDB 10.0.0",description:"RxDB Major Release 10.0.0",source:"@site/docs/releases/10.0.0.md",sourceDirName:"releases",slug:"/releases/10.0.0.html",permalink:"/releases/10.0.0.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxDB 10.0.0",slug:"10.0.0.html",description:"RxDB Major Release 10.0.0"},sidebar:"tutorialSidebar",previous:{title:"RxDB 11.0.0",permalink:"/releases/11.0.0.html"},next:{title:"RxDB 9.0.0",permalink:"/releases/9.0.0.html"}},d={},h=[{value:"The main thing first",id:"the-main-thing-first",level:2},{value:"Other breaking changes",id:"other-breaking-changes",level:2},{value:"Primary key is required",id:"primary-key-is-required",level:3},{value:"Attachment data must be Blob or Buffer",id:"attachment-data-must-be-blob-or-buffer",level:3},{value:"Outgoing data is now readonly and deep-frozen",id:"outgoing-data-is-now-readonly-and-deep-frozen",level:3},{value:"The in-memory plugin does no longer work.",id:"the-in-memory-plugin-does-no-longer-work",level:3},{value:"What else is a breaking change?",id:"what-else-is-a-breaking-change",level:2},{value:"New features",id:"new-features",level:2},{value:"Composite primary key",id:"composite-primary-key",level:3},{value:"For the future",id:"for-the-future",level:2},{value:"You can help!",id:"you-can-help",level:2},{value:"Discuss!",id:"discuss",level:2}];function l(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"1000",children:"10.0.0"}),"\n",(0,o.jsxs)(n.p,{children:["One year after version ",(0,o.jsx)(n.code,{children:"9.0.0"})," we now have RxDB version ",(0,o.jsx)(n.code,{children:"10.0.0"}),".\nThe main goal of version 10 was to change things that make RxDB ready for the future."]}),"\n",(0,o.jsx)(n.p,{children:"Notice that I use major releases to bundle stuff that breaks the RxDB usage in your project, not to add new features."}),"\n",(0,o.jsx)(n.h2,{id:"the-main-thing-first",children:"The main thing first"}),"\n",(0,o.jsx)(n.p,{children:"In the past, RxDB was build around Pouchdb. Before I started making RxDB I tried to solve the problems of my current project with other existing databases out there. I evaluated all of them and then started using Pouchdb and added many features via plugin. Then I realised it will be easier to create a separate project that wraps around Pouchdb, that was RxDB. Back then pouchdb was the most major browser database out there and it was well maintained and had a big community.\nBut in the last 5 years, things have changed. A big part of the RxDB users do not use couchdb in the backend and do not need the couchdb replication.\nTherefore they do not really need the overhead with revision handling that slows down the performance of pouchdb. Also there where many other problems with using pouchdb. It is not actively developed, many bugs are not fixed and no new features get added. Also there are many unsolved problems like how to finally delete document data or how to replicate more then 6 databases at the same time, how to use replication without attachments data, and so on..."}),"\n",(0,o.jsxs)(n.p,{children:["So for this release, I abstracted all parts that we use from pouchdb into the ",(0,o.jsx)(n.code,{children:"RxStorage"})," interface. RxDB works on top of any implementation of the ",(0,o.jsx)(n.code,{children:"RxStorage"})," interface. This means it is now possible to use RxDB together with other underlying storages like SQLite, PostgreSQL, Minimongo, MongoDB, and so on, as long as someone writes the ",(0,o.jsx)(n.code,{children:"RxStorage"})," class for it."]}),"\n",(0,o.jsxs)(n.p,{children:["This means, to create a ",(0,o.jsx)(n.code,{children:"RxDatabase"})," you have to pass the storage class instead of pouchdb specific settings:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"\n// import pouchdb specific stuff and add pouchdb adapters\nimport {\n addPouchPlugin,\n getRxStoragePouch\n} from 'rxdb/plugins/pouchdb';\n\n// IMPORTANT: Do not use addRxPlugin to add pouchdb adapter, instead use addPouchPlugin\naddPouchPlugin(require('pouchdb-adapter-memory'));\n\nimport {\n addRxPlugin,\n createRxDatabase,\n randomCouchString,\n} from 'rxdb/plugins/core';\n\n// create the database with the storage creator.\nconst db = await createRxDatabase({\n name: 'mydatabase',\n storage: getRxStoragePouch('memory'),\n});\n\n"})}),"\n",(0,o.jsxs)(n.p,{children:["To access the internal ",(0,o.jsx)(n.code,{children:"pouch"})," instance of a collection, you have to go over the ",(0,o.jsx)(n.code,{children:"storageInstance"}),":"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"const pouch = myRxCollection.storageInstance.internals.pouch;\n"})}),"\n",(0,o.jsx)(n.h2,{id:"other-breaking-changes",children:"Other breaking changes"}),"\n",(0,o.jsx)(n.h3,{id:"primary-key-is-required",children:"Primary key is required"}),"\n",(0,o.jsxs)(n.p,{children:["In the past, using a primary key was optional. When no primary key was defined, RxDB filled up the ",(0,o.jsx)(n.code,{children:"_id"})," field with an uuid-like string which was then used as primary. When I researched on github how people use RxDB, I found out that many use a secondary index for what should be the primary key.\nAlso having the primary key optional, caused much confusing when using RxDB with typescript."]}),"\n",(0,o.jsxs)(n.p,{children:["So now the primary key MUST be set when creating a schema for RxDB.\nAlso the primary key is defined with the ",(0,o.jsx)(n.code,{children:"primaryKey"})," property at the top level of the schema. This ensures that typescript will complain if no ",(0,o.jsx)(n.code,{children:"primaryKey"})," is defined."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"\n// when using the type `RxJsonSchema` the `DocType` is now required\nconst mySchema: RxJsonSchema = {\n version: 0,\n primaryKey: 'passportId',\n type: 'object',\n properties: {\n passportId: {\n type: 'string'\n }\n },\n // primaryKey is always required\n required: ['passportId']\n}\n\n"})}),"\n",(0,o.jsx)(n.h3,{id:"attachment-data-must-be-blob-or-buffer",children:"Attachment data must be Blob or Buffer"}),"\n",(0,o.jsxs)(n.p,{children:["In the past, an ",(0,o.jsx)(n.code,{children:"RxAttachment"})," could be stored with ",(0,o.jsx)(n.code,{children:"Blob"}),", ",(0,o.jsx)(n.code,{children:"Buffer"})," and ",(0,o.jsx)(n.code,{children:"string"})," data. If a ",(0,o.jsx)(n.code,{children:"string"})," was passed, pouchdb internally transformed the data to a ",(0,o.jsx)(n.code,{children:"Blob"})," or ",(0,o.jsx)(n.code,{children:"Buffer"}),", depending on in which environment it is running.\nThis behavior caused much trouble and weird edge cases because of how the data is transformed from and to ",(0,o.jsx)(n.code,{children:"string"}),".\nSo now you can only store ",(0,o.jsx)(n.code,{children:"Blob"})," or ",(0,o.jsx)(n.code,{children:"Buffer"})," as attachment data. ",(0,o.jsx)(n.code,{children:"string"})," is no longer allowed. You can still transform a string to a Blob or Buffer by yourself and then store it."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"import { blobBufferUtil } from 'rxdb';\n\nconst attachment = await myDocument.putAttachment(\n {\n id: 'cat.txt',\n data: blobBufferUtil.createBlobBuffer('miau', 'text/plain')\n type: 'text/plain'\n }\n);\n\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Also ",(0,o.jsx)(n.code,{children:"putAttachment()"})," now defaults to ",(0,o.jsx)(n.code,{children:"skipIfSame=true"}),". This means when you write attachment data that already is exactly the same in the database, no write will be done."]}),"\n",(0,o.jsx)(n.h3,{id:"outgoing-data-is-now-readonly-and-deep-frozen",children:"Outgoing data is now readonly and deep-frozen"}),"\n",(0,o.jsx)(n.p,{children:"RxDB often uses outgoing data also in the internals. For example the result of a query is not only send to the user, but also used inside of RxDB's query-change-detection. To ensure that mutation of the outgoing data is not changing internal stuff, which would cause strange bugs, outgoing data was always deep-cloned before handing it out to the user. This is a common practice on many javascript libraries."}),"\n",(0,o.jsxs)(n.p,{children:["The problem is that deep-cloning big objects can be very CPU/Memory expensive. So instead of doing a deep-clone, RxDB does now assume that outgoing data is ",(0,o.jsx)(n.strong,{children:"immutable"}),". If the users wants to modify that data, it has be be deep-cloned by the user.\nTo ensure immutability, RxDB runs a ",(0,o.jsx)(n.a,{href:"https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze",children:"deep-freeze"})," in the dev-mode (about same expensive as deep clone). Also typescript will throw a build-time error because we use ",(0,o.jsx)(n.code,{children:"ReadonlyArray"})," and ",(0,o.jsx)(n.code,{children:"readonly"})," to define outgoing data immutable. In production-mode, there will be nothing besides typescript that ensures immutability to have best performance."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",children:"const data = myRxDocument.toJSON();\ndata.foo = bar; // This does NOT work!\n\n// instead clone the data before changing it\nimport { clone } from 'rxjs';\nconst clonedData = clone(data);\ndata.foo = bar; // This works!\n"})}),"\n",(0,o.jsx)(n.h3,{id:"the-in-memory-plugin-does-no-longer-work",children:"The in-memory plugin does no longer work."}),"\n",(0,o.jsxs)(n.p,{children:["The in-memory plugin was used to spawn in-memory collections on top of a normal ",(0,o.jsx)(n.code,{children:"RxCollection"}),". The benefit is to have the data replicated into the memory of the javascript runtime, which allows for faster queries."]}),"\n",(0,o.jsxs)(n.p,{children:["After doing many tests and observations, I found out that the in-memory plugin was slow. Really slow, even slower then just using the indexeddb adapter in the browser. You can reproduce my observations at the event-reduce testpage. Here you can see that random-writes+query are slower on the ",(0,o.jsx)(n.a,{href:"https://pubkey.github.io/event-reduce/?tech=pouchdb:memory",children:"memory-adapter"})," then on ",(0,o.jsx)(n.a,{href:"https://pubkey.github.io/event-reduce/?tech=pouchdb:indexeddb",children:"indexeddb"}),".\nThe reason for this are the big abstraction layers. Pouchdb uses the adapter system. The memory adapter uses the ",(0,o.jsx)(n.a,{href:"https://github.com/Level/levelup",children:"leveldown abstraction layer"}),". Each write/read goes to the ",(0,o.jsx)(n.a,{href:"https://github.com/Level/memdown",children:"memdown module"}),"."]}),"\n",(0,o.jsxs)(n.p,{children:["So the in-memory plugin is not working for now. In the future it will be reimplemented in a custom memory based ",(0,o.jsx)(n.code,{children:"RxStorage"})," class."]}),"\n",(0,o.jsxs)(n.p,{children:["Notice: You can of course still use the pouchdb ",(0,o.jsx)(n.code,{children:"memory"})," adapter as usual. It is not affected by this change."]}),"\n",(0,o.jsx)(n.h2,{id:"what-else-is-a-breaking-change",children:"What else is a breaking change?"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["Removed the deprecated ",(0,o.jsx)(n.code,{children:"atomicSet()"}),", use ",(0,o.jsx)(n.code,{children:"atomicPatch()"})," instead."]}),"\n",(0,o.jsxs)(n.li,{children:["Removed the deprecated ",(0,o.jsx)(n.code,{children:"RxDatabase.collection()"})," use ",(0,o.jsx)(n.code,{children:"RxDatabase().addCollections()"})," instead."]}),"\n",(0,o.jsxs)(n.li,{children:["Removed plugin hook ",(0,o.jsx)(n.code,{children:"preCreatePouchDb"})," because it is no longer needed."]}),"\n",(0,o.jsxs)(n.li,{children:["Removed the ",(0,o.jsx)(n.code,{children:"watch-for-changes"})," plugin. We now overwrite pouchdbs ",(0,o.jsx)(n.code,{children:"bulkDocs"})," method to generate events. This is faster and more reliable."]}),"\n",(0,o.jsxs)(n.li,{children:["Removed the ",(0,o.jsx)(n.code,{children:"adapter-check"})," plugin. (The function ",(0,o.jsx)(n.code,{children:"adapterCheck"})," is move to the pouchdb plugin)."]}),"\n",(0,o.jsxs)(n.li,{children:["Calling ",(0,o.jsx)(n.code,{children:"RxDatabase.server()"})," now returns a promise that resolves when the server is started up."]}),"\n",(0,o.jsxs)(n.li,{children:["Changed the defaults of ",(0,o.jsx)(n.code,{children:"PouchDBExpressServerOptions"})," from the ",(0,o.jsx)(n.code,{children:"server()"})," method, by default we now store logs in the ",(0,o.jsx)(n.code,{children:"tmp"})," folder and the config is in memory."]}),"\n",(0,o.jsxs)(n.li,{children:["Renamed ",(0,o.jsx)(n.code,{children:"replication"}),"-plugin to ",(0,o.jsx)(n.code,{children:"replication-couchdb"})," to be more consistent in naming like with ",(0,o.jsx)(n.code,{children:"replication-graphql"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["For the same reason, renamed ",(0,o.jsx)(n.code,{children:"RxCollection().sync()"})," to ",(0,o.jsx)(n.code,{children:"RxCollection().syncCouchDB()"})]}),"\n"]}),"\n"]}),"\n",(0,o.jsxs)(n.li,{children:["Renamed the functions of the json import/export plugin to be less confusing.","\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"dump()"})," is now ",(0,o.jsx)(n.code,{children:"exportJSON()"})]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"importDump()"})," is now ",(0,o.jsx)(n.code,{children:"importJSON()"})]}),"\n"]}),"\n"]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"RxCollection"})," uses a separate pouchdb instance for local documents, so that they can persist during migrations."]}),"\n",(0,o.jsxs)(n.li,{children:["A JsonSchema must have the ",(0,o.jsx)(n.code,{children:"required"})," array at the top level and it must contain the primary key."]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"new-features",children:"New features"}),"\n",(0,o.jsx)(n.h3,{id:"composite-primary-key",children:"Composite primary key"}),"\n",(0,o.jsx)(n.p,{children:"You can now use a composite primary key for the schema where you can join different properties of the document data to create a primary key."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:"const mySchema = {\n keyCompression: true, // set this to true, to enable the keyCompression\n version: 0,\n title: 'human schema with composite primary',\n primaryKey: {\n // where should the composed string be stored\n key: 'id',\n // fields that will be used to create the composed key\n fields: [\n 'firstName',\n 'lastName'\n ],\n // separator which is used to concat the fields values.\n separator: '|'\n }\n type: 'object',\n properties: {\n id: {\n type: 'string'\n },\n firstName: {\n type: 'string'\n },\n lastName: {\n type: 'string'\n }\n },\n required: [\n 'id', \n 'firstName',\n 'lastName'\n ]\n};\n"})}),"\n",(0,o.jsx)(n.h2,{id:"for-the-future",children:"For the future"}),"\n",(0,o.jsx)(n.p,{children:"With these changes, RxDB is now ready for the future plans:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["I want to replace the ",(0,o.jsx)(n.code,{children:"revision"})," handling of documents with conflict resolution strategies that can always directly resolve conflicts instead of maintaining the revision tree."]}),"\n",(0,o.jsxs)(n.li,{children:["Implement different implementations for ",(0,o.jsx)(n.code,{children:"RxStorage"}),". I will first work on a memory based version. I am in good hope that the community will create other implementations depending on their needs."]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"you-can-help",children:"You can help!"}),"\n",(0,o.jsxs)(n.p,{children:["There are many things that can be done by ",(0,o.jsx)(n.strong,{children:"you"})," to improve RxDB:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["Check the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/blob/master/orga/BACKLOG.md",children:"BACKLOG"})," for features that would be great to have."]}),"\n",(0,o.jsxs)(n.li,{children:["Check the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/blob/master/orga/before-next-major.md",children:"breaking backlog"})," for breaking changes that must be implemented in the future but where I did not had the time yet."]}),"\n",(0,o.jsxs)(n.li,{children:["Check the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/search?q=TODO",children:"TODOs"})," in the code. There are many small improvements that can be done for performance and build size."]}),"\n",(0,o.jsx)(n.li,{children:"Review the code and add tests. I am only a single dude with a laptop. My code is not perfect and much small improvements can be done when people review the code and help me to clarify undefined behaviors."}),"\n",(0,o.jsx)(n.li,{children:"Improve the documentation. In the last user survey many users told me that the documentation is not good enough. But I reviewed the docs and could not find clear flaws. The problem is that I am way to deep into RxDB so that I am not able to understand which documentation a newcomer to the project needs. Likely I assume too much knowledge or focus writing about the wrong parts."}),"\n",(0,o.jsxs)(n.li,{children:["Update the ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples",children:"example projects"})," many of them are outdated and need updates."]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"discuss",children:"Discuss!"}),"\n",(0,o.jsxs)(n.p,{children:["Please ",(0,o.jsx)(n.a,{href:"https://github.com/pubkey/rxdb/issues/3279",children:"discuss here"}),"."]})]})}function c(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>i});var o=t(6540);const s={},r=o.createContext(s);function a(e){const n=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),o.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/ee1b9f21.07d6eced.js b/docs/assets/js/ee1b9f21.07d6eced.js deleted file mode 100644 index d51a4d587b6..00000000000 --- a/docs/assets/js/ee1b9f21.07d6eced.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6998],{3179:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>d,frontMatter:()=>n,metadata:()=>o,toc:()=>l});var i=a(4848),s=a(8453);const n={title:"React Native Database",slug:"react-native-database.html",description:"Explore React Native database solutions for cross-platform apps - AsyncStorage, SQLite, RxDB, and more. Tailored for iOS, Android, and Windows, ensuring seamless data storage and sync."},r="React Native Database",o={id:"react-native-database",title:"React Native Database",description:"Explore React Native database solutions for cross-platform apps - AsyncStorage, SQLite, RxDB, and more. Tailored for iOS, Android, and Windows, ensuring seamless data storage and sync.",source:"@site/docs/react-native-database.md",sourceDirName:".",slug:"/react-native-database.html",permalink:"/react-native-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"React Native Database",slug:"react-native-database.html",description:"Explore React Native database solutions for cross-platform apps - AsyncStorage, SQLite, RxDB, and more. Tailored for iOS, Android, and Windows, ensuring seamless data storage and sync."},sidebar:"tutorialSidebar",previous:{title:"Why NOSQL",permalink:"/why-nosql.html"},next:{title:"Alternatives for realtime offline-first JavaScript applications and local databases",permalink:"/alternatives.html"}},c={},l=[{value:"Database Solutions for React-Native",id:"database-solutions-for-react-native",level:2},{value:"AsyncStorage",id:"asyncstorage",level:3},{value:"SQLite",id:"sqlite",level:3},{value:"PouchDB",id:"pouchdb",level:3},{value:"RxDB",id:"rxdb",level:3},{value:"WatermelonDB",id:"watermelondb",level:3},{value:"Firebase / Firestore",id:"firebase--firestore",level:3},{value:"Follow up",id:"follow-up",level:2}];function h(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"react-native-database",children:"React Native Database"}),"\n",(0,i.jsx)(t.p,{children:"React Native provides a cross-platform JavaScript runtime that runs on different operating systems like Android, iOS, Windows and others. Mostly it is used to create hybrid Apps that run on mobile devices at Android (google) and iOS (apple)."}),"\n",(0,i.jsx)(t.p,{children:"In difference to the JavaScript runtime of browsers, React Native does not support all HTML5 APIs and so it is not possible to use browser storage possibilities like localstorage, cookies, WebSQL or IndexedDB.\nInstead a different storage solution must be chosen that does not come directly with React Native itself but has to be installed as a library or plugin."}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"NOTICE:"})," You are reading this inside of the ",(0,i.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," documentation, so everything might be opinionated."]}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/icons/react-native.png",alt:"React Native",width:"20"})}),"\n",(0,i.jsx)(t.h2,{id:"database-solutions-for-react-native",children:"Database Solutions for React-Native"}),"\n",(0,i.jsxs)(t.p,{children:["There are multiple database solutions that can be used with React Native. While I would recommend to use ",(0,i.jsx)(t.a,{href:"./",children:"RxDB"})," for most use cases, it is still helpful to learn about other alternatives."]}),"\n",(0,i.jsx)(t.h3,{id:"asyncstorage",children:"AsyncStorage"}),"\n",(0,i.jsxs)(t.p,{children:["AsyncStorage is a key->value storage solution that works similar to the browsers ",(0,i.jsx)(t.a,{href:"/articles/localstorage.html",children:"localstorage API"}),". The big difference is that access to the AsyncStorage is not a blocking operation but instead everything is ",(0,i.jsx)(t.code,{children:"Promise"})," based. This is a big benefit because long running writes and reads will not block your JavaScript process which would cause a laggy user interface."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"/**\n * Because it is Promise-based,\n * you have to 'await' the call to getItem()\n */\nawait setItem('myKey', 'myValue');\nconst value = await AsyncStorage.getItem('myKey');\n"})}),"\n",(0,i.jsxs)(t.p,{children:["AsyncStorage was originally included in ",(0,i.jsx)(t.a,{href:"https://reactnative.dev/docs/asyncstorage",children:"React Native itself"}),". But it was deprecated by the React Native Team which recommends to use a community based package instead. There is a ",(0,i.jsx)(t.a,{href:"https://github.com/react-native-async-storage/async-storage",children:"community fork of AsyncStorage"})," that is actively maintained and open source."]}),"\n",(0,i.jsx)(t.p,{children:"AsyncStorage is fine when only a small amount of data needs to be stored and when no query capabilities besides the key-access are required. Complex queries or features are not supported which makes AsyncStorage not suitable for anything more then storing simple user settings data."}),"\n",(0,i.jsx)(t.h3,{id:"sqlite",children:"SQLite"}),"\n",(0,i.jsx)(t.p,{children:"SQLite is a SQL based relational database written in C that was crafted to be embed inside of applications. Operations are written in the SQL query language and SQLite generally follows the PostgreSQL syntax."}),"\n",(0,i.jsxs)(t.p,{children:["To use SQLite in React Native, you first have to include the SQLite library itself as a plugin. There a different project out there that can be used, but I would recommend to use the ",(0,i.jsx)(t.a,{href:"https://github.com/ospfranco/react-native-quick-sqlite",children:"react-native-quick-sqlite"})," project."]}),"\n",(0,i.jsxs)(t.p,{children:["First you have to install the library into your React Native project via ",(0,i.jsx)(t.code,{children:"npm install react-native-quick-sqlite"}),".\nIn your code you can then import the library and create a database connection:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import {open} from 'react-native-quick-sqlite';\nconst db = open('myDb.sqlite');\n"})}),"\n",(0,i.jsx)(t.p,{children:"Notice that SQLite is a file based database where all data is stored directly in the filesystem of the OS. Therefore to create a connection, you have to provide a filename."}),"\n",(0,i.jsx)(t.p,{children:"With the open connection you can then run SQL queries:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"let { rows } = db.execute('SELECT somevalue FROM sometable');\n"})}),"\n",(0,i.jsxs)(t.p,{children:["If that does not work for you, you might want to try the ",(0,i.jsx)(t.a,{href:"https://github.com/andpor/react-native-sqlite-storage",children:"react-native-sqlite-storage"})," project instead which is also very popular."]}),"\n",(0,i.jsx)(t.p,{children:"The downside of SQLite is that it is lacking many features that are handful when using a database together with an UI based application. For example it is not possible to observe queries or document fields.\nAlso there is no replication method. This makes SQLite a good solution when you want to solely store data on the client, but not when you want to sync data with a server or other clients."}),"\n",(0,i.jsx)(t.h3,{id:"pouchdb",children:"PouchDB"}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/icons/pouchdb.png",alt:"PouchDB",width:"40"})}),"\n",(0,i.jsxs)(t.p,{children:["PouchDB is a JavaScript NoSQL database that follows the API of the ",(0,i.jsx)(t.a,{href:"https://couchdb.apache.org/",children:"Apache CouchDB"})," server database.\nThe core feature of PouchDB is the ability to do a two-way replication with any CouchDB compliant endpoint.\nWhile PouchDB is pretty mature, it has some drawbacks that blocks it from being used in a client-side React Native application. For example it has to store all documents states over time which is required to replicate with CouchDB. Also it is not easily possible to fully purge documents and so it will fill up disc space over time. A big problem is also that PouchDB is not really maintained and major bugs like wrong query results are not fixed anymore. The performance of PouchDB is a general bottleneck which is caused by how it has to store and fetch documents while being compliant to CouchDB. The only real reason to use PouchDB in React Native, is when you want to replicate with a CouchDB or Couchbase server."]}),"\n",(0,i.jsx)(t.p,{children:"Because PouchDB is based on an adapter system for storage, there are two options to use it with React Native:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["Either use the ",(0,i.jsx)(t.a,{href:"https://github.com/craftzdog/pouchdb-react-native",children:"pouchdb-adapter-react-native-sqlite"})," adapter"]}),"\n",(0,i.jsxs)(t.li,{children:["or the ",(0,i.jsx)(t.a,{href:"https://github.com/seigel/pouchdb-react-native",children:"pouchdb-adapter-asyncstorage"})," adapter."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["Because the ",(0,i.jsx)(t.code,{children:"asyncstorage"})," adapter is no longer maintained, it is recommended to use the ",(0,i.jsx)(t.code,{children:"native-sqlite"})," adapter:"]}),"\n",(0,i.jsxs)(t.p,{children:["First you have to install the adapter and other dependencies via ",(0,i.jsx)(t.code,{children:"npm install pouchdb-adapter-react-native-sqlite react-native-quick-sqlite react-native-quick-websql"}),"."]}),"\n",(0,i.jsx)(t.p,{children:"Then you have to craft a custom PouchDB class that combines these plugins:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import 'react-native-get-random-values';\nimport PouchDB from 'pouchdb-core';\nimport HttpPouch from 'pouchdb-adapter-http';\nimport replication from 'pouchdb-replication';\nimport mapreduce from 'pouchdb-mapreduce';\nimport SQLiteAdapterFactory from 'pouchdb-adapter-react-native-sqlite';\nimport WebSQLite from 'react-native-quick-websql';\n\nconst SQLiteAdapter = SQLiteAdapterFactory(WebSQLite);\nexport default PouchDB.plugin(HttpPouch)\n .plugin(replication)\n .plugin(mapreduce)\n .plugin(SQLiteAdapter);\n"})}),"\n",(0,i.jsx)(t.p,{children:"This can then be used to create a PouchDB database instance which can store and query documents:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"const db = new PouchDB('mydb.db', {\n adapter: 'react-native-sqlite'\n});\n"})}),"\n",(0,i.jsx)(t.h3,{id:"rxdb",children:"RxDB"}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/logo/rxdb_javascript_database.svg",alt:"RxDB",width:"170"})}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," is an local-first, NoSQL-database for JavaScript applications. It is reactive which means that you can not only query the current state, but subscribe to all state changes like the result of a query or even a single field of a document. This is great for UI-based realtime applications in a way that makes it easy to develop realtime applications like what you need in React Native."]}),"\n",(0,i.jsx)(t.p,{children:"There are multiple ways to use RxDB in React Native:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["Use the ",(0,i.jsx)(t.a,{href:"/rx-storage-memory.html",children:"memory RxStorage"})," that stores the data inside of the JavaScript memory without persistence"]}),"\n",(0,i.jsxs)(t.li,{children:["Use the ",(0,i.jsx)(t.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"})," with the ",(0,i.jsx)(t.a,{href:"https://github.com/cawfree/react-native-lokijs",children:"react-native-lokijs"})," plugin"]}),"\n",(0,i.jsxs)(t.li,{children:["Use the ",(0,i.jsx)(t.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"})," with the ",(0,i.jsx)(t.a,{href:"https://github.com/ospfranco/react-native-quick-sqlite",children:"react-native-quick-sqlite"})," plugin."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["It is recommended to use the ",(0,i.jsx)(t.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"})," because it has the best performance and is the easiest to set up. However it is part of the ",(0,i.jsx)(t.a,{href:"/premium",children:"\ud83d\udc51 Premium Plugins"})," which must be purchased, so to try out RxDB with React Native, you might want to use one of the other three options."]}),"\n",(0,i.jsxs)(t.p,{children:["First you have to install all dependencies via ",(0,i.jsx)(t.code,{children:"npm install rxdb rxjs rxdb-premium react-native-quick-sqlite"}),".\nThen you can assemble the RxStorage and create a database with it:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport {\n getRxStorageSQLite,\n getSQLiteBasicsQuickSQLite\n} from 'rxdb-premium/plugins/storage-sqlite';\nimport { open } from 'react-native-quick-sqlite';\n\n// create database\nconst myRxDatabase = await createRxDatabase({\n // Instead of a simple name, you can use a folder path to determine the database location \n name: 'exampledb',\n multiInstance: false, // <- Set this to false when using RxDB in React Native\n storage: getRxStorageSQLite({\n sqliteBasics: getSQLiteBasicsQuickSQLite(open)\n })\n});\n\n// create collections\nconst collections = await myRxDatabase.addCollections({\n humans: {\n /* ... */\n }\n});\n\n// insert document\nawait collections.humans.insert({id: 'foo', name: 'bar'});\n\n// run a query\nconst result = await collections.humans.find({\n selector: {\n name: 'bar'\n }\n}).exec();\n\n// observe a query\nawait collections.humans.find({\n selector: {\n name: 'bar'\n }\n}).$.subscribe(result => {/* ... */});\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Using the SQLite RxStorage is pretty fast, which is shown in the ",(0,i.jsx)(t.a,{href:"/rx-storage.html#performance-comparison",children:"performance comparison"}),".\nTo learn more about using RxDB with React Native, you might want to check out ",(0,i.jsx)(t.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/react-native",children:"this example project"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["Also RxDB provides many other features like ",(0,i.jsx)(t.a,{href:"/encryption.html",children:"encryption"})," or ",(0,i.jsx)(t.a,{href:"/key-compression.html",children:"compression"}),". You can even store binary data as ",(0,i.jsx)(t.a,{href:"/rx-attachment.html",children:"attachments"})," or use RxDB as an ORM in React Native."]}),"\n",(0,i.jsx)(t.h3,{id:"watermelondb",children:"WatermelonDB"}),"\n",(0,i.jsx)(t.p,{children:"WatermelonDB is a reactive and asynchronous database for React and React Native apps. It is based on SQLite in React Native and LokiJS when it is used in the browser. It supports schemas, observable queries, migrations and relations.\nThe schema layout is handled by TypeScript decorators and looks like this:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"class Post extends Model {\n @field('name') name;\n @field('body') body;\n @children('comments') comments;\n\n // a relation to another table\n @relation('users', 'author_id') author;\n}\n"})}),"\n",(0,i.jsxs)(t.p,{children:["WatermelonDB also supports replication but the sync protocol is pretty complex because on how it resolves conflicts. I recommend to watch ",(0,i.jsx)(t.a,{href:"https://www.youtube.com/watch?v=uFvHURTRLxQ",children:"this video"})," to learn how the replication works."]}),"\n",(0,i.jsxs)(t.p,{children:["According to the roadmap, despite being essentially feature-complete, WatermelonDB is still on the ",(0,i.jsx)(t.code,{children:"0.xx"})," version and intends to switch to a ",(0,i.jsx)(t.code,{children:"1.x.x"})," version as once it reaches a long-term stable API."]}),"\n",(0,i.jsx)(t.h3,{id:"firebase--firestore",children:"Firebase / Firestore"}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/alternatives/firebase.svg",alt:"Firebase",width:"80"})}),"\n",(0,i.jsx)(t.p,{children:"Firestore is a cloud based database technology that stores data on clients devices and replicates it with the Firebase cloud service that is run by google. It has many features like observability and authentication.\nThe main feature lacking is the non-complete offline first support because clients cannot start the application while being offline because then the authentication does not work. After they are authenticated, being offline is no longer a problem.\nAlso using firestore creates a vendor lock-in because it is not possible to replicate with a custom self hosted backend."}),"\n",(0,i.jsxs)(t.p,{children:["To get started with Firestore in React Native, it is recommended to use the ",(0,i.jsx)(t.a,{href:"https://github.com/invertase/react-native-firebase",children:"React Native Firebase"})," open-source project."]}),"\n",(0,i.jsx)(t.h2,{id:"follow-up",children:"Follow up"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["A good way to learn using RxDB database with React Native is to check out the ",(0,i.jsx)(t.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/react-native",children:"RxDB React Native example"})," and use that as a tutorial."]}),"\n",(0,i.jsxs)(t.li,{children:["If you haven't done so yet, you should start learning about RxDB with the ",(0,i.jsx)(t.a,{href:"/quickstart.html",children:"Quickstart Tutorial"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:["There is a followup list of other ",(0,i.jsx)(t.a,{href:"/alternatives.html",children:"client side database alternatives"})," that might work with React Native."]}),"\n"]})]})}function d(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},8453:(e,t,a)=>{a.d(t,{R:()=>r,x:()=>o});var i=a(6540);const s={},n=i.createContext(s);function r(e){const t=i.useContext(n);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/ee1b9f21.b9abbce3.js b/docs/assets/js/ee1b9f21.b9abbce3.js new file mode 100644 index 00000000000..e8153af3ac8 --- /dev/null +++ b/docs/assets/js/ee1b9f21.b9abbce3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[6998],{3179:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>d,frontMatter:()=>n,metadata:()=>r,toc:()=>l});var i=a(4848),s=a(8453);const n={title:"React Native Database",slug:"react-native-database.html",description:"Explore React Native database solutions for cross-platform apps - AsyncStorage, SQLite, RxDB, and more. Tailored for iOS, Android, and Windows, ensuring seamless data storage and sync."},o="React Native Database",r={id:"react-native-database",title:"React Native Database",description:"Explore React Native database solutions for cross-platform apps - AsyncStorage, SQLite, RxDB, and more. Tailored for iOS, Android, and Windows, ensuring seamless data storage and sync.",source:"@site/docs/react-native-database.md",sourceDirName:".",slug:"/react-native-database.html",permalink:"/react-native-database.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"React Native Database",slug:"react-native-database.html",description:"Explore React Native database solutions for cross-platform apps - AsyncStorage, SQLite, RxDB, and more. Tailored for iOS, Android, and Windows, ensuring seamless data storage and sync."},sidebar:"tutorialSidebar",previous:{title:"Why NOSQL",permalink:"/why-nosql.html"},next:{title:"Alternatives for realtime offline-first JavaScript applications and local databases",permalink:"/alternatives.html"}},c={},l=[{value:"Database Solutions for React-Native",id:"database-solutions-for-react-native",level:2},{value:"AsyncStorage",id:"asyncstorage",level:3},{value:"SQLite",id:"sqlite",level:3},{value:"PouchDB",id:"pouchdb",level:3},{value:"RxDB",id:"rxdb",level:3},{value:"WatermelonDB",id:"watermelondb",level:3},{value:"Firebase / Firestore",id:"firebase--firestore",level:3},{value:"Follow up",id:"follow-up",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"react-native-database",children:"React Native Database"}),"\n",(0,i.jsx)(t.p,{children:"React Native provides a cross-platform JavaScript runtime that runs on different operating systems like Android, iOS, Windows and others. Mostly it is used to create hybrid Apps that run on mobile devices at Android (google) and iOS (apple)."}),"\n",(0,i.jsx)(t.p,{children:"In difference to the JavaScript runtime of browsers, React Native does not support all HTML5 APIs and so it is not possible to use browser storage possibilities like localstorage, cookies, WebSQL or IndexedDB.\nInstead a different storage solution must be chosen that does not come directly with React Native itself but has to be installed as a library or plugin."}),"\n",(0,i.jsx)(t.admonition,{type:"note",children:(0,i.jsxs)(t.p,{children:["You are reading this inside of the ",(0,i.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," documentation, so everything might be opinionated."]})}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/icons/react-native.png",alt:"React Native",width:"20"})}),"\n",(0,i.jsx)(t.h2,{id:"database-solutions-for-react-native",children:"Database Solutions for React-Native"}),"\n",(0,i.jsxs)(t.p,{children:["There are multiple database solutions that can be used with React Native. While I would recommend to use ",(0,i.jsx)(t.a,{href:"./",children:"RxDB"})," for most use cases, it is still helpful to learn about other alternatives."]}),"\n",(0,i.jsx)(t.h3,{id:"asyncstorage",children:"AsyncStorage"}),"\n",(0,i.jsxs)(t.p,{children:["AsyncStorage is a key->value storage solution that works similar to the browsers ",(0,i.jsx)(t.a,{href:"/articles/localstorage.html",children:"localstorage API"}),". The big difference is that access to the AsyncStorage is not a blocking operation but instead everything is ",(0,i.jsx)(t.code,{children:"Promise"})," based. This is a big benefit because long running writes and reads will not block your JavaScript process which would cause a laggy user interface."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"/**\n * Because it is Promise-based,\n * you have to 'await' the call to getItem()\n */\nawait setItem('myKey', 'myValue');\nconst value = await AsyncStorage.getItem('myKey');\n"})}),"\n",(0,i.jsxs)(t.p,{children:["AsyncStorage was originally included in ",(0,i.jsx)(t.a,{href:"https://reactnative.dev/docs/asyncstorage",children:"React Native itself"}),". But it was deprecated by the React Native Team which recommends to use a community based package instead. There is a ",(0,i.jsx)(t.a,{href:"https://github.com/react-native-async-storage/async-storage",children:"community fork of AsyncStorage"})," that is actively maintained and open source."]}),"\n",(0,i.jsx)(t.p,{children:"AsyncStorage is fine when only a small amount of data needs to be stored and when no query capabilities besides the key-access are required. Complex queries or features are not supported which makes AsyncStorage not suitable for anything more then storing simple user settings data."}),"\n",(0,i.jsx)(t.h3,{id:"sqlite",children:"SQLite"}),"\n",(0,i.jsx)(t.p,{children:"SQLite is a SQL based relational database written in C that was crafted to be embed inside of applications. Operations are written in the SQL query language and SQLite generally follows the PostgreSQL syntax."}),"\n",(0,i.jsxs)(t.p,{children:["To use SQLite in React Native, you first have to include the SQLite library itself as a plugin. There a different project out there that can be used, but I would recommend to use the ",(0,i.jsx)(t.a,{href:"https://github.com/ospfranco/react-native-quick-sqlite",children:"react-native-quick-sqlite"})," project."]}),"\n",(0,i.jsxs)(t.p,{children:["First you have to install the library into your React Native project via ",(0,i.jsx)(t.code,{children:"npm install react-native-quick-sqlite"}),".\nIn your code you can then import the library and create a database connection:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import {open} from 'react-native-quick-sqlite';\nconst db = open('myDb.sqlite');\n"})}),"\n",(0,i.jsx)(t.p,{children:"Notice that SQLite is a file based database where all data is stored directly in the filesystem of the OS. Therefore to create a connection, you have to provide a filename."}),"\n",(0,i.jsx)(t.p,{children:"With the open connection you can then run SQL queries:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"let { rows } = db.execute('SELECT somevalue FROM sometable');\n"})}),"\n",(0,i.jsxs)(t.p,{children:["If that does not work for you, you might want to try the ",(0,i.jsx)(t.a,{href:"https://github.com/andpor/react-native-sqlite-storage",children:"react-native-sqlite-storage"})," project instead which is also very popular."]}),"\n",(0,i.jsx)(t.p,{children:"The downside of SQLite is that it is lacking many features that are handful when using a database together with an UI based application. For example it is not possible to observe queries or document fields.\nAlso there is no replication method. This makes SQLite a good solution when you want to solely store data on the client, but not when you want to sync data with a server or other clients."}),"\n",(0,i.jsx)(t.h3,{id:"pouchdb",children:"PouchDB"}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/icons/pouchdb.png",alt:"PouchDB",width:"40"})}),"\n",(0,i.jsxs)(t.p,{children:["PouchDB is a JavaScript NoSQL database that follows the API of the ",(0,i.jsx)(t.a,{href:"https://couchdb.apache.org/",children:"Apache CouchDB"})," server database.\nThe core feature of PouchDB is the ability to do a two-way replication with any CouchDB compliant endpoint.\nWhile PouchDB is pretty mature, it has some drawbacks that blocks it from being used in a client-side React Native application. For example it has to store all documents states over time which is required to replicate with CouchDB. Also it is not easily possible to fully purge documents and so it will fill up disc space over time. A big problem is also that PouchDB is not really maintained and major bugs like wrong query results are not fixed anymore. The performance of PouchDB is a general bottleneck which is caused by how it has to store and fetch documents while being compliant to CouchDB. The only real reason to use PouchDB in React Native, is when you want to replicate with a CouchDB or Couchbase server."]}),"\n",(0,i.jsx)(t.p,{children:"Because PouchDB is based on an adapter system for storage, there are two options to use it with React Native:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["Either use the ",(0,i.jsx)(t.a,{href:"https://github.com/craftzdog/pouchdb-react-native",children:"pouchdb-adapter-react-native-sqlite"})," adapter"]}),"\n",(0,i.jsxs)(t.li,{children:["or the ",(0,i.jsx)(t.a,{href:"https://github.com/seigel/pouchdb-react-native",children:"pouchdb-adapter-asyncstorage"})," adapter."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["Because the ",(0,i.jsx)(t.code,{children:"asyncstorage"})," adapter is no longer maintained, it is recommended to use the ",(0,i.jsx)(t.code,{children:"native-sqlite"})," adapter:"]}),"\n",(0,i.jsxs)(t.p,{children:["First you have to install the adapter and other dependencies via ",(0,i.jsx)(t.code,{children:"npm install pouchdb-adapter-react-native-sqlite react-native-quick-sqlite react-native-quick-websql"}),"."]}),"\n",(0,i.jsx)(t.p,{children:"Then you have to craft a custom PouchDB class that combines these plugins:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import 'react-native-get-random-values';\nimport PouchDB from 'pouchdb-core';\nimport HttpPouch from 'pouchdb-adapter-http';\nimport replication from 'pouchdb-replication';\nimport mapreduce from 'pouchdb-mapreduce';\nimport SQLiteAdapterFactory from 'pouchdb-adapter-react-native-sqlite';\nimport WebSQLite from 'react-native-quick-websql';\n\nconst SQLiteAdapter = SQLiteAdapterFactory(WebSQLite);\nexport default PouchDB.plugin(HttpPouch)\n .plugin(replication)\n .plugin(mapreduce)\n .plugin(SQLiteAdapter);\n"})}),"\n",(0,i.jsx)(t.p,{children:"This can then be used to create a PouchDB database instance which can store and query documents:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"const db = new PouchDB('mydb.db', {\n adapter: 'react-native-sqlite'\n});\n"})}),"\n",(0,i.jsx)(t.h3,{id:"rxdb",children:"RxDB"}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/logo/rxdb_javascript_database.svg",alt:"RxDB",width:"170"})}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.a,{href:"https://rxdb.info/",children:"RxDB"})," is an local-first, NoSQL-database for JavaScript applications. It is reactive which means that you can not only query the current state, but subscribe to all state changes like the result of a query or even a single field of a document. This is great for UI-based realtime applications in a way that makes it easy to develop realtime applications like what you need in React Native."]}),"\n",(0,i.jsx)(t.p,{children:"There are multiple ways to use RxDB in React Native:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["Use the ",(0,i.jsx)(t.a,{href:"/rx-storage-memory.html",children:"memory RxStorage"})," that stores the data inside of the JavaScript memory without persistence"]}),"\n",(0,i.jsxs)(t.li,{children:["Use the ",(0,i.jsx)(t.a,{href:"/rx-storage-lokijs.html",children:"LokiJS RxStorage"})," with the ",(0,i.jsx)(t.a,{href:"https://github.com/cawfree/react-native-lokijs",children:"react-native-lokijs"})," plugin"]}),"\n",(0,i.jsxs)(t.li,{children:["Use the ",(0,i.jsx)(t.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"})," with the ",(0,i.jsx)(t.a,{href:"https://github.com/ospfranco/react-native-quick-sqlite",children:"react-native-quick-sqlite"})," plugin."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["It is recommended to use the ",(0,i.jsx)(t.a,{href:"/rx-storage-sqlite.html",children:"SQLite RxStorage"})," because it has the best performance and is the easiest to set up. However it is part of the ",(0,i.jsx)(t.a,{href:"/premium",children:"\ud83d\udc51 Premium Plugins"})," which must be purchased, so to try out RxDB with React Native, you might want to use one of the other three options."]}),"\n",(0,i.jsxs)(t.p,{children:["First you have to install all dependencies via ",(0,i.jsx)(t.code,{children:"npm install rxdb rxjs rxdb-premium react-native-quick-sqlite"}),".\nThen you can assemble the RxStorage and create a database with it:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"import {\n createRxDatabase\n} from 'rxdb';\nimport {\n getRxStorageSQLite,\n getSQLiteBasicsQuickSQLite\n} from 'rxdb-premium/plugins/storage-sqlite';\nimport { open } from 'react-native-quick-sqlite';\n\n// create database\nconst myRxDatabase = await createRxDatabase({\n // Instead of a simple name, you can use a folder path to determine the database location \n name: 'exampledb',\n multiInstance: false, // <- Set this to false when using RxDB in React Native\n storage: getRxStorageSQLite({\n sqliteBasics: getSQLiteBasicsQuickSQLite(open)\n })\n});\n\n// create collections\nconst collections = await myRxDatabase.addCollections({\n humans: {\n /* ... */\n }\n});\n\n// insert document\nawait collections.humans.insert({id: 'foo', name: 'bar'});\n\n// run a query\nconst result = await collections.humans.find({\n selector: {\n name: 'bar'\n }\n}).exec();\n\n// observe a query\nawait collections.humans.find({\n selector: {\n name: 'bar'\n }\n}).$.subscribe(result => {/* ... */});\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Using the SQLite RxStorage is pretty fast, which is shown in the ",(0,i.jsx)(t.a,{href:"/rx-storage.html#performance-comparison",children:"performance comparison"}),".\nTo learn more about using RxDB with React Native, you might want to check out ",(0,i.jsx)(t.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/react-native",children:"this example project"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["Also RxDB provides many other features like ",(0,i.jsx)(t.a,{href:"/encryption.html",children:"encryption"})," or ",(0,i.jsx)(t.a,{href:"/key-compression.html",children:"compression"}),". You can even store binary data as ",(0,i.jsx)(t.a,{href:"/rx-attachment.html",children:"attachments"})," or use RxDB as an ORM in React Native."]}),"\n",(0,i.jsx)(t.h3,{id:"watermelondb",children:"WatermelonDB"}),"\n",(0,i.jsx)(t.p,{children:"WatermelonDB is a reactive and asynchronous database for React and React Native apps. It is based on SQLite in React Native and LokiJS when it is used in the browser. It supports schemas, observable queries, migrations and relations.\nThe schema layout is handled by TypeScript decorators and looks like this:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-ts",children:"class Post extends Model {\n @field('name') name;\n @field('body') body;\n @children('comments') comments;\n\n // a relation to another table\n @relation('users', 'author_id') author;\n}\n"})}),"\n",(0,i.jsxs)(t.p,{children:["WatermelonDB also supports replication but the sync protocol is pretty complex because on how it resolves conflicts. I recommend to watch ",(0,i.jsx)(t.a,{href:"https://www.youtube.com/watch?v=uFvHURTRLxQ",children:"this video"})," to learn how the replication works."]}),"\n",(0,i.jsxs)(t.p,{children:["According to the roadmap, despite being essentially feature-complete, WatermelonDB is still on the ",(0,i.jsx)(t.code,{children:"0.xx"})," version and intends to switch to a ",(0,i.jsx)(t.code,{children:"1.x.x"})," version as once it reaches a long-term stable API."]}),"\n",(0,i.jsx)(t.h3,{id:"firebase--firestore",children:"Firebase / Firestore"}),"\n",(0,i.jsx)("p",{align:"center",children:(0,i.jsx)("img",{src:"./files/alternatives/firebase.svg",alt:"Firebase",width:"80"})}),"\n",(0,i.jsx)(t.p,{children:"Firestore is a cloud based database technology that stores data on clients devices and replicates it with the Firebase cloud service that is run by google. It has many features like observability and authentication.\nThe main feature lacking is the non-complete offline first support because clients cannot start the application while being offline because then the authentication does not work. After they are authenticated, being offline is no longer a problem.\nAlso using firestore creates a vendor lock-in because it is not possible to replicate with a custom self hosted backend."}),"\n",(0,i.jsxs)(t.p,{children:["To get started with Firestore in React Native, it is recommended to use the ",(0,i.jsx)(t.a,{href:"https://github.com/invertase/react-native-firebase",children:"React Native Firebase"})," open-source project."]}),"\n",(0,i.jsx)(t.h2,{id:"follow-up",children:"Follow up"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["A good way to learn using RxDB database with React Native is to check out the ",(0,i.jsx)(t.a,{href:"https://github.com/pubkey/rxdb/tree/master/examples/react-native",children:"RxDB React Native example"})," and use that as a tutorial."]}),"\n",(0,i.jsxs)(t.li,{children:["If you haven't done so yet, you should start learning about RxDB with the ",(0,i.jsx)(t.a,{href:"/quickstart.html",children:"Quickstart Tutorial"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:["There is a followup list of other ",(0,i.jsx)(t.a,{href:"/alternatives.html",children:"client side database alternatives"})," that might work with React Native."]}),"\n"]})]})}function d(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},8453:(e,t,a)=>{a.d(t,{R:()=>o,x:()=>r});var i=a(6540);const s={},n=i.createContext(s);function o(e){const t=i.useContext(n);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),i.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/f43e80a8.289f2a88.js b/docs/assets/js/f43e80a8.289f2a88.js deleted file mode 100644 index 2a0b800cdda..00000000000 --- a/docs/assets/js/f43e80a8.289f2a88.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[1558],{9544:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var r=t(4848),i=t(8453);const s={title:"RxServer",slug:"rx-server.html"},a="RxDB Server",o={id:"rx-server",title:"RxServer",description:'The RxDB Server Plugin makes it possible to spawn a server on top of a RxDB database that offers multiple types of endpoints for various usages. It can spawn basic CRUD REST endpoints or even realtime replication endpoints that can be used by the client devices to replicate data. The RxServer plugin is designed to be used in Node.js but you can also use it in Deno, Bun or the Electron "main" process. You can use it either as a standalone server or add it on top of an existing http server (like express) in nodejs.',source:"@site/docs/rx-server.md",sourceDirName:".",slug:"/rx-server.html",permalink:"/rx-server.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxServer",slug:"rx-server.html"},sidebar:"tutorialSidebar",previous:{title:"NATS Replication",permalink:"/replication-nats.html"},next:{title:"RxServer Scaling",permalink:"/rx-server-scaling.html"}},l={},d=[{value:"Starting a RxServer",id:"starting-a-rxserver",level:2},{value:"RxServer Endpoints",id:"rxserver-endpoints",level:2},{value:"Replication Endpoint",id:"replication-endpoint",level:2},{value:"REST endpoint",id:"rest-endpoint",level:2},{value:"CORS",id:"cors",level:2},{value:"Auth handler",id:"auth-handler",level:2},{value:"Query modifier",id:"query-modifier",level:2},{value:"Change validator",id:"change-validator",level:2},{value:"Server-only fields",id:"server-only-fields",level:2},{value:"Readonly fields",id:"readonly-fields",level:2},{value:"$regex queries not allowed",id:"regex-queries-not-allowed",level:2},{value:"Conflict handling",id:"conflict-handling",level:2},{value:"Missing features",id:"missing-features",level:2},{value:"FAQ",id:"faq",level:2}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"rxdb-server",children:"RxDB Server"}),"\n",(0,r.jsxs)(n.p,{children:['The RxDB Server Plugin makes it possible to spawn a server on top of a RxDB database that offers multiple types of endpoints for various usages. It can spawn basic CRUD REST endpoints or even realtime replication endpoints that can be used by the client devices to replicate data. The RxServer plugin is designed to be used in Node.js but you can also use it in Deno, Bun or the Electron "main" process. You can use it either as a ',(0,r.jsx)(n.strong,{children:"standalone server"})," or add it on top of an ",(0,r.jsx)(n.strong,{children:"existing http server"})," (like express) in nodejs."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"NOTICE:"})," The server plugin is in beta mode and the API might be changed without a major RxDB release."]}),"\n",(0,r.jsx)(n.h2,{id:"starting-a-rxserver",children:"Starting a RxServer"}),"\n",(0,r.jsxs)(n.p,{children:["To create an ",(0,r.jsx)(n.code,{children:"RxServer"}),", you have to install the ",(0,r.jsx)(n.code,{children:"rxdb-server"})," package with ",(0,r.jsx)(n.code,{children:"npm install rxdb-server --save"})," and then you can import the ",(0,r.jsx)(n.code,{children:"startRxServer()"})," function and create a server on a given ",(0,r.jsx)(n.a,{href:"/rx-database.html",children:"RxDatabase"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"import { startRxServer } from 'rxdb-server/plugins/server';\n\nconst myServer = await startRxServer({\n database: myRxDatabase,\n port: 443\n});\n"})}),"\n",(0,r.jsx)(n.h2,{id:"rxserver-endpoints",children:"RxServer Endpoints"}),"\n",(0,r.jsxs)(n.p,{children:["On top of the RxServer you can add different types of ",(0,r.jsx)(n.strong,{children:"endpoints"}),". An endpoint is always connected to exactly one ",(0,r.jsx)(n.a,{href:"/rx-collection.html",children:"RxCollection"})," and it only serves data from that single collection."]}),"\n",(0,r.jsxs)(n.p,{children:["For now there are only two endpoints implemented, the ",(0,r.jsx)(n.a,{href:"#replication-endpoint",children:"replication endpoint"})," and the ",(0,r.jsx)(n.a,{href:"#rest-endpoint",children:"REST endpoint"}),". Others will be added in the future."]}),"\n",(0,r.jsxs)(n.p,{children:["An endpoint is added to the server by calling the add endpoint method like ",(0,r.jsx)(n.code,{children:"myRxServer.addReplicationEndpoint()"}),". Each needs a different ",(0,r.jsx)(n.code,{children:"name"})," string as input which will define the resulting endpoint url."]}),"\n",(0,r.jsxs)(n.p,{children:["The endpoint urls is a combination of the given ",(0,r.jsx)(n.code,{children:"name"})," and schema ",(0,r.jsx)(n.code,{children:"version"})," of the collection, like ",(0,r.jsx)(n.code,{children:"/my-endpoint/0"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"const myEndpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection\n});\n\nconsole.log(myEndpoint.urlPath) // > 'my-endpoint/0'\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Notice that it is ",(0,r.jsx)(n.strong,{children:"not required"})," that the server side schema version is equal to the client side schema version. You might want to change server schemas more often and then only do a ",(0,r.jsx)(n.a,{href:"/migration-schema.html",children:"migration"})," on the server, not on the clients."]}),"\n",(0,r.jsx)(n.h2,{id:"replication-endpoint",children:"Replication Endpoint"}),"\n",(0,r.jsxs)(n.p,{children:["The replication endpoint allows clients that connect to it to replicate data with the server via the RxDB ",(0,r.jsx)(n.a,{href:"/replication.html",children:"replication protocol"}),". There is also the ",(0,r.jsx)(n.a,{href:"/replication-server",children:"Replication Server"})," plugin that is used on the client side to connect to the endpoint."]}),"\n",(0,r.jsxs)(n.p,{children:["The endpoint is added to the server with the ",(0,r.jsx)(n.code,{children:"addReplicationEndpoint()"})," method. It requires a specific collection and the endpoint will only provided replication for documents inside of that collection."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"// > server.ts\nconst endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection\n});\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Then you can start the ",(0,r.jsx)(n.a,{href:"/replication-server",children:"Server Replication"})," on the client:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"// > client.ts\nconst replicationState = await replicateServer({\n collection: usersCollection,\n replicationIdentifier: 'my-server-replication',\n url: 'http://localhost:80/my-endpoint/0',\n push: {},\n pull: {}\n});\n"})}),"\n",(0,r.jsx)(n.h2,{id:"rest-endpoint",children:"REST endpoint"}),"\n",(0,r.jsx)(n.p,{children:"The REST endpoint exposes various methods to access the data from the RxServer with non-RxDB tools via plain HTTP operations. You can use it to connect apps that are programmed in different programming languages than JavaScript or to access data from other third party tools."}),"\n",(0,r.jsx)(n.p,{children:"Creating a REST endpoint on a RxServer:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"const endpoint = await server.addRestEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection\n});\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"// plain http request with fetch\nconst request = await fetch('http://localhost:80/' + endpoint.urlPath + '/query', {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ selector: {} })\n});\nconst response = await request.json();\n"})}),"\n",(0,r.jsxs)(n.p,{children:["There is also the ",(0,r.jsx)(n.code,{children:"client-rest"})," plugin that provides typesave interactions with the REST endpoint:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"// using the client (optional)\nimport { createRestClient } from 'rxdb-server/plugins/client-rest';\nconst client = createRestClient('http://localhost:80/' + endpoint.urlPath, {/* headers */});\nconst response = await client.query({ selector: {} });\n"})}),"\n",(0,r.jsx)(n.p,{children:"The REST endpoint exposes the following paths:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"query [POST]"}),": Fetch the results of a NoSQL query."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"query/observe [GET]"}),": Observe a query's results via ",(0,r.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events",children:"Server Send Events"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"get [POST]"}),": Fetch multiple documents by their primary key."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"set [POST]"}),": Write multiple documents at once."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"delete [POST]"}),": Delete multiple documents by their primary key."]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"cors",children:"CORS"}),"\n",(0,r.jsxs)(n.p,{children:["When creating a server or adding endpoints, you can specify a ",(0,r.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS",children:"CORS"})," string.\nEndpoint cors always overwrite server cors. The default is the wildcard ",(0,r.jsx)(n.code,{children:"*"})," which allows all requests."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"const myServer = await startRxServer({\n database: myRxDatabase,\n cors: 'http://example.com'\n port: 443\n});\nconst endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection,\n cors: 'http://example.com'\n});\n"})}),"\n",(0,r.jsx)(n.h2,{id:"auth-handler",children:"Auth handler"}),"\n",(0,r.jsxs)(n.p,{children:["To authenticate users and to make user-specific data available on server requests, an ",(0,r.jsx)(n.code,{children:"authHandler"})," must be provided that parses the headers and returns the actual auth data that is used to authenticate the client and in the ",(0,r.jsx)(n.a,{href:"#query-modifier",children:"queryModifier"})," and ",(0,r.jsx)(n.a,{href:"#change-validator",children:"changeValidator"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["An auth handler gets the given headers object as input and returns the auth data in the format ",(0,r.jsx)(n.code,{children:"{ data: {}, validUntil: 1706579817126}"}),".\nThe ",(0,r.jsx)(n.code,{children:"data"})," field can contain any data that can be used afterwards in the queryModifier and changeValidator.\nThe ",(0,r.jsx)(n.code,{children:"validUntil"})," field contains the unix timestamp in milliseconds at which the authentication is no longer valid and the client will get disconnected."]}),"\n",(0,r.jsxs)(n.p,{children:["For example your authHandler could get the ",(0,r.jsx)(n.code,{children:"Authorization"})," header and parse the ",(0,r.jsx)(n.a,{href:"https://jwt.io/",children:"JSON web token"})," to identify the user and store the user id in the ",(0,r.jsx)(n.code,{children:"data"})," field for later use."]}),"\n",(0,r.jsx)(n.h2,{id:"query-modifier",children:"Query modifier"}),"\n",(0,r.jsx)(n.p,{children:"The query modifier is a JavaScript function that is used to restrict which documents a client can fetch or replicate from the server.\nIt gets the auth data and the actual NoSQL query as input parameter and returns a modified NoSQL query that is then used internally by the server.\nYou can pass a different query modifier to each endpoint so that you can have different endpoints for different use cases on the same server."}),"\n",(0,r.jsxs)(n.p,{children:["For example you could use a query modifier that get the ",(0,r.jsx)(n.code,{children:"userId"})," from the auth data and then restricts the query to only return documents that have the same ",(0,r.jsx)(n.code,{children:"userId"})," set."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"function myQueryModifier(authData, query) {\n query.selector.userId = { $eq: authData.data.userid };\n return query;\n}\n\nconst endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection,\n queryModifier: myQueryModifier\n});\n"})}),"\n",(0,r.jsx)(n.p,{children:"The RxServer will use the queryModifier at many places internally to determine which queries to run or if a document is allowed to be seen/edited by a client."}),"\n",(0,r.jsxs)(n.p,{children:["NOTICE: For performance reasons the ",(0,r.jsx)(n.code,{children:"queryModifier"})," and ",(0,r.jsx)(n.code,{children:"changeValidator"})," ",(0,r.jsx)(n.strong,{children:"MUST NOT"})," be ",(0,r.jsx)(n.code,{children:"async"})," and return a promise. If you need async data to run them, you should gather that data in the ",(0,r.jsx)(n.code,{children:"RxServerAuthHandler"})," and store it in the auth data to access it later."]}),"\n",(0,r.jsx)(n.h2,{id:"change-validator",children:"Change validator"}),"\n",(0,r.jsx)(n.p,{children:"The change validator is a JavaScript function that is used to restrict which document writes are allowed to be done by a client.\nFor example you could restrict clients to only change specific document fields or to not do any document writes at all.\nIt can also be used to validate change document data before storing it at the server."}),"\n",(0,r.jsxs)(n.p,{children:["In this example we restrict clients from doing inserts and only allow updates. For that we check if the change contains an ",(0,r.jsx)(n.code,{children:"assumedMasterState"})," property and return false to block the write."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"\nfunction myChangeValidator(authData, change) {\n if(change.assumedMasterState) {\n return false;\n } else {\n return true;\n }\n}\n\nconst endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection,\n changeValidator: myChangeValidator\n});\n"})}),"\n",(0,r.jsx)(n.h1,{id:"server-only-indexes",children:"Server-only indexes"}),"\n",(0,r.jsxs)(n.p,{children:["Normal RxDB schema indexes get the ",(0,r.jsx)(n.code,{children:"_deleted"})," field prepended because all ",(0,r.jsx)(n.a,{href:"/rx-query.html",children:"RxQueries"})," automatically only search for documents with ",(0,r.jsx)(n.code,{children:"_deleted=false"}),".\nWhen you use RxDB on a server, this might not be optimal because there can be the need to query for documents where the value of ",(0,r.jsx)(n.code,{children:"_deleted"})," does not mather. Mostly this is required in the ",(0,r.jsx)(n.a,{href:"/replication.html#checkpoint-iteration",children:"pull.stream$"})," of a replication when a ",(0,r.jsx)(n.a,{href:"#query-modifier",children:"queryModifier"})," is used to add an additional field to the query."]}),"\n",(0,r.jsxs)(n.p,{children:["To set indexes without ",(0,r.jsx)(n.code,{children:"_deleted"}),", you can use the ",(0,r.jsx)(n.code,{children:"internalIndexes"})," field of the schema like the following:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:' {\n "version": 0,\n "primaryKey": "id",\n "type": "object",\n "properties": {\n "id": {\n "type": "string",\n "maxLength": 100\n },\n "name": {\n "type": "string",\n "maxLength": 100\n }\n },\n "internalIndexes": [\n ["name", "id"]\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"NOTICE:"})," Indexes come with a performance burden. You should only use the indexes you need and make sure you ",(0,r.jsx)(n.strong,{children:"do not"})," accidentally set the ",(0,r.jsx)(n.code,{children:"internalIndexes"})," in your client side ",(0,r.jsx)(n.a,{href:"/rx-collection.html",children:"RxCollections"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"server-only-fields",children:"Server-only fields"}),"\n",(0,r.jsxs)(n.p,{children:["All endpoints can be created with the ",(0,r.jsx)(n.code,{children:"serverOnlyFields"})," set which defines some fields to only exist on the server, not on the clients. Clients will not see that fields and cannot do writes where one of the ",(0,r.jsx)(n.code,{children:"serverOnlyFields"})," is set.\nNotice that when you use ",(0,r.jsx)(n.code,{children:"serverOnlyFields"})," you likely need to have a different schema on the server than the schema that is used on the clients."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"const endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: col,\n // here the field 'my-secretss' is defined to be server-only\n serverOnlyFields: ['my-secrets']\n});\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"NOTICE"}),": For performance reasons, only top-level fields can be used as ",(0,r.jsx)(n.code,{children:"serverOnlyFields"}),". Otherwise the server would have to deep-clone all document data which is too expensive."]}),"\n",(0,r.jsx)(n.h2,{id:"readonly-fields",children:"Readonly fields"}),"\n",(0,r.jsxs)(n.p,{children:["When you have fields that should only be modified by the server, but not by the client, you can ensure that by comparing the fields value in the ",(0,r.jsx)(n.a,{href:"#change-validator",children:"changeValidator"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"\nconst myChangeValidator = function(authData, change){\n if(change.newDocumentState.myReadonlyField !== change.assumedMasterState.myReadonlyField){\n throw new Error('myReadonlyField is readonly');\n }\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"regex-queries-not-allowed",children:"$regex queries not allowed"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"$regex"})," queries are not allowed to run at the server to prevent ",(0,r.jsx)(n.a,{href:"https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS",children:"ReDos Attacks"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"conflict-handling",children:"Conflict handling"}),"\n",(0,r.jsxs)(n.p,{children:["To ",(0,r.jsx)(n.a,{href:"/replication.html#conflict-handling",children:"detect and handle conflicts"}),", the conflict handler from the endpoints RxCollection is used."]}),"\n",(0,r.jsx)(n.h2,{id:"missing-features",children:"Missing features"}),"\n",(0,r.jsx)(n.p,{children:"The server plugin is in beta mode and some features are still missing. Make a Pull Request when you need them."}),"\n",(0,r.jsx)(n.h2,{id:"faq",children:"FAQ"}),"\n",(0,r.jsxs)(t,{children:[(0,r.jsx)("summary",{children:"Why are the server plugins in a different github repo and npm package?"}),(0,r.jsxs)("div",{children:[(0,r.jsx)(n.p,{children:"The RxServer and its other plugins are in a different github repository because:"}),(0,r.jsxs)("ul",{children:[(0,r.jsx)("li",{children:(0,r.jsx)(n.p,{children:"It has too many dependencies that you do not want to install if you only use RxDB at the client side"})}),(0,r.jsx)("li",{children:(0,r.jsxs)(n.p,{children:["It has a different license ",(0,r.jsx)("a",{href:"https://en.wikipedia.org/wiki/Server_Side_Public_License",children:"(SSPL)"}),' to prevent large cloud vendors from "stealing" the revenue, similar to MongoDB\'s license.']})})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>o});var r=t(6540);const i={},s=r.createContext(i);function a(e){const n=r.useContext(s);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),r.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/f43e80a8.9e1daf62.js b/docs/assets/js/f43e80a8.9e1daf62.js new file mode 100644 index 00000000000..614d68c8c1a --- /dev/null +++ b/docs/assets/js/f43e80a8.9e1daf62.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[1558],{9544:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var r=t(4848),i=t(8453);const s={title:"RxServer",slug:"rx-server.html"},a="RxDB Server",o={id:"rx-server",title:"RxServer",description:'The RxDB Server Plugin makes it possible to spawn a server on top of a RxDB database that offers multiple types of endpoints for various usages. It can spawn basic CRUD REST endpoints or even realtime replication endpoints that can be used by the client devices to replicate data. The RxServer plugin is designed to be used in Node.js but you can also use it in Deno, Bun or the Electron "main" process. You can use it either as a standalone server or add it on top of an existing http server (like express) in nodejs.',source:"@site/docs/rx-server.md",sourceDirName:".",slug:"/rx-server.html",permalink:"/rx-server.html",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{title:"RxServer",slug:"rx-server.html"},sidebar:"tutorialSidebar",previous:{title:"NATS Replication",permalink:"/replication-nats.html"},next:{title:"RxServer Scaling",permalink:"/rx-server-scaling.html"}},l={},d=[{value:"Starting a RxServer",id:"starting-a-rxserver",level:2},{value:"RxServer Endpoints",id:"rxserver-endpoints",level:2},{value:"Replication Endpoint",id:"replication-endpoint",level:2},{value:"REST endpoint",id:"rest-endpoint",level:2},{value:"CORS",id:"cors",level:2},{value:"Auth handler",id:"auth-handler",level:2},{value:"Query modifier",id:"query-modifier",level:2},{value:"Change validator",id:"change-validator",level:2},{value:"Server-only fields",id:"server-only-fields",level:2},{value:"Readonly fields",id:"readonly-fields",level:2},{value:"$regex queries not allowed",id:"regex-queries-not-allowed",level:2},{value:"Conflict handling",id:"conflict-handling",level:2},{value:"Missing features",id:"missing-features",level:2},{value:"FAQ",id:"faq",level:2}];function c(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"rxdb-server",children:"RxDB Server"}),"\n",(0,r.jsxs)(n.p,{children:['The RxDB Server Plugin makes it possible to spawn a server on top of a RxDB database that offers multiple types of endpoints for various usages. It can spawn basic CRUD REST endpoints or even realtime replication endpoints that can be used by the client devices to replicate data. The RxServer plugin is designed to be used in Node.js but you can also use it in Deno, Bun or the Electron "main" process. You can use it either as a ',(0,r.jsx)(n.strong,{children:"standalone server"})," or add it on top of an ",(0,r.jsx)(n.strong,{children:"existing http server"})," (like express) in nodejs."]}),"\n",(0,r.jsx)(n.admonition,{title:"beta",type:"warning",children:(0,r.jsxs)(n.p,{children:["The server plugin is in ",(0,r.jsx)(n.strong,{children:"beta"})," mode and the API might be changed without a major RxDB release."]})}),"\n",(0,r.jsx)(n.h2,{id:"starting-a-rxserver",children:"Starting a RxServer"}),"\n",(0,r.jsxs)(n.p,{children:["To create an ",(0,r.jsx)(n.code,{children:"RxServer"}),", you have to install the ",(0,r.jsx)(n.code,{children:"rxdb-server"})," package with ",(0,r.jsx)(n.code,{children:"npm install rxdb-server --save"})," and then you can import the ",(0,r.jsx)(n.code,{children:"startRxServer()"})," function and create a server on a given ",(0,r.jsx)(n.a,{href:"/rx-database.html",children:"RxDatabase"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"import { startRxServer } from 'rxdb-server/plugins/server';\n\nconst myServer = await startRxServer({\n database: myRxDatabase,\n port: 443\n});\n"})}),"\n",(0,r.jsx)(n.h2,{id:"rxserver-endpoints",children:"RxServer Endpoints"}),"\n",(0,r.jsxs)(n.p,{children:["On top of the RxServer you can add different types of ",(0,r.jsx)(n.strong,{children:"endpoints"}),". An endpoint is always connected to exactly one ",(0,r.jsx)(n.a,{href:"/rx-collection.html",children:"RxCollection"})," and it only serves data from that single collection."]}),"\n",(0,r.jsxs)(n.p,{children:["For now there are only two endpoints implemented, the ",(0,r.jsx)(n.a,{href:"#replication-endpoint",children:"replication endpoint"})," and the ",(0,r.jsx)(n.a,{href:"#rest-endpoint",children:"REST endpoint"}),". Others will be added in the future."]}),"\n",(0,r.jsxs)(n.p,{children:["An endpoint is added to the server by calling the add endpoint method like ",(0,r.jsx)(n.code,{children:"myRxServer.addReplicationEndpoint()"}),". Each needs a different ",(0,r.jsx)(n.code,{children:"name"})," string as input which will define the resulting endpoint url."]}),"\n",(0,r.jsxs)(n.p,{children:["The endpoint urls is a combination of the given ",(0,r.jsx)(n.code,{children:"name"})," and schema ",(0,r.jsx)(n.code,{children:"version"})," of the collection, like ",(0,r.jsx)(n.code,{children:"/my-endpoint/0"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"const myEndpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection\n});\n\nconsole.log(myEndpoint.urlPath) // > 'my-endpoint/0'\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Notice that it is ",(0,r.jsx)(n.strong,{children:"not required"})," that the server side schema version is equal to the client side schema version. You might want to change server schemas more often and then only do a ",(0,r.jsx)(n.a,{href:"/migration-schema.html",children:"migration"})," on the server, not on the clients."]}),"\n",(0,r.jsx)(n.h2,{id:"replication-endpoint",children:"Replication Endpoint"}),"\n",(0,r.jsxs)(n.p,{children:["The replication endpoint allows clients that connect to it to replicate data with the server via the RxDB ",(0,r.jsx)(n.a,{href:"/replication.html",children:"replication protocol"}),". There is also the ",(0,r.jsx)(n.a,{href:"/replication-server",children:"Replication Server"})," plugin that is used on the client side to connect to the endpoint."]}),"\n",(0,r.jsxs)(n.p,{children:["The endpoint is added to the server with the ",(0,r.jsx)(n.code,{children:"addReplicationEndpoint()"})," method. It requires a specific collection and the endpoint will only provided replication for documents inside of that collection."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"// > server.ts\nconst endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection\n});\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Then you can start the ",(0,r.jsx)(n.a,{href:"/replication-server",children:"Server Replication"})," on the client:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"// > client.ts\nconst replicationState = await replicateServer({\n collection: usersCollection,\n replicationIdentifier: 'my-server-replication',\n url: 'http://localhost:80/my-endpoint/0',\n push: {},\n pull: {}\n});\n"})}),"\n",(0,r.jsx)(n.h2,{id:"rest-endpoint",children:"REST endpoint"}),"\n",(0,r.jsx)(n.p,{children:"The REST endpoint exposes various methods to access the data from the RxServer with non-RxDB tools via plain HTTP operations. You can use it to connect apps that are programmed in different programming languages than JavaScript or to access data from other third party tools."}),"\n",(0,r.jsx)(n.p,{children:"Creating a REST endpoint on a RxServer:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"const endpoint = await server.addRestEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection\n});\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"// plain http request with fetch\nconst request = await fetch('http://localhost:80/' + endpoint.urlPath + '/query', {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ selector: {} })\n});\nconst response = await request.json();\n"})}),"\n",(0,r.jsxs)(n.p,{children:["There is also the ",(0,r.jsx)(n.code,{children:"client-rest"})," plugin that provides typesave interactions with the REST endpoint:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"// using the client (optional)\nimport { createRestClient } from 'rxdb-server/plugins/client-rest';\nconst client = createRestClient('http://localhost:80/' + endpoint.urlPath, {/* headers */});\nconst response = await client.query({ selector: {} });\n"})}),"\n",(0,r.jsx)(n.p,{children:"The REST endpoint exposes the following paths:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"query [POST]"}),": Fetch the results of a NoSQL query."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"query/observe [GET]"}),": Observe a query's results via ",(0,r.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events",children:"Server Send Events"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"get [POST]"}),": Fetch multiple documents by their primary key."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"set [POST]"}),": Write multiple documents at once."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"delete [POST]"}),": Delete multiple documents by their primary key."]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"cors",children:"CORS"}),"\n",(0,r.jsxs)(n.p,{children:["When creating a server or adding endpoints, you can specify a ",(0,r.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS",children:"CORS"})," string.\nEndpoint cors always overwrite server cors. The default is the wildcard ",(0,r.jsx)(n.code,{children:"*"})," which allows all requests."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"const myServer = await startRxServer({\n database: myRxDatabase,\n cors: 'http://example.com'\n port: 443\n});\nconst endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection,\n cors: 'http://example.com'\n});\n"})}),"\n",(0,r.jsx)(n.h2,{id:"auth-handler",children:"Auth handler"}),"\n",(0,r.jsxs)(n.p,{children:["To authenticate users and to make user-specific data available on server requests, an ",(0,r.jsx)(n.code,{children:"authHandler"})," must be provided that parses the headers and returns the actual auth data that is used to authenticate the client and in the ",(0,r.jsx)(n.a,{href:"#query-modifier",children:"queryModifier"})," and ",(0,r.jsx)(n.a,{href:"#change-validator",children:"changeValidator"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["An auth handler gets the given headers object as input and returns the auth data in the format ",(0,r.jsx)(n.code,{children:"{ data: {}, validUntil: 1706579817126}"}),".\nThe ",(0,r.jsx)(n.code,{children:"data"})," field can contain any data that can be used afterwards in the queryModifier and changeValidator.\nThe ",(0,r.jsx)(n.code,{children:"validUntil"})," field contains the unix timestamp in milliseconds at which the authentication is no longer valid and the client will get disconnected."]}),"\n",(0,r.jsxs)(n.p,{children:["For example your authHandler could get the ",(0,r.jsx)(n.code,{children:"Authorization"})," header and parse the ",(0,r.jsx)(n.a,{href:"https://jwt.io/",children:"JSON web token"})," to identify the user and store the user id in the ",(0,r.jsx)(n.code,{children:"data"})," field for later use."]}),"\n",(0,r.jsx)(n.h2,{id:"query-modifier",children:"Query modifier"}),"\n",(0,r.jsx)(n.p,{children:"The query modifier is a JavaScript function that is used to restrict which documents a client can fetch or replicate from the server.\nIt gets the auth data and the actual NoSQL query as input parameter and returns a modified NoSQL query that is then used internally by the server.\nYou can pass a different query modifier to each endpoint so that you can have different endpoints for different use cases on the same server."}),"\n",(0,r.jsxs)(n.p,{children:["For example you could use a query modifier that get the ",(0,r.jsx)(n.code,{children:"userId"})," from the auth data and then restricts the query to only return documents that have the same ",(0,r.jsx)(n.code,{children:"userId"})," set."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"function myQueryModifier(authData, query) {\n query.selector.userId = { $eq: authData.data.userid };\n return query;\n}\n\nconst endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection,\n queryModifier: myQueryModifier\n});\n"})}),"\n",(0,r.jsx)(n.p,{children:"The RxServer will use the queryModifier at many places internally to determine which queries to run or if a document is allowed to be seen/edited by a client."}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["For performance reasons the ",(0,r.jsx)(n.code,{children:"queryModifier"})," and ",(0,r.jsx)(n.code,{children:"changeValidator"})," ",(0,r.jsx)(n.strong,{children:"MUST NOT"})," be ",(0,r.jsx)(n.code,{children:"async"})," and return a promise. If you need async data to run them, you should gather that data in the ",(0,r.jsx)(n.code,{children:"RxServerAuthHandler"})," and store it in the auth data to access it later."]})}),"\n",(0,r.jsx)(n.h2,{id:"change-validator",children:"Change validator"}),"\n",(0,r.jsx)(n.p,{children:"The change validator is a JavaScript function that is used to restrict which document writes are allowed to be done by a client.\nFor example you could restrict clients to only change specific document fields or to not do any document writes at all.\nIt can also be used to validate change document data before storing it at the server."}),"\n",(0,r.jsxs)(n.p,{children:["In this example we restrict clients from doing inserts and only allow updates. For that we check if the change contains an ",(0,r.jsx)(n.code,{children:"assumedMasterState"})," property and return false to block the write."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"\nfunction myChangeValidator(authData, change) {\n if(change.assumedMasterState) {\n return false;\n } else {\n return true;\n }\n}\n\nconst endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: myServerCollection,\n changeValidator: myChangeValidator\n});\n"})}),"\n",(0,r.jsx)(n.h1,{id:"server-only-indexes",children:"Server-only indexes"}),"\n",(0,r.jsxs)(n.p,{children:["Normal RxDB schema indexes get the ",(0,r.jsx)(n.code,{children:"_deleted"})," field prepended because all ",(0,r.jsx)(n.a,{href:"/rx-query.html",children:"RxQueries"})," automatically only search for documents with ",(0,r.jsx)(n.code,{children:"_deleted=false"}),".\nWhen you use RxDB on a server, this might not be optimal because there can be the need to query for documents where the value of ",(0,r.jsx)(n.code,{children:"_deleted"})," does not mather. Mostly this is required in the ",(0,r.jsx)(n.a,{href:"/replication.html#checkpoint-iteration",children:"pull.stream$"})," of a replication when a ",(0,r.jsx)(n.a,{href:"#query-modifier",children:"queryModifier"})," is used to add an additional field to the query."]}),"\n",(0,r.jsxs)(n.p,{children:["To set indexes without ",(0,r.jsx)(n.code,{children:"_deleted"}),", you can use the ",(0,r.jsx)(n.code,{children:"internalIndexes"})," field of the schema like the following:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:' {\n "version": 0,\n "primaryKey": "id",\n "type": "object",\n "properties": {\n "id": {\n "type": "string",\n "maxLength": 100\n },\n "name": {\n "type": "string",\n "maxLength": 100\n }\n },\n "internalIndexes": [\n ["name", "id"]\n ]\n}\n'})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Indexes come with a performance burden. You should only use the indexes you need and make sure you ",(0,r.jsx)(n.strong,{children:"do not"})," accidentally set the ",(0,r.jsx)(n.code,{children:"internalIndexes"})," in your client side ",(0,r.jsx)(n.a,{href:"/rx-collection.html",children:"RxCollections"}),"."]})}),"\n",(0,r.jsx)(n.h2,{id:"server-only-fields",children:"Server-only fields"}),"\n",(0,r.jsxs)(n.p,{children:["All endpoints can be created with the ",(0,r.jsx)(n.code,{children:"serverOnlyFields"})," set which defines some fields to only exist on the server, not on the clients. Clients will not see that fields and cannot do writes where one of the ",(0,r.jsx)(n.code,{children:"serverOnlyFields"})," is set.\nNotice that when you use ",(0,r.jsx)(n.code,{children:"serverOnlyFields"})," you likely need to have a different schema on the server than the schema that is used on the clients."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"const endpoint = await server.addReplicationEndpoint({\n name: 'my-endpoint',\n collection: col,\n // here the field 'my-secretss' is defined to be server-only\n serverOnlyFields: ['my-secrets']\n});\n"})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["For performance reasons, only top-level fields can be used as ",(0,r.jsx)(n.code,{children:"serverOnlyFields"}),". Otherwise the server would have to deep-clone all document data which is too expensive."]})}),"\n",(0,r.jsx)(n.h2,{id:"readonly-fields",children:"Readonly fields"}),"\n",(0,r.jsxs)(n.p,{children:["When you have fields that should only be modified by the server, but not by the client, you can ensure that by comparing the fields value in the ",(0,r.jsx)(n.a,{href:"#change-validator",children:"changeValidator"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ts",children:"\nconst myChangeValidator = function(authData, change){\n if(change.newDocumentState.myReadonlyField !== change.assumedMasterState.myReadonlyField){\n throw new Error('myReadonlyField is readonly');\n }\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"regex-queries-not-allowed",children:"$regex queries not allowed"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"$regex"})," queries are not allowed to run at the server to prevent ",(0,r.jsx)(n.a,{href:"https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS",children:"ReDos Attacks"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"conflict-handling",children:"Conflict handling"}),"\n",(0,r.jsxs)(n.p,{children:["To ",(0,r.jsx)(n.a,{href:"/replication.html#conflict-handling",children:"detect and handle conflicts"}),", the conflict handler from the endpoints RxCollection is used."]}),"\n",(0,r.jsx)(n.h2,{id:"missing-features",children:"Missing features"}),"\n",(0,r.jsx)(n.p,{children:"The server plugin is in beta mode and some features are still missing. Make a Pull Request when you need them."}),"\n",(0,r.jsx)(n.h2,{id:"faq",children:"FAQ"}),"\n",(0,r.jsxs)(t,{children:[(0,r.jsx)("summary",{children:"Why are the server plugins in a different github repo and npm package?"}),(0,r.jsxs)("div",{children:[(0,r.jsx)(n.p,{children:"The RxServer and its other plugins are in a different github repository because:"}),(0,r.jsxs)("ul",{children:[(0,r.jsx)("li",{children:(0,r.jsx)(n.p,{children:"It has too many dependencies that you do not want to install if you only use RxDB at the client side"})}),(0,r.jsx)("li",{children:(0,r.jsxs)(n.p,{children:["It has a different license ",(0,r.jsx)("a",{href:"https://en.wikipedia.org/wiki/Server_Side_Public_License",children:"(SSPL)"}),' to prevent large cloud vendors from "stealing" the revenue, similar to MongoDB\'s license.']})})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>o});var r=t(6540);const i={},s=r.createContext(i);function a(e){const n=r.useContext(s);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),r.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/docs/assets/js/main.8b52f384.js b/docs/assets/js/main.a2d95f78.js similarity index 99% rename from docs/assets/js/main.8b52f384.js rename to docs/assets/js/main.a2d95f78.js index 95397e155ed..b48c410d908 100644 --- a/docs/assets/js/main.8b52f384.js +++ b/docs/assets/js/main.a2d95f78.js @@ -1,2 +1,2 @@ -/*! For license information please see main.8b52f384.js.LICENSE.txt */ -(self.webpackChunkrxdb=self.webpackChunkrxdb||[]).push([[8792],{8328:(e,t,n)=>{"use strict";n.d(t,{A:()=>p});n(6540);var r=n(3259),a=n.n(r),o=n(4054);const i={"0027230a":[()=>n.e(8382).then(n.bind(n,2081)),"@site/docs/rx-storage-lokijs.md",2081],"01684a0a":[()=>n.e(6616).then(n.bind(n,3395)),"@site/docs/rx-storage-memory-synced.md",3395],"03e37916":[()=>n.e(6465).then(n.bind(n,3184)),"@site/docs/transactions-conflicts-revisions.md",3184],"045bd6f5":[()=>n.e(4475).then(n.bind(n,8201)),"@site/docs/encryption.md",8201],"04b0214f":[()=>n.e(6264).then(n.t.bind(n,4061,19)),"/home/runner/work/rxdb/rxdb/docs-src/.docusaurus/docusaurus-plugin-content-pages/default/plugin-route-context-module-100.json",4061],"0e268d20":[()=>Promise.all([n.e(5106),n.e(2584)]).then(n.bind(n,5607)),"@site/src/pages/premium.tsx",5607],"0e945c41":[()=>n.e(9796).then(n.bind(n,9722)),"@site/docs/replication-server.md",9722],"0f6e10f0":[()=>n.e(1475).then(n.bind(n,654)),"@site/docs/articles/progressive-web-app-database.md",654],"118cde4c":[()=>n.e(5272).then(n.bind(n,7245)),"@site/docs/replication-couchdb.md",7245],"14d72841":[()=>n.e(9772).then(n.bind(n,5221)),"@site/src/pages/newsletter.tsx",5221],17896441:[()=>Promise.all([n.e(1869),n.e(1968),n.e(8401)]).then(n.bind(n,242)),"@theme/DocItem",242],"187b985e":[()=>n.e(6866).then(n.bind(n,844)),"@site/docs/replication-webrtc.md",844],"1b0f8c91":[()=>n.e(3129).then(n.bind(n,9642)),"@site/docs/backup.md",9642],"1b238727":[()=>n.e(4013).then(n.bind(n,3721)),"@site/docs/rx-storage-performance.md",3721],"1c0701dd":[()=>n.e(6953).then(n.bind(n,668)),"@site/docs/middleware.md",668],"1da545ff":[()=>n.e(9743).then(n.bind(n,2678)),"@site/docs/releases/9.0.0.md",2678],"1df93b7f":[()=>Promise.all([n.e(1869),n.e(5106),n.e(4579),n.e(4583)]).then(n.bind(n,6866)),"@site/src/pages/index.tsx",6866],"1e0353aa":[()=>n.e(8588).then(n.bind(n,142)),"@site/docs/rx-storage.md",142],"1f391b9e":[()=>Promise.all([n.e(1869),n.e(1968),n.e(6061)]).then(n.bind(n,7973)),"@theme/MDXPage",7973],"21fa2740":[()=>n.e(5694).then(n.bind(n,763)),"@site/docs/releases/15.0.0.md",763],"2456d5e0":[()=>n.e(2966).then(n.bind(n,5654)),"@site/docs/rx-database.md",5654],"25626d15":[()=>n.e(268).then(n.bind(n,7258)),"@site/src/pages/chat.tsx",7258],"2564bf4f":[()=>n.e(6724).then(n.bind(n,5705)),"@site/docs/rxdb-tradeoffs.md",5705],"25a43fd4":[()=>n.e(4812).then(n.bind(n,3917)),"@site/docs/rx-storage-shared-worker.md",3917],"26b8a621":[()=>n.e(2055).then(n.bind(n,3439)),"@site/docs/replication-p2p.md",3439],"294ac9d5":[()=>n.e(8744).then(n.bind(n,958)),"@site/docs/plugins.md",958],"2efd0200":[()=>n.e(4132).then(n.bind(n,9839)),"@site/docs/tutorials/typescript.md",9839],"32667c41":[()=>n.e(8191).then(n.bind(n,6895)),"@site/docs/nosql-performance-tips.md",6895],"326aca46":[()=>n.e(4853).then(n.bind(n,3142)),"@site/docs/offline-first.md",3142],"34f94d1b":[()=>n.e(5832).then(n.bind(n,7746)),"@site/docs/electron-database.md",7746],36715375:[()=>n.e(2076).then(n.bind(n,2323)),"@site/src/pages/code.tsx",2323],"380cc66a":[()=>n.e(2061).then(n.bind(n,5668)),"@site/docs/rx-storage-dexie.md",5668],"38bbf12a":[()=>n.e(2078).then(n.bind(n,3762)),"@site/docs/articles/data-base.md",3762],"393be207":[()=>n.e(4134).then(n.bind(n,6602)),"@site/src/pages/markdown-page.md",6602],"39600c95":[()=>n.e(3148).then(n.bind(n,755)),"@site/docs/rx-query.md",755],"401008a8":[()=>n.e(5219).then(n.bind(n,6805)),"@site/docs/nodejs-database.md",6805],"41f941a1":[()=>n.e(5966).then(n.bind(n,3057)),"@site/docs/leader-election.md",3057],"432b83f9":[()=>n.e(4424).then(n.bind(n,9429)),"@site/docs/releases/8.0.0.md",9429],"4616b86a":[()=>n.e(465).then(n.bind(n,81)),"@site/docs/rx-storage-mongodb.md",81],"4777fd9a":[()=>n.e(6386).then(n.bind(n,1600)),"@site/docs/alternatives.md",1600],"4adf80bb":[()=>n.e(9548).then(n.bind(n,4442)),"@site/docs/rx-storage-pouchdb.md",4442],"4af60d2e":[()=>n.e(8545).then(n.bind(n,9249)),"@site/docs/articles/realtime-database.md",9249],"4ba7e5a3":[()=>n.e(9591).then(n.bind(n,6467)),"@site/docs/contribute.md",6467],"4ed9495b":[()=>n.e(1199).then(n.bind(n,1787)),"@site/docs/rx-storage-sqlite.md",1787],"502d8946":[()=>n.e(7817).then(n.bind(n,732)),"@site/src/pages/legal-notice.tsx",732],51334108:[()=>n.e(8926).then(n.bind(n,2644)),"@site/docs/articles/mobile-database.md",2644],"55a5b596":[()=>n.e(6717).then(n.bind(n,5536)),"@site/docs/rx-schema.md",5536],"5a273530":[()=>n.e(3881).then(n.bind(n,7589)),"@site/docs/rx-storage-remote.md",7589],"5e95c892":[()=>n.e(9647).then(n.bind(n,7121)),"@theme/DocsRoot",7121],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,4784)),"@generated/docusaurus.config",4784],"60c23941":[()=>n.e(2915).then(n.t.bind(n,1966,19)),"/home/runner/work/rxdb/rxdb/docs-src/.docusaurus/docusaurus-plugin-content-docs/default/plugin-route-context-module-100.json",1966],"6187b59a":[()=>n.e(5866).then(n.bind(n,7409)),"@site/docs/rx-storage-worker.md",7409],"68a466be":[()=>n.e(3997).then(n.bind(n,3923)),"@site/docs/downsides-of-offline-first.md",3923],"6ae3580c":[()=>n.e(5320).then(n.bind(n,5376)),"@site/docs/replication.md",5376],"6bfb0089":[()=>n.e(6422).then(n.bind(n,300)),"@site/docs/adapters.md",300],"6cbff7c2":[()=>n.e(7408).then(n.bind(n,5943)),"@site/docs/rx-storage-opfs.md",5943],"6fd28feb":[()=>n.e(4618).then(n.bind(n,9408)),"@site/docs/rx-storage-foundationdb.md",9408],"714575d7":[()=>n.e(3185).then(n.bind(n,4880)),"@site/docs/rx-local-document.md",4880],"77d975e6":[()=>n.e(3949).then(n.bind(n,5707)),"@site/docs/articles/in-memory-nosql-database.md",5707],"7815dd0c":[()=>n.e(5335).then(n.bind(n,5515)),"@site/docs/population.md",5515],"7f02c700":[()=>n.e(9592).then(n.bind(n,9640)),"@site/docs/replication-firestore.md",9640],"8070e160":[()=>n.e(3822).then(n.bind(n,1685)),"@site/docs/quickstart.md",1685],"8288c265":[()=>n.e(9881).then(n.bind(n,1839)),"@site/docs/releases/11.0.0.md",1839],"84a3af36":[()=>n.e(6861).then(n.bind(n,8160)),"@site/docs/articles/json-database.md",8160],"84ae55a4":[()=>n.e(588).then(n.bind(n,2045)),"@site/docs/replication-nats.md",2045],"86b4e356":[()=>n.e(2786).then(n.bind(n,9278)),"@site/docs/orm.md",9278],"8aa53ed7":[()=>n.e(6723).then(n.bind(n,5870)),"@site/docs/articles/angular-database.md",5870],"8b0a0922":[()=>n.e(4557).then(n.bind(n,767)),"@site/docs/slow-indexeddb.md",767],"8bc07e20":[()=>n.e(1850).then(n.bind(n,5054)),"@site/docs/capacitor-database.md",5054],"91b454ee":[()=>n.e(4202).then(n.bind(n,9107)),"@site/docs/rx-storage-sharding.md",9107],"924d6dd6":[()=>n.e(5122).then(n.bind(n,2414)),"@site/docs/electron.md",2414],"92698a99":[()=>n.e(4166).then(n.bind(n,8568)),"@site/docs/rx-storage-indexeddb.md",8568],"931f4566":[()=>n.e(3595).then(n.bind(n,2439)),"@site/docs/schema-validation.md",2439],"935f2afb":[()=>n.e(8581).then(n.t.bind(n,5610,19)),"~docs/default/version-current-metadata-prop-751.json",5610],98405524:[()=>n.e(5504).then(n.bind(n,1934)),"@site/src/pages/survey.tsx",1934],"9dd8ea89":[()=>n.e(1715).then(n.bind(n,7919)),"@site/docs/logger.md",7919],"9e91b6f0":[()=>n.e(3021).then(n.bind(n,1844)),"@site/docs/why-nosql.md",1844],a406dc27:[()=>n.e(1500).then(n.bind(n,8306)),"@site/docs/migration-storage.md",8306],a442adcd:[()=>n.e(8760).then(n.bind(n,188)),"@site/docs/reactivity.md",188],a574e172:[()=>n.e(7149).then(n.bind(n,9329)),"@site/docs/replication-http.md",9329],a69eebfc:[()=>n.e(9408).then(n.bind(n,4120)),"@site/docs/query-optimizer.md",4120],a7bd4aaa:[()=>n.e(7098).then(n.bind(n,4532)),"@theme/DocVersionRoot",4532],a7f10198:[()=>n.e(1098).then(n.bind(n,769)),"@site/docs/data-migration.md",769],a94703ab:[()=>Promise.all([n.e(1869),n.e(9048)]).then(n.bind(n,2559)),"@theme/DocRoot",2559],aa14e6b1:[()=>n.e(9824).then(n.bind(n,3268)),"@site/docs/replication-graphql.md",3268],ab919a1f:[()=>n.e(6491).then(n.bind(n,4760)),"@site/docs/articles/embedded-database.md",4760],ac62b32d:[()=>n.e(9192).then(n.bind(n,9585)),"@site/docs/replication-websocket.md",9585],ad16b3ea:[()=>n.e(8674).then(n.bind(n,2462)),"@site/docs/dev-mode.md",2462],b0889a22:[()=>n.e(1107).then(n.bind(n,1798)),"@site/docs/cleanup.md",1798],b30f4f1f:[()=>n.e(3779).then(n.bind(n,5830)),"@site/docs/rx-attachment.md",5830],b8c49ce4:[()=>n.e(6355).then(n.bind(n,1605)),"@site/docs/releases/13.0.0.md",1605],badcd764:[()=>n.e(8318).then(n.bind(n,6042)),"@site/docs/articles/flutter-database.md",6042],c3bc9c50:[()=>n.e(9167).then(n.bind(n,4337)),"@site/docs/articles/react-database.md",4337],c4de80f8:[()=>n.e(2777).then(n.bind(n,1173)),"@site/docs/install.md",1173],c6349bb6:[()=>n.e(5740).then(n.bind(n,2964)),"@site/docs/releases/14.0.0.md",2964],c6fdd490:[()=>n.e(4141).then(n.bind(n,5008)),"@site/docs/rx-server-scaling.md",5008],c843a053:[()=>n.e(9146).then(n.bind(n,8723)),"@site/docs/third-party-plugins.md",8723],c9c8e0b6:[()=>n.e(7249).then(n.bind(n,7674)),"@site/docs/articles/ionic-database.md",7674],cbbe8f0a:[()=>n.e(3852).then(n.bind(n,8783)),"@site/docs/rx-collection.md",8783],cde77f4f:[()=>Promise.all([n.e(5106),n.e(6287)]).then(n.bind(n,6465)),"@site/src/pages/premium-submitted.tsx",6465],d20e74b4:[()=>n.e(5123).then(n.bind(n,2853)),"@site/docs/crdt.md",2853],d2758528:[()=>n.e(2586).then(n.bind(n,7108)),"@site/docs/articles/browser-storage.md",7108],d4da9db3:[()=>n.e(1400).then(n.bind(n,4228)),"@site/docs/rx-storage-memory.md",4228],d622bd51:[()=>n.e(2845).then(n.bind(n,9804)),"@site/docs/migration-schema.md",9804],db34d6b0:[()=>Promise.all([n.e(5106),n.e(7320)]).then(n.bind(n,9891)),"@site/src/pages/license.tsx",9891],dbde2ffe:[()=>n.e(6543).then(n.bind(n,118)),"@site/docs/rx-document.md",118],dc42ba65:[()=>n.e(4962).then(n.bind(n,4494)),"@site/docs/key-compression.md",4494],e24529eb:[()=>n.e(6797).then(n.bind(n,9263)),"@site/docs/rx-storage-localstorage-meta-optimizer.md",9263],e6b4453d:[()=>n.e(2360).then(n.bind(n,9133)),"@site/docs/query-cache.md",9133],e7478ff0:[()=>n.e(5416).then(n.bind(n,1873)),"@site/docs/questions-answers.md",1873],eadd9b3c:[()=>n.e(4886).then(n.bind(n,8041)),"@site/docs/rx-storage-filesystem-node.md",8041],ebace26e:[()=>n.e(4028).then(n.bind(n,7016)),"@site/docs/releases/10.0.0.md",7016],ec526260:[()=>n.e(7396).then(n.bind(n,9592)),"@site/docs/articles/browser-database.md",9592],ed2d6610:[()=>n.e(3469).then(n.bind(n,9222)),"@site/docs/releases/12.0.0.md",9222],ee1b9f21:[()=>n.e(6998).then(n.bind(n,3179)),"@site/docs/react-native-database.md",3179],f15938da:[()=>n.e(4630).then(n.bind(n,5674)),"@site/docs/articles/localstorage.md",5674],f43e80a8:[()=>n.e(1558).then(n.bind(n,9544)),"@site/docs/rx-server.md",9544],f44bb875:[()=>n.e(561).then(n.bind(n,46)),"@site/docs/articles/frontend-database.md",46],fe7a07ee:[()=>n.e(2085).then(n.bind(n,9378)),"@site/docs/rx-storage-denokv.md",9378]};var l=n(4848);function s(e){let{error:t,retry:n,pastDelay:r}=e;return t?(0,l.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,l.jsx)("p",{children:String(t)}),(0,l.jsx)("div",{children:(0,l.jsx)("button",{type:"button",onClick:n,children:"Retry"})})]}):r?(0,l.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,l.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,l.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,l.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var c=n(6921),u=n(3102);function d(e,t){if("*"===e)return a()({loading:s,loader:()=>n.e(9293).then(n.bind(n,9293)),modules:["@theme/NotFound"],webpack:()=>[9293],render(e,t){const n=e.default;return(0,l.jsx)(u.W,{value:{plugin:{name:"native",id:"default"}},children:(0,l.jsx)(n,{...t})})}});const r=o[`${e}-${t}`],d={},p=[],f=[],m=(0,c.A)(r);return Object.entries(m).forEach((e=>{let[t,n]=e;const r=i[n];r&&(d[t]=r[0],p.push(r[1]),f.push(r[2]))})),a().Map({loading:s,loader:d,modules:p,webpack:()=>f,render(t,n){const a=JSON.parse(JSON.stringify(r));Object.entries(t).forEach((t=>{let[n,r]=t;const o=r.default;if(!o)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof o&&"function"!=typeof o||Object.keys(r).filter((e=>"default"!==e)).forEach((e=>{o[e]=r[e]}));let i=a;const l=n.split(".");l.slice(0,-1).forEach((e=>{i=i[e]})),i[l[l.length-1]]=o}));const o=a.__comp;delete a.__comp;const i=a.__context;return delete a.__context,(0,l.jsx)(u.W,{value:i,children:(0,l.jsx)(o,{...a,...n})})}})}const p=[{path:"/chat",component:d("/chat","fd7"),exact:!0},{path:"/code",component:d("/code","8ef"),exact:!0},{path:"/legal-notice",component:d("/legal-notice","cfc"),exact:!0},{path:"/license",component:d("/license","d06"),exact:!0},{path:"/markdown-page",component:d("/markdown-page","784"),exact:!0},{path:"/newsletter",component:d("/newsletter","d15"),exact:!0},{path:"/premium",component:d("/premium","d94"),exact:!0},{path:"/premium-submitted",component:d("/premium-submitted","714"),exact:!0},{path:"/survey",component:d("/survey","9e0"),exact:!0},{path:"/",component:d("/","d82"),exact:!0},{path:"/",component:d("/","734"),routes:[{path:"/",component:d("/","494"),routes:[{path:"/",component:d("/","780"),routes:[{path:"/adapters.html",component:d("/adapters.html","fb4"),exact:!0},{path:"/alternatives.html",component:d("/alternatives.html","d53"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/angular-database.html",component:d("/articles/angular-database.html","e30"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/browser-database.html",component:d("/articles/browser-database.html","b0a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/browser-storage.html",component:d("/articles/browser-storage.html","286"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/data-base.html",component:d("/articles/data-base.html","797"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/embedded-database.html",component:d("/articles/embedded-database.html","596"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/flutter-database.html",component:d("/articles/flutter-database.html","f8a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/frontend-database.html",component:d("/articles/frontend-database.html","a83"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/in-memory-nosql-database.html",component:d("/articles/in-memory-nosql-database.html","ead"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/ionic-database.html",component:d("/articles/ionic-database.html","df6"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/json-database.html",component:d("/articles/json-database.html","bff"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/localstorage.html",component:d("/articles/localstorage.html","b1b"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/mobile-database.html",component:d("/articles/mobile-database.html","1d7"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/progressive-web-app-database.html",component:d("/articles/progressive-web-app-database.html","862"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/react-database.html",component:d("/articles/react-database.html","179"),exact:!0,sidebar:"tutorialSidebar"},{path:"/articles/realtime-database.html",component:d("/articles/realtime-database.html","bc3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/backup.html",component:d("/backup.html","123"),exact:!0,sidebar:"tutorialSidebar"},{path:"/capacitor-database.html",component:d("/capacitor-database.html","71f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/cleanup.html",component:d("/cleanup.html","d7f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/contribution.html",component:d("/contribution.html","129"),exact:!0,sidebar:"tutorialSidebar"},{path:"/crdt.html",component:d("/crdt.html","c69"),exact:!0,sidebar:"tutorialSidebar"},{path:"/data-migration",component:d("/data-migration","7c7"),exact:!0},{path:"/dev-mode.html",component:d("/dev-mode.html","11d"),exact:!0,sidebar:"tutorialSidebar"},{path:"/downsides-of-offline-first.html",component:d("/downsides-of-offline-first.html","ba4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/electron-database.html",component:d("/electron-database.html","44e"),exact:!0,sidebar:"tutorialSidebar"},{path:"/electron.html",component:d("/electron.html","693"),exact:!0,sidebar:"tutorialSidebar"},{path:"/encryption.html",component:d("/encryption.html","265"),exact:!0,sidebar:"tutorialSidebar"},{path:"/install.html",component:d("/install.html","9ec"),exact:!0,sidebar:"tutorialSidebar"},{path:"/key-compression.html",component:d("/key-compression.html","171"),exact:!0,sidebar:"tutorialSidebar"},{path:"/leader-election.html",component:d("/leader-election.html","9aa"),exact:!0,sidebar:"tutorialSidebar"},{path:"/logger.html",component:d("/logger.html","949"),exact:!0,sidebar:"tutorialSidebar"},{path:"/middleware.html",component:d("/middleware.html","34f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/migration-schema.html",component:d("/migration-schema.html","618"),exact:!0,sidebar:"tutorialSidebar"},{path:"/migration-storage.html",component:d("/migration-storage.html","34a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/nodejs-database.html",component:d("/nodejs-database.html","6a2"),exact:!0,sidebar:"tutorialSidebar"},{path:"/nosql-performance-tips.html",component:d("/nosql-performance-tips.html","fd8"),exact:!0,sidebar:"tutorialSidebar"},{path:"/offline-first.html",component:d("/offline-first.html","e1b"),exact:!0,sidebar:"tutorialSidebar"},{path:"/orm.html",component:d("/orm.html","798"),exact:!0,sidebar:"tutorialSidebar"},{path:"/plugins.html",component:d("/plugins.html","f25"),exact:!0,sidebar:"tutorialSidebar"},{path:"/population.html",component:d("/population.html","b89"),exact:!0,sidebar:"tutorialSidebar"},{path:"/query-cache.html",component:d("/query-cache.html","45f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/query-optimizer.html",component:d("/query-optimizer.html","bd3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/questions-answers.html",component:d("/questions-answers.html","840"),exact:!0,sidebar:"tutorialSidebar"},{path:"/quickstart.html",component:d("/quickstart.html","417"),exact:!0,sidebar:"tutorialSidebar"},{path:"/react-native-database.html",component:d("/react-native-database.html","7b4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/reactivity.html",component:d("/reactivity.html","4d4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/releases/10.0.0.html",component:d("/releases/10.0.0.html","8ce"),exact:!0,sidebar:"tutorialSidebar"},{path:"/releases/11.0.0.html",component:d("/releases/11.0.0.html","712"),exact:!0,sidebar:"tutorialSidebar"},{path:"/releases/12.0.0.html",component:d("/releases/12.0.0.html","a36"),exact:!0,sidebar:"tutorialSidebar"},{path:"/releases/13.0.0.html",component:d("/releases/13.0.0.html","605"),exact:!0,sidebar:"tutorialSidebar"},{path:"/releases/14.0.0.html",component:d("/releases/14.0.0.html","14c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/releases/15.0.0.html",component:d("/releases/15.0.0.html","4dd"),exact:!0,sidebar:"tutorialSidebar"},{path:"/releases/8.0.0.html",component:d("/releases/8.0.0.html","fdc"),exact:!0,sidebar:"tutorialSidebar"},{path:"/releases/9.0.0.html",component:d("/releases/9.0.0.html","f4b"),exact:!0,sidebar:"tutorialSidebar"},{path:"/replication-couchdb.html",component:d("/replication-couchdb.html","6d5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/replication-firestore.html",component:d("/replication-firestore.html","bcb"),exact:!0,sidebar:"tutorialSidebar"},{path:"/replication-graphql.html",component:d("/replication-graphql.html","a6c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/replication-http.html",component:d("/replication-http.html","16a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/replication-nats.html",component:d("/replication-nats.html","ac4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/replication-p2p.html",component:d("/replication-p2p.html","ec6"),exact:!0},{path:"/replication-server",component:d("/replication-server","aa6"),exact:!0,sidebar:"tutorialSidebar"},{path:"/replication-webrtc.html",component:d("/replication-webrtc.html","ff6"),exact:!0,sidebar:"tutorialSidebar"},{path:"/replication-websocket.html",component:d("/replication-websocket.html","4f9"),exact:!0,sidebar:"tutorialSidebar"},{path:"/replication.html",component:d("/replication.html","62b"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-attachment.html",component:d("/rx-attachment.html","7d7"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-collection.html",component:d("/rx-collection.html","7dc"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-database.html",component:d("/rx-database.html","e4e"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-document.html",component:d("/rx-document.html","e09"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-local-document.html",component:d("/rx-local-document.html","0db"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-query.html",component:d("/rx-query.html","2cf"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-schema.html",component:d("/rx-schema.html","671"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-server-scaling.html",component:d("/rx-server-scaling.html","da1"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-server.html",component:d("/rx-server.html","ed5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-denokv.html",component:d("/rx-storage-denokv.html","b6e"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-dexie.html",component:d("/rx-storage-dexie.html","bb3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-filesystem-node.html",component:d("/rx-storage-filesystem-node.html","a57"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-foundationdb.html",component:d("/rx-storage-foundationdb.html","3c8"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-indexeddb.html",component:d("/rx-storage-indexeddb.html","631"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-localstorage-meta-optimizer.html",component:d("/rx-storage-localstorage-meta-optimizer.html","064"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-lokijs.html",component:d("/rx-storage-lokijs.html","1be"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-memory-synced.html",component:d("/rx-storage-memory-synced.html","65f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-memory.html",component:d("/rx-storage-memory.html","c21"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-mongodb.html",component:d("/rx-storage-mongodb.html","5ad"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-opfs.html",component:d("/rx-storage-opfs.html","6f7"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-performance.html",component:d("/rx-storage-performance.html","b12"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-pouchdb.html",component:d("/rx-storage-pouchdb.html","d82"),exact:!0},{path:"/rx-storage-remote.html",component:d("/rx-storage-remote.html","5df"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-sharding.html",component:d("/rx-storage-sharding.html","b30"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-shared-worker.html",component:d("/rx-storage-shared-worker.html","0f4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-sqlite.html",component:d("/rx-storage-sqlite.html","ad7"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage-worker.html",component:d("/rx-storage-worker.html","a4e"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rx-storage.html",component:d("/rx-storage.html","144"),exact:!0,sidebar:"tutorialSidebar"},{path:"/rxdb-tradeoffs.html",component:d("/rxdb-tradeoffs.html","5cb"),exact:!0},{path:"/schema-validation.html",component:d("/schema-validation.html","3f1"),exact:!0},{path:"/slow-indexeddb.html",component:d("/slow-indexeddb.html","a40"),exact:!0,sidebar:"tutorialSidebar"},{path:"/third-party-plugins.html",component:d("/third-party-plugins.html","549"),exact:!0,sidebar:"tutorialSidebar"},{path:"/transactions-conflicts-revisions.html",component:d("/transactions-conflicts-revisions.html","53a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/tutorials/typescript.html",component:d("/tutorials/typescript.html","a33"),exact:!0,sidebar:"tutorialSidebar"},{path:"/why-nosql.html",component:d("/why-nosql.html","ad0"),exact:!0,sidebar:"tutorialSidebar"}]}]}]},{path:"*",component:d("*")}]},6125:(e,t,n)=>{"use strict";n.d(t,{o:()=>o,x:()=>i});var r=n(6540),a=n(4848);const o=r.createContext(!1);function i(e){let{children:t}=e;const[n,i]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{i(!0)}),[]),(0,a.jsx)(o.Provider,{value:n,children:t})}},8522:(e,t,n)=>{"use strict";var r=n(6540),a=n(5338),o=n(4625),i=n(545),l=n(8193);const s=[n(1911),n(119),n(6134),n(6294),n(1043)];var c=n(8328),u=n(6347),d=n(2831),p=n(2303),f=n(4848);function m(e){let{children:t}=e;const n=(0,p.A)();return(0,r.useEffect)((()=>{n&&(function(){const e="fixed-chat-button";if(document.getElementById(e))return;const t=document.createElement("a");t.id=e,t.href="/chat",t.target="_blank",t.innerHTML="\ud83d\udcac Community Chat";const n=document.createElement("style");n.type="text/css",n.innerText="#"+e+" {color: white;position: fixed;right: 0;bottom: 0;background-color: var(--color-top);padding-left: 17px;padding-right: 17px;padding-top: 10px;padding-bottom: 5px;text-align: center;margin-right: 50px;font-weight: bold;border-top-left-radius: 9px;border-top-right-radius: 9px;z-index: 11;}#fixed-chat-button:hover {box-shadow: 2px 2px 13px #ca007c, -2px -1px 14px #ff009e;text-decoration: underline;}",document.head.appendChild(n),document.body.appendChild(t)}(),function(){if("/"===location.pathname)return;const e=[{text:"Follow",keyword:"@twitter",url:"https://twitter.com/intent/user?screen_name=rxdbjs",icon:"\ud83d\udc26"},{text:"Follow",keyword:"@LinkedIn",url:"https://www.linkedin.com/company/rxdb",icon:"[in]"},{text:"Chat",keyword:"@discord",url:"https://rxdb.info/chat",icon:"\ud83d\udcac"},{text:"Star",keyword:"@github",url:"https://rxdb.info/code",icon:"\ud83d\udc19\ud83d\udcbb"},{text:"Subscribe",keyword:"@newsletter",url:"https://rxdb.info/newsletter",icon:"\ud83d\udcf0"}];function t(e,t){e.parentNode.insertBefore(t,e.nextSibling)}const n="rxdb-call-to-action-button";function r(){console.log("set call to action button");const r=6e5,a=Date.now(),o=(a-a%r)/r;console.log("timeslot "+o);const i=o%e.length;console.log("randid: "+i);const l=e[i],s=document.querySelector(".call-to-action");s&&s.parentNode.removeChild(s);const c=document.querySelector(".navbar__items");if(!c)return;const u=document.createElement("div");u.classList.add("call-to-action");const d=document.createElement("a");d.onclick=()=>{window.trigger("call-to-action",.35)},d.classList.add("hover-shadow-top"),d.id=n,d.innerHTML=l.text+' '+l.keyword+''+l.icon+"",d.href=l.url,d.target="_blank",u.append(d),t(c,u)}r()}())})),(0,f.jsx)(f.Fragment,{children:t})}var h=n(5260),g=n(4586),b=n(6025),y=n(6342),v=n(1003),w=n(2131),x=n(4090),k=n(2967),S=n(440),E=n(1463);function _(){const{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,g.A)(),r=(0,w.o)(),a=n[e].htmlLang,o=e=>e.replace("-","_");return(0,f.jsxs)(h.A,{children:[Object.entries(n).map((e=>{let[t,{htmlLang:n}]=e;return(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:n},t)})),(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,f.jsx)("meta",{property:"og:locale",content:o(a)}),Object.values(n).filter((e=>a!==e.htmlLang)).map((e=>(0,f.jsx)("meta",{property:"og:locale:alternate",content:o(e.htmlLang)},`meta-og-${e.htmlLang}`)))]})}function C(e){let{permalink:t}=e;const{siteConfig:{url:n}}=(0,g.A)(),r=function(){const{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,g.A)(),{pathname:r}=(0,u.zy)();return e+(0,S.applyTrailingSlash)((0,b.A)(r),{trailingSlash:n,baseUrl:t})}(),a=t?`${n}${t}`:r;return(0,f.jsxs)(h.A,{children:[(0,f.jsx)("meta",{property:"og:url",content:a}),(0,f.jsx)("link",{rel:"canonical",href:a})]})}function T(){const{i18n:{currentLocale:e}}=(0,g.A)(),{metadata:t,image:n}=(0,y.p)();return(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(h.A,{children:[(0,f.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,f.jsx)("body",{className:x.w})]}),n&&(0,f.jsx)(v.be,{image:n}),(0,f.jsx)(C,{}),(0,f.jsx)(_,{}),(0,f.jsx)(E.A,{tag:k.Cy,locale:e}),(0,f.jsx)(h.A,{children:t.map(((e,t)=>(0,f.jsx)("meta",{...e},t)))})]})}const A=new Map;function j(e){if(A.has(e.pathname))return{...e,pathname:A.get(e.pathname)};if((0,d.u)(c.A,e.pathname).some((e=>{let{route:t}=e;return!0===t.exact})))return A.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return A.set(e.pathname,t),{...e,pathname:t}}var N=n(6125),R=n(6988),L=n(205);function P(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{const r=t.default?.[e]??t[e];return r?.(...n)}));return()=>a.forEach((e=>e?.()))}const O=function(e){let{children:t,location:n,previousLocation:r}=e;return(0,L.A)((()=>{r!==n&&(!function(e){let{location:t,previousLocation:n}=e;if(!n)return;const r=t.pathname===n.pathname,a=t.hash===n.hash,o=t.search===n.search;if(r&&a&&!o)return;const{hash:i}=t;if(i){const e=decodeURIComponent(i.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:n,previousLocation:r}),P("onRouteDidUpdate",{previousLocation:r,location:n}))}),[r,n]),t};function I(e){const t=Array.from(new Set([e,decodeURI(e)])).map((e=>(0,d.u)(c.A,e))).flat();return Promise.all(t.map((e=>e.route.component.preload?.())))}class D extends r.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=l.A.canUseDOM?P("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=P("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),I(n.pathname).then((()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})})).catch((e=>{console.warn(e),window.location.reload()})),!1}render(){const{children:e,location:t}=this.props;return(0,f.jsx)(O,{previousLocation:this.previousLocation,location:t,children:(0,f.jsx)(u.qh,{location:t,render:()=>e})})}}const M=D,F="__docusaurus-base-url-issue-banner-container",z="__docusaurus-base-url-issue-banner",B="__docusaurus-base-url-issue-banner-suggestion-container";function $(e){return`\ndocument.addEventListener('DOMContentLoaded', function maybeInsertBanner() {\n var shouldInsert = typeof window['docusaurus'] === 'undefined';\n shouldInsert && insertBanner();\n});\n\nfunction insertBanner() {\n var bannerContainer = document.createElement('div');\n bannerContainer.id = '${F}';\n var bannerHtml = ${JSON.stringify(function(e){return`\n
\n

Your Docusaurus site did not load properly.

\n

A very common reason is a wrong site baseUrl configuration.

\n

Current configured baseUrl = ${e} ${"/"===e?" (default value)":""}

\n

We suggest trying baseUrl =

\n
\n`}(e)).replace(/{if("undefined"==typeof document)return void n();const r=document.createElement("link");r.setAttribute("rel","prefetch"),r.setAttribute("href",e),r.onload=()=>t(),r.onerror=()=>n();const a=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;a?.appendChild(r)}))}:function(e){return new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)}))};var Y=n(6921);const J=new Set,Z=new Set,X=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ee={prefetch(e){if(!(e=>!X()&&!Z.has(e)&&!J.has(e))(e))return!1;J.add(e);const t=(0,d.u)(c.A,e).flatMap((e=>{return t=e.route.path,Object.entries(Q).filter((e=>{let[n]=e;return n.replace(/-[^-]+$/,"")===t})).flatMap((e=>{let[,t]=e;return Object.values((0,Y.A)(t))}));var t}));return Promise.all(t.map((e=>{const t=n.gca(e);return t&&!t.includes("undefined")?K(t).catch((()=>{})):Promise.resolve()})))},preload:e=>!!(e=>!X()&&!Z.has(e))(e)&&(Z.add(e),I(e))},te=Object.freeze(ee),ne=Boolean(!0);if(l.A.canUseDOM){window.docusaurus=te;const e=document.getElementById("__docusaurus"),t=(0,f.jsx)(i.vd,{children:(0,f.jsx)(o.Kd,{children:(0,f.jsx)(V,{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},l=()=>{if(ne)r.startTransition((()=>{a.hydrateRoot(e,t,{onRecoverableError:n})}));else{const o=a.createRoot(e,{onRecoverableError:n});r.startTransition((()=>{o.render(t)}))}};I(window.location.pathname).then(l)}},6988:(e,t,n)=>{"use strict";n.d(t,{o:()=>d,l:()=>p});var r=n(6540),a=n(4784);const o=JSON.parse('{"docusaurus-plugin-google-gtag":{"default":{"trackingID":["G-62D63SY3S0"],"anonymizeIP":false,"id":"default"}},"docusaurus-lunr-search":{"default":{"fileNames":{"searchDoc":"search-doc-1709136442943.json","lunrIndex":"lunr-index-1709136442943.json"}}},"docusaurus-plugin-content-docs":{"default":{"path":"/","versions":[{"name":"current","label":"Next","isLast":true,"path":"/","mainDocId":"quickstart","docs":[{"id":"adapters","path":"/adapters.html"},{"id":"alternatives","path":"/alternatives.html","sidebar":"tutorialSidebar"},{"id":"articles/angular-database","path":"/articles/angular-database.html","sidebar":"tutorialSidebar"},{"id":"articles/browser-database","path":"/articles/browser-database.html","sidebar":"tutorialSidebar"},{"id":"articles/browser-storage","path":"/articles/browser-storage.html","sidebar":"tutorialSidebar"},{"id":"articles/data-base","path":"/articles/data-base.html","sidebar":"tutorialSidebar"},{"id":"articles/embedded-database","path":"/articles/embedded-database.html","sidebar":"tutorialSidebar"},{"id":"articles/flutter-database","path":"/articles/flutter-database.html","sidebar":"tutorialSidebar"},{"id":"articles/frontend-database","path":"/articles/frontend-database.html","sidebar":"tutorialSidebar"},{"id":"articles/in-memory-nosql-database","path":"/articles/in-memory-nosql-database.html","sidebar":"tutorialSidebar"},{"id":"articles/ionic-database","path":"/articles/ionic-database.html","sidebar":"tutorialSidebar"},{"id":"articles/json-database","path":"/articles/json-database.html","sidebar":"tutorialSidebar"},{"id":"articles/localstorage","path":"/articles/localstorage.html","sidebar":"tutorialSidebar"},{"id":"articles/mobile-database","path":"/articles/mobile-database.html","sidebar":"tutorialSidebar"},{"id":"articles/progressive-web-app-database","path":"/articles/progressive-web-app-database.html","sidebar":"tutorialSidebar"},{"id":"articles/react-database","path":"/articles/react-database.html","sidebar":"tutorialSidebar"},{"id":"articles/realtime-database","path":"/articles/realtime-database.html","sidebar":"tutorialSidebar"},{"id":"backup","path":"/backup.html","sidebar":"tutorialSidebar"},{"id":"capacitor-database","path":"/capacitor-database.html","sidebar":"tutorialSidebar"},{"id":"cleanup","path":"/cleanup.html","sidebar":"tutorialSidebar"},{"id":"contribute","path":"/contribution.html","sidebar":"tutorialSidebar"},{"id":"crdt","path":"/crdt.html","sidebar":"tutorialSidebar"},{"id":"data-migration","path":"/data-migration"},{"id":"dev-mode","path":"/dev-mode.html","sidebar":"tutorialSidebar"},{"id":"downsides-of-offline-first","path":"/downsides-of-offline-first.html","sidebar":"tutorialSidebar"},{"id":"electron","path":"/electron.html","sidebar":"tutorialSidebar"},{"id":"electron-database","path":"/electron-database.html","sidebar":"tutorialSidebar"},{"id":"encryption","path":"/encryption.html","sidebar":"tutorialSidebar"},{"id":"install","path":"/install.html","sidebar":"tutorialSidebar"},{"id":"key-compression","path":"/key-compression.html","sidebar":"tutorialSidebar"},{"id":"leader-election","path":"/leader-election.html","sidebar":"tutorialSidebar"},{"id":"logger","path":"/logger.html","sidebar":"tutorialSidebar"},{"id":"middleware","path":"/middleware.html","sidebar":"tutorialSidebar"},{"id":"migration-schema","path":"/migration-schema.html","sidebar":"tutorialSidebar"},{"id":"migration-storage","path":"/migration-storage.html","sidebar":"tutorialSidebar"},{"id":"nodejs-database","path":"/nodejs-database.html","sidebar":"tutorialSidebar"},{"id":"nosql-performance-tips","path":"/nosql-performance-tips.html","sidebar":"tutorialSidebar"},{"id":"offline-first","path":"/offline-first.html","sidebar":"tutorialSidebar"},{"id":"orm","path":"/orm.html","sidebar":"tutorialSidebar"},{"id":"plugins","path":"/plugins.html","sidebar":"tutorialSidebar"},{"id":"population","path":"/population.html","sidebar":"tutorialSidebar"},{"id":"query-cache","path":"/query-cache.html","sidebar":"tutorialSidebar"},{"id":"query-optimizer","path":"/query-optimizer.html","sidebar":"tutorialSidebar"},{"id":"questions-answers","path":"/questions-answers.html","sidebar":"tutorialSidebar"},{"id":"quickstart","path":"/quickstart.html","sidebar":"tutorialSidebar"},{"id":"react-native-database","path":"/react-native-database.html","sidebar":"tutorialSidebar"},{"id":"reactivity","path":"/reactivity.html","sidebar":"tutorialSidebar"},{"id":"releases/10.0.0","path":"/releases/10.0.0.html","sidebar":"tutorialSidebar"},{"id":"releases/11.0.0","path":"/releases/11.0.0.html","sidebar":"tutorialSidebar"},{"id":"releases/12.0.0","path":"/releases/12.0.0.html","sidebar":"tutorialSidebar"},{"id":"releases/13.0.0","path":"/releases/13.0.0.html","sidebar":"tutorialSidebar"},{"id":"releases/14.0.0","path":"/releases/14.0.0.html","sidebar":"tutorialSidebar"},{"id":"releases/15.0.0","path":"/releases/15.0.0.html","sidebar":"tutorialSidebar"},{"id":"releases/8.0.0","path":"/releases/8.0.0.html","sidebar":"tutorialSidebar"},{"id":"releases/9.0.0","path":"/releases/9.0.0.html","sidebar":"tutorialSidebar"},{"id":"replication","path":"/replication.html","sidebar":"tutorialSidebar"},{"id":"replication-couchdb","path":"/replication-couchdb.html","sidebar":"tutorialSidebar"},{"id":"replication-firestore","path":"/replication-firestore.html","sidebar":"tutorialSidebar"},{"id":"replication-graphql","path":"/replication-graphql.html","sidebar":"tutorialSidebar"},{"id":"replication-http","path":"/replication-http.html","sidebar":"tutorialSidebar"},{"id":"replication-nats","path":"/replication-nats.html","sidebar":"tutorialSidebar"},{"id":"replication-p2p","path":"/replication-p2p.html"},{"id":"replication-server","path":"/replication-server","sidebar":"tutorialSidebar"},{"id":"replication-webrtc","path":"/replication-webrtc.html","sidebar":"tutorialSidebar"},{"id":"replication-websocket","path":"/replication-websocket.html","sidebar":"tutorialSidebar"},{"id":"rx-attachment","path":"/rx-attachment.html","sidebar":"tutorialSidebar"},{"id":"rx-collection","path":"/rx-collection.html","sidebar":"tutorialSidebar"},{"id":"rx-database","path":"/rx-database.html","sidebar":"tutorialSidebar"},{"id":"rx-document","path":"/rx-document.html","sidebar":"tutorialSidebar"},{"id":"rx-local-document","path":"/rx-local-document.html","sidebar":"tutorialSidebar"},{"id":"rx-query","path":"/rx-query.html","sidebar":"tutorialSidebar"},{"id":"rx-schema","path":"/rx-schema.html","sidebar":"tutorialSidebar"},{"id":"rx-server","path":"/rx-server.html","sidebar":"tutorialSidebar"},{"id":"rx-server-scaling","path":"/rx-server-scaling.html","sidebar":"tutorialSidebar"},{"id":"rx-storage","path":"/rx-storage.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-denokv","path":"/rx-storage-denokv.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-dexie","path":"/rx-storage-dexie.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-filesystem-node","path":"/rx-storage-filesystem-node.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-foundationdb","path":"/rx-storage-foundationdb.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-indexeddb","path":"/rx-storage-indexeddb.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-localstorage-meta-optimizer","path":"/rx-storage-localstorage-meta-optimizer.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-lokijs","path":"/rx-storage-lokijs.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-memory","path":"/rx-storage-memory.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-memory-synced","path":"/rx-storage-memory-synced.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-mongodb","path":"/rx-storage-mongodb.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-opfs","path":"/rx-storage-opfs.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-performance","path":"/rx-storage-performance.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-pouchdb","path":"/rx-storage-pouchdb.html"},{"id":"rx-storage-remote","path":"/rx-storage-remote.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-sharding","path":"/rx-storage-sharding.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-shared-worker","path":"/rx-storage-shared-worker.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-sqlite","path":"/rx-storage-sqlite.html","sidebar":"tutorialSidebar"},{"id":"rx-storage-worker","path":"/rx-storage-worker.html","sidebar":"tutorialSidebar"},{"id":"rxdb-tradeoffs","path":"/rxdb-tradeoffs.html"},{"id":"schema-validation","path":"/schema-validation.html"},{"id":"slow-indexeddb","path":"/slow-indexeddb.html","sidebar":"tutorialSidebar"},{"id":"third-party-plugins","path":"/third-party-plugins.html","sidebar":"tutorialSidebar"},{"id":"transactions-conflicts-revisions","path":"/transactions-conflicts-revisions.html","sidebar":"tutorialSidebar"},{"id":"tutorials/typescript","path":"/tutorials/typescript.html","sidebar":"tutorialSidebar"},{"id":"why-nosql","path":"/why-nosql.html","sidebar":"tutorialSidebar"}],"draftIds":[],"sidebars":{"tutorialSidebar":{"link":{"path":"/quickstart.html","label":"quickstart"}}}}],"breadcrumbs":true}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"}}}');var l=n(2654);const s=JSON.parse('{"docusaurusVersion":"3.0.1","siteVersion":"0.0.0","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.0.1"},"docusaurus-plugin-content-blog":{"type":"package","name":"@docusaurus/plugin-content-blog","version":"3.0.1"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.0.1"},"docusaurus-plugin-google-gtag":{"type":"package","name":"@docusaurus/plugin-google-gtag","version":"3.0.1"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.0.1"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.0.1"},"docusaurus-lunr-search":{"type":"package","name":"docusaurus-lunr-search","version":"3.3.1"}}}');var c=n(4848);const u={siteConfig:a.default,siteMetadata:s,globalData:o,i18n:i,codeTranslations:l},d=r.createContext(u);function p(e){let{children:t}=e;return(0,c.jsx)(d.Provider,{value:u,children:t})}},7489:(e,t,n)=>{"use strict";n.d(t,{A:()=>f});var r=n(6540),a=n(8193),o=n(5260),i=n(440),l=n(1595),s=n(4848);function c(e){let{error:t,tryAgain:n}=e;return(0,s.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,s.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,s.jsx)("button",{type:"button",onClick:n,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,s.jsx)(u,{error:t})]})}function u(e){let{error:t}=e;const n=(0,i.getErrorCausalChain)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,s.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:n})}function d(e){let{error:t,tryAgain:n}=e;return(0,s.jsxs)(f,{fallback:()=>(0,s.jsx)(c,{error:t,tryAgain:n}),children:[(0,s.jsx)(o.A,{children:(0,s.jsx)("title",{children:"Page Error"})}),(0,s.jsx)(l.A,{children:(0,s.jsx)(c,{error:t,tryAgain:n})})]})}const p=e=>(0,s.jsx)(d,{...e});class f extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){a.A.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??p)(e)}return e??null}}},8193:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});const r="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,a={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},5260:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});n(6540);var r=n(545),a=n(4848);function o(e){return(0,a.jsx)(r.mg,{...e})}},5489:(e,t,n)=>{"use strict";n.d(t,{A:()=>f});var r=n(6540),a=n(4625),o=n(440),i=n(4586),l=n(6654),s=n(8193),c=n(4848);const u=r.createContext({collectLink:()=>{}});var d=n(6025);function p(e,t){let{isNavLink:n,to:p,href:f,activeClassName:m,isActive:h,"data-noBrokenLinkCheck":g,autoAddBaseUrl:b=!0,...y}=e;const{siteConfig:{trailingSlash:v,baseUrl:w}}=(0,i.A)(),{withBaseUrl:x}=(0,d.h)(),k=(0,r.useContext)(u),S=(0,r.useRef)(null);(0,r.useImperativeHandle)(t,(()=>S.current));const E=p||f;const _=(0,l.A)(E),C=E?.replace("pathname://","");let T=void 0!==C?(A=C,b&&(e=>e.startsWith("/"))(A)?x(A):A):void 0;var A;T&&_&&(T=(0,o.applyTrailingSlash)(T,{trailingSlash:v,baseUrl:w}));const j=(0,r.useRef)(!1),N=n?a.k2:a.N_,R=s.A.canUseIntersectionObserver,L=(0,r.useRef)(),P=()=>{j.current||null==T||(window.docusaurus.preload(T),j.current=!0)};(0,r.useEffect)((()=>(!R&&_&&null!=T&&window.docusaurus.prefetch(T),()=>{R&&L.current&&L.current.disconnect()})),[L,T,R,_]);const O=T?.startsWith("#")??!1,I=!T||!_||O;return I||g||k.collectLink(T),I?(0,c.jsx)("a",{ref:S,href:T,...E&&!_&&{target:"_blank",rel:"noopener noreferrer"},...y}):(0,c.jsx)(N,{...y,onMouseEnter:P,onTouchStart:P,innerRef:e=>{S.current=e,R&&e&&_&&(L.current=new window.IntersectionObserver((t=>{t.forEach((t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(L.current.unobserve(e),L.current.disconnect(),null!=T&&window.docusaurus.prefetch(T))}))})),L.current.observe(e))},to:T,...n&&{isActive:h,activeClassName:m}})}const f=r.forwardRef(p)},1312:(e,t,n)=>{"use strict";n.d(t,{A:()=>c,T:()=>s});var r=n(6540),a=n(4848);function o(e,t){const n=e.split(/(\{\w+\})/).map(((e,n)=>{if(n%2==1){const n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e}));return n.some((e=>(0,r.isValidElement)(e)))?n.map(((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e)).filter((e=>""!==e)):n.join("")}var i=n(2654);function l(e){let{id:t,message:n}=e;if(void 0===t&&void 0===n)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return i[t??n]??n??t}function s(e,t){let{message:n,id:r}=e;return o(l({message:n,id:r}),t)}function c(e){let{children:t,id:n,values:r}=e;if(t&&"string"!=typeof t)throw console.warn("Illegal children",t),new Error("The Docusaurus component only accept simple string values");const i=l({message:t,id:n});return(0,a.jsx)(a.Fragment,{children:o(i,r)})}},7065:(e,t,n)=>{"use strict";n.d(t,{W:()=>r});const r="default"},6654:(e,t,n)=>{"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function a(e){return void 0!==e&&!r(e)}n.d(t,{A:()=>a,z:()=>r})},6025:(e,t,n)=>{"use strict";n.d(t,{A:()=>l,h:()=>i});var r=n(6540),a=n(4586),o=n(6654);function i(){const{siteConfig:{baseUrl:e,url:t}}=(0,a.A)(),n=(0,r.useCallback)(((n,r)=>function(e,t,n,r){let{forcePrependBaseUrl:a=!1,absolute:i=!1}=void 0===r?{}:r;if(!n||n.startsWith("#")||(0,o.z)(n))return n;if(a)return t+n.replace(/^\//,"");if(n===t.replace(/\/$/,""))return t;const l=n.startsWith(t)?n:t+n.replace(/^\//,"");return i?e+l:l}(t,e,n,r)),[t,e]);return{withBaseUrl:n}}function l(e,t){void 0===t&&(t={});const{withBaseUrl:n}=i();return n(e,t)}},4586:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(6540),a=n(6988);function o(){return(0,r.useContext)(a.o)}},6588:(e,t,n)=>{"use strict";n.d(t,{P_:()=>i,kh:()=>o});var r=n(4586),a=n(7065);function o(e,t){void 0===t&&(t={});const n=function(){const{globalData:e}=(0,r.A)();return e}()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}function i(e,t,n){void 0===t&&(t=a.W),void 0===n&&(n={});const r=o(e),i=r?.[t];if(!i&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return i}},2303:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(6540),a=n(6125);function o(){return(0,r.useContext)(a.o)}},205:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});var r=n(6540);const a=n(8193).A.canUseDOM?r.useLayoutEffect:r.useEffect},6921:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});const r=e=>"object"==typeof e&&!!e&&Object.keys(e).length>0;function a(e){const t={};return function e(n,a){Object.entries(n).forEach((n=>{let[o,i]=n;const l=a?`${a}.${o}`:o;r(i)?e(i,l):t[l]=i}))}(e),t}},3102:(e,t,n)=>{"use strict";n.d(t,{W:()=>i,o:()=>o});var r=n(6540),a=n(4848);const o=r.createContext(null);function i(e){let{children:t,value:n}=e;const i=r.useContext(o),l=(0,r.useMemo)((()=>function(e){let{parent:t,value:n}=e;if(!t){if(!n)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in n))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return n}const r={...t.data,...n?.data};return{plugin:t.plugin,data:r}}({parent:i,value:n})),[i,n]);return(0,a.jsx)(o.Provider,{value:l,children:t})}},8295:(e,t,n)=>{"use strict";n.d(t,{zK:()=>f,vT:()=>u,Gy:()=>s,HW:()=>m,ht:()=>c,r7:()=>p,jh:()=>d});var r=n(6347),a=n(6588);const o=e=>e.versions.find((e=>e.isLast));function i(e,t){const n=function(e,t){const n=o(e);return[...e.versions.filter((e=>e!==n)),n].find((e=>!!(0,r.B6)(t,{path:e.path,exact:!1,strict:!1})))}(e,t),a=n?.docs.find((e=>!!(0,r.B6)(t,{path:e.path,exact:!0,strict:!1})));return{activeVersion:n,activeDoc:a,alternateDocVersions:a?function(t){const n={};return e.versions.forEach((e=>{e.docs.forEach((r=>{r.id===t&&(n[e.name]=r)}))})),n}(a.id):{}}}const l={},s=()=>(0,a.kh)("docusaurus-plugin-content-docs")??l,c=e=>(0,a.P_)("docusaurus-plugin-content-docs",e,{failfast:!0});function u(e){void 0===e&&(e={});const t=s(),{pathname:n}=(0,r.zy)();return function(e,t,n){void 0===n&&(n={});const a=Object.entries(e).sort(((e,t)=>t[1].path.localeCompare(e[1].path))).find((e=>{let[,n]=e;return!!(0,r.B6)(t,{path:n.path,exact:!1,strict:!1})})),o=a?{pluginId:a[0],pluginData:a[1]}:void 0;if(!o&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map((e=>e.path)).join(", ")}`);return o}(t,n,e)}function d(e){return c(e).versions}function p(e){const t=c(e);return o(t)}function f(e){const t=c(e),{pathname:n}=(0,r.zy)();return i(t,n)}function m(e){const t=c(e),{pathname:n}=(0,r.zy)();return function(e,t){const n=o(e);return{latestDocSuggestion:i(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},1911:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r={onRouteDidUpdate(e){let{location:t,previousLocation:n}=e;!n||t.pathname===n.pathname&&t.search===n.search&&t.hash===n.hash||setTimeout((()=>{window.gtag("set","page_path",t.pathname+t.search+t.hash),window.gtag("event","page_view")}))}}},6294:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(5947),a=n.n(r);a().configure({showSpinner:!1});const o={onRouteUpdate(e){let{location:t,previousLocation:n}=e;if(n&&t.pathname!==n.pathname){const e=window.setTimeout((()=>{a().start()}),200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){a().done()}}},6134:(e,t,n)=>{"use strict";n.r(t);var r=n(1765),a=n(4784);!function(e){const{themeConfig:{prism:t}}=a.default,{additionalLanguages:r}=t;globalThis.Prism=e,r.forEach((e=>{"php"===e&&n(9700),n(8692)(`./prism-${e}`)})),delete globalThis.Prism}(r.My)},1107:(e,t,n)=>{"use strict";n.d(t,{A:()=>c});n(6540);var r=n(4164),a=n(1312),o=n(6342),i=n(5489);const l={anchorWithStickyNavbar:"anchorWithStickyNavbar_LWe7",anchorWithHideOnScrollNavbar:"anchorWithHideOnScrollNavbar_WYt5"};var s=n(4848);function c(e){let{as:t,id:n,...c}=e;const{navbar:{hideOnScroll:u}}=(0,o.p)();if("h1"===t||!n)return(0,s.jsx)(t,{...c,id:void 0});const d=(0,a.T)({id:"theme.common.headingLinkTitle",message:"Direct link to {heading}",description:"Title for link to heading"},{heading:"string"==typeof c.children?c.children:n});return(0,s.jsxs)(t,{...c,className:(0,r.A)("anchor",u?l.anchorWithHideOnScrollNavbar:l.anchorWithStickyNavbar,c.className),id:n,children:[c.children,(0,s.jsx)(i.A,{className:"hash-link",to:`#${n}`,"aria-label":d,title:d,children:"\u200b"})]})}},3186:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});n(6540);const r={iconExternalLink:"iconExternalLink_nPIU"};var a=n(4848);function o(e){let{width:t=13.5,height:n=13.5}=e;return(0,a.jsx)("svg",{width:t,height:n,"aria-hidden":"true",viewBox:"0 0 24 24",className:r.iconExternalLink,children:(0,a.jsx)("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"})})}},1595:(e,t,n)=>{"use strict";n.d(t,{A:()=>bt});var r=n(6540),a=n(4164),o=n(7489),i=n(1003),l=n(6347),s=n(1312),c=n(5062),u=n(4848);const d="__docusaurus_skipToContent_fallback";function p(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}function f(){const e=(0,r.useRef)(null),{action:t}=(0,l.W6)(),n=(0,r.useCallback)((e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.getElementById(d);t&&p(t)}),[]);return(0,c.$)((n=>{let{location:r}=n;e.current&&!r.hash&&"PUSH"===t&&p(e.current)})),{containerRef:e,onClick:n}}const m=(0,s.T)({id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",message:"Skip to main content"});function h(e){const t=e.children??m,{containerRef:n,onClick:r}=f();return(0,u.jsx)("div",{ref:n,role:"region","aria-label":m,children:(0,u.jsx)("a",{...e,href:`#${d}`,onClick:r,children:t})})}var g=n(7559),b=n(4090);const y={skipToContent:"skipToContent_fXgn"};function v(){return(0,u.jsx)(h,{className:y.skipToContent})}var w=n(6342),x=n(5041);function k(e){let{width:t=21,height:n=21,color:r="currentColor",strokeWidth:a=1.2,className:o,...i}=e;return(0,u.jsx)("svg",{viewBox:"0 0 15 15",width:t,height:n,...i,children:(0,u.jsx)("g",{stroke:r,strokeWidth:a,children:(0,u.jsx)("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})})})}const S={closeButton:"closeButton_CVFx"};function E(e){return(0,u.jsx)("button",{type:"button","aria-label":(0,s.T)({id:"theme.AnnouncementBar.closeButtonAriaLabel",message:"Close",description:"The ARIA label for close button of announcement bar"}),...e,className:(0,a.A)("clean-btn close",S.closeButton,e.className),children:(0,u.jsx)(k,{width:14,height:14,strokeWidth:3.1})})}const _={content:"content_knG7"};function C(e){const{announcementBar:t}=(0,w.p)(),{content:n}=t;return(0,u.jsx)("div",{...e,className:(0,a.A)(_.content,e.className),dangerouslySetInnerHTML:{__html:n}})}const T={announcementBar:"announcementBar_mb4j",announcementBarPlaceholder:"announcementBarPlaceholder_vyr4",announcementBarClose:"announcementBarClose_gvF7",announcementBarContent:"announcementBarContent_xLdY"};function A(){const{announcementBar:e}=(0,w.p)(),{isActive:t,close:n}=(0,x.Mj)();if(!t)return null;const{backgroundColor:r,textColor:a,isCloseable:o}=e;return(0,u.jsxs)("div",{className:T.announcementBar,style:{backgroundColor:r,color:a},role:"banner",children:[o&&(0,u.jsx)("div",{className:T.announcementBarPlaceholder}),(0,u.jsx)(C,{className:T.announcementBarContent}),o&&(0,u.jsx)(E,{onClick:n,className:T.announcementBarClose})]})}var j=n(9876),N=n(3104);var R=n(9532),L=n(5600);const P=r.createContext(null);function O(e){let{children:t}=e;const n=function(){const e=(0,j.M)(),t=(0,L.YL)(),[n,a]=(0,r.useState)(!1),o=null!==t.component,i=(0,R.ZC)(o);return(0,r.useEffect)((()=>{o&&!i&&a(!0)}),[o,i]),(0,r.useEffect)((()=>{o?e.shown||a(!0):a(!1)}),[e.shown,o]),(0,r.useMemo)((()=>[n,a]),[n])}();return(0,u.jsx)(P.Provider,{value:n,children:t})}function I(e){if(e.component){const t=e.component;return(0,u.jsx)(t,{...e.props})}}function D(){const e=(0,r.useContext)(P);if(!e)throw new R.dV("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,a=(0,r.useCallback)((()=>n(!1)),[n]),o=(0,L.YL)();return(0,r.useMemo)((()=>({shown:t,hide:a,content:I(o)})),[a,o,t])}function M(e){let{header:t,primaryMenu:n,secondaryMenu:r}=e;const{shown:o}=D();return(0,u.jsxs)("div",{className:"navbar-sidebar",children:[t,(0,u.jsxs)("div",{className:(0,a.A)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":o}),children:[(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:n}),(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:r})]})]})}var F=n(5293),z=n(2303);function B(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"})})}function $(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"})})}const U={toggle:"toggle_vylO",toggleButton:"toggleButton_gllP",darkToggleIcon:"darkToggleIcon_wfgR",lightToggleIcon:"lightToggleIcon_pyhR",toggleButtonDisabled:"toggleButtonDisabled_aARS"};function q(e){let{className:t,buttonClassName:n,value:r,onChange:o}=e;const i=(0,z.A)(),l=(0,s.T)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===r?(0,s.T)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,s.T)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return(0,u.jsx)("div",{className:(0,a.A)(U.toggle,t),children:(0,u.jsxs)("button",{className:(0,a.A)("clean-btn",U.toggleButton,!i&&U.toggleButtonDisabled,n),type:"button",onClick:()=>o("dark"===r?"light":"dark"),disabled:!i,title:l,"aria-label":l,"aria-live":"polite",children:[(0,u.jsx)(B,{className:(0,a.A)(U.toggleIcon,U.lightToggleIcon)}),(0,u.jsx)($,{className:(0,a.A)(U.toggleIcon,U.darkToggleIcon)})]})})}const H=r.memo(q),G={darkNavbarColorModeToggle:"darkNavbarColorModeToggle_X3D1"};function W(e){let{className:t}=e;const n=(0,w.p)().navbar.style,r=(0,w.p)().colorMode.disableSwitch,{colorMode:a,setColorMode:o}=(0,F.G)();return r?null:(0,u.jsx)(H,{className:t,buttonClassName:"dark"===n?G.darkNavbarColorModeToggle:void 0,value:a,onChange:o})}var V=n(3465);function Q(){return(0,u.jsx)(V.A,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}function K(){const e=(0,j.M)();return(0,u.jsx)("button",{type:"button","aria-label":(0,s.T)({id:"theme.docs.sidebar.closeSidebarButtonAriaLabel",message:"Close navigation bar",description:"The ARIA label for close button of mobile sidebar"}),className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle(),children:(0,u.jsx)(k,{color:"var(--ifm-color-emphasis-600)"})})}function Y(){return(0,u.jsxs)("div",{className:"navbar-sidebar__brand",children:[(0,u.jsx)(Q,{}),(0,u.jsx)(W,{className:"margin-right--md"}),(0,u.jsx)(K,{})]})}var J=n(5489),Z=n(6025),X=n(6654);function ee(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}var te=n(3186);function ne(e){let{activeBasePath:t,activeBaseRegex:n,to:r,href:a,label:o,html:i,isDropdownLink:l,prependBaseUrlToHref:s,...c}=e;const d=(0,Z.A)(r),p=(0,Z.A)(t),f=(0,Z.A)(a,{forcePrependBaseUrl:!0}),m=o&&a&&!(0,X.A)(a),h=i?{dangerouslySetInnerHTML:{__html:i}}:{children:(0,u.jsxs)(u.Fragment,{children:[o,m&&(0,u.jsx)(te.A,{...l&&{width:12,height:12}})]})};return a?(0,u.jsx)(J.A,{href:s?f:a,...c,...h}):(0,u.jsx)(J.A,{to:d,isNavLink:!0,...(t||n)&&{isActive:(e,t)=>n?ee(n,t.pathname):t.pathname.startsWith(p)},...c,...h})}function re(e){let{className:t,isDropdownItem:n=!1,...r}=e;const o=(0,u.jsx)(ne,{className:(0,a.A)(n?"dropdown__link":"navbar__item navbar__link",t),isDropdownLink:n,...r});return n?(0,u.jsx)("li",{children:o}):o}function ae(e){let{className:t,isDropdownItem:n,...r}=e;return(0,u.jsx)("li",{className:"menu__list-item",children:(0,u.jsx)(ne,{className:(0,a.A)("menu__link",t),...r})})}function oe(e){let{mobile:t=!1,position:n,...r}=e;const a=t?ae:re;return(0,u.jsx)(a,{...r,activeClassName:r.activeClassName??(t?"menu__link--active":"navbar__link--active")})}var ie=n(1422),le=n(9169),se=n(4586);const ce={dropdownNavbarItemMobile:"dropdownNavbarItemMobile_S0Fm"};function ue(e,t){return e.some((e=>function(e,t){return!!(0,le.ys)(e.to,t)||!!ee(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t)))}function de(e){let{items:t,position:n,className:o,onClick:i,...l}=e;const s=(0,r.useRef)(null),[c,d]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{const e=e=>{s.current&&!s.current.contains(e.target)&&d(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),document.addEventListener("focusin",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e),document.removeEventListener("focusin",e)}}),[s]),(0,u.jsxs)("div",{ref:s,className:(0,a.A)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===n,"dropdown--show":c}),children:[(0,u.jsx)(ne,{"aria-haspopup":"true","aria-expanded":c,role:"button",href:l.to?void 0:"#",className:(0,a.A)("navbar__link",o),...l,onClick:l.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),d(!c))},children:l.children??l.label}),(0,u.jsx)("ul",{className:"dropdown__menu",children:t.map(((e,t)=>(0,r.createElement)(je,{isDropdownItem:!0,activeClassName:"dropdown__link--active",...e,key:t})))})]})}function pe(e){let{items:t,className:n,position:o,onClick:i,...s}=e;const c=function(){const{siteConfig:{baseUrl:e}}=(0,se.A)(),{pathname:t}=(0,l.zy)();return t.replace(e,"/")}(),d=ue(t,c),{collapsed:p,toggleCollapsed:f,setCollapsed:m}=(0,ie.u)({initialState:()=>!d});return(0,r.useEffect)((()=>{d&&m(!d)}),[c,d,m]),(0,u.jsxs)("li",{className:(0,a.A)("menu__list-item",{"menu__list-item--collapsed":p}),children:[(0,u.jsx)(ne,{role:"button",className:(0,a.A)(ce.dropdownNavbarItemMobile,"menu__link menu__link--sublist menu__link--sublist-caret",n),...s,onClick:e=>{e.preventDefault(),f()},children:s.children??s.label}),(0,u.jsx)(ie.N,{lazy:!0,as:"ul",className:"menu__list",collapsed:p,children:t.map(((e,t)=>(0,r.createElement)(je,{mobile:!0,isDropdownItem:!0,onClick:i,activeClassName:"menu__link--active",...e,key:t})))})]})}function fe(e){let{mobile:t=!1,...n}=e;const r=t?pe:de;return(0,u.jsx)(r,{...n})}var me=n(2131);function he(e){let{width:t=20,height:n=20,...r}=e;return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:t,height:n,"aria-hidden":!0,...r,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"})})}const ge="iconLanguage_nlXk";var be=n(6588),ye=n(689),ve=n.n(ye);function we(){const e=(0,l.zy)(),t=(0,l.W6)(),{siteConfig:{baseUrl:n}}=(0,se.A)(),[a,o]=(0,r.useState)({wordToHighlight:"",isTitleSuggestion:!1,titleText:""});return(0,r.useEffect)((()=>{if(!e.state?.highlightState||0===e.state.highlightState.wordToHighlight.length)return;o(e.state.highlightState);const{highlightState:n,...r}=e.state;t.replace({...e,state:r})}),[e.state?.highlightState,t,e]),(0,r.useEffect)((()=>{if(0===a.wordToHighlight.length)return;const e=document.getElementsByTagName("article")[0]??document.getElementsByTagName("main")[0];if(!e)return;const t=new(ve())(e),n={ignoreJoiners:!0};return t.mark(a.wordToHighlight,n),()=>t.unmark(n)}),[a,n]),null}const xe=e=>{const t=(0,r.useRef)(!1),o=(0,r.useRef)(null),[i,s]=(0,r.useState)(!1),c=(0,l.W6)(),{siteConfig:d={}}=(0,se.A)(),p=(d.plugins||[]).find((e=>Array.isArray(e)&&"string"==typeof e[0]&&e[0].includes("docusaurus-lunr-search"))),f=(0,z.A)(),{baseUrl:m}=d,h=p&&p[1]?.assetUrl||m,g=(0,be.P_)("docusaurus-lunr-search"),b=()=>{t.current||(Promise.all([fetch(`${h}${g.fileNames.searchDoc}`).then((e=>e.json())),fetch(`${h}${g.fileNames.lunrIndex}`).then((e=>e.json())),Promise.all([n.e(4250),n.e(7443)]).then(n.bind(n,4004)),Promise.all([n.e(1869),n.e(9187)]).then(n.bind(n,9187))]).then((e=>{let[t,n,{default:r}]=e;const{searchDocs:a,options:o}=t;a&&0!==a.length&&(((e,t,n,r)=>{new n({searchDocs:e,searchIndex:t,baseUrl:m,inputSelector:"#search_input_react",handleSelected:(e,t,n)=>{const a=n.url||"/";document.createElement("a").href=a,e.setVal(""),t.target.blur();let o="";if(r.highlightResult)try{const e=(n.text||n.subcategory||n.title).match(new RegExp("\\w*","g"));if(e&&e.length>0){const t=document.createElement("div");t.innerHTML=e[0],o=t.textContent}}catch(i){console.log(i)}c.push(a,{highlightState:{wordToHighlight:o}})},maxHits:r.maxHits})})(a,n,r,o),s(!0))})),t.current=!0)},y=(0,r.useCallback)((t=>{o.current.contains(t.target)||o.current.focus(),e.handleSearchBarToggle&&e.handleSearchBarToggle(!e.isSearchBarExpanded)}),[e.isSearchBarExpanded]);let v;return f&&(b(),v=window.navigator.platform.startsWith("Mac")?"Search \u2318+K":"Search Ctrl+K"),(0,u.jsxs)("div",{className:"navbar__search",children:[(0,u.jsx)("span",{"aria-label":"expand searchbar",role:"button",className:(0,a.A)("search-icon",{"search-icon-hidden":e.isSearchBarExpanded}),onClick:y,onKeyDown:y,tabIndex:0}),(0,u.jsx)("input",{id:"search_input_react",type:"search",placeholder:i?v:"Loading...","aria-label":"Search",className:(0,a.A)("navbar__search-input",{"search-bar-expanded":e.isSearchBarExpanded},{"search-bar":!e.isSearchBarExpanded}),onClick:b,onMouseOver:b,onFocus:y,onBlur:y,ref:o,disabled:!i}),(0,u.jsx)(we,{})]},"search-box")},ke={navbarSearchContainer:"navbarSearchContainer_Bca1"};function Se(e){let{children:t,className:n}=e;return(0,u.jsx)("div",{className:(0,a.A)(n,ke.navbarSearchContainer),children:t})}var Ee=n(8295),_e=n(1754);var Ce=n(5597);const Te=e=>e.docs.find((t=>t.id===e.mainDocId));const Ae={default:oe,localeDropdown:function(e){let{mobile:t,dropdownItemsBefore:n,dropdownItemsAfter:r,queryString:a="",...o}=e;const{i18n:{currentLocale:i,locales:c,localeConfigs:d}}=(0,se.A)(),p=(0,me.o)(),{search:f,hash:m}=(0,l.zy)(),h=[...n,...c.map((e=>{const n=`${`pathname://${p.createUrl({locale:e,fullyQualified:!1})}`}${f}${m}${a}`;return{label:d[e].label,lang:d[e].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:e===i?t?"menu__link--active":"dropdown__link--active":""}})),...r],g=t?(0,s.T)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):d[i].label;return(0,u.jsx)(fe,{...o,mobile:t,label:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(he,{className:ge}),g]}),items:h})},search:function(e){let{mobile:t,className:n}=e;return t?null:(0,u.jsx)(Se,{className:n,children:(0,u.jsx)(xe,{})})},dropdown:fe,html:function(e){let{value:t,className:n,mobile:r=!1,isDropdownItem:o=!1}=e;const i=o?"li":"div";return(0,u.jsx)(i,{className:(0,a.A)({navbar__item:!r&&!o,"menu__list-item":r},n),dangerouslySetInnerHTML:{__html:t}})},doc:function(e){let{docId:t,label:n,docsPluginId:r,...a}=e;const{activeDoc:o}=(0,Ee.zK)(r),i=(0,_e.QB)(t,r),l=o?.path===i?.path;return null===i||i.unlisted&&!l?null:(0,u.jsx)(oe,{exact:!0,...a,isActive:()=>l||!!o?.sidebar&&o.sidebar===i.sidebar,label:n??i.id,to:i.path})},docSidebar:function(e){let{sidebarId:t,label:n,docsPluginId:r,...a}=e;const{activeDoc:o}=(0,Ee.zK)(r),i=(0,_e.fW)(t,r).link;if(!i)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${t}" doesn't have anything to be linked to.`);return(0,u.jsx)(oe,{exact:!0,...a,isActive:()=>o?.sidebar===t,label:n??i.label,to:i.path})},docsVersion:function(e){let{label:t,to:n,docsPluginId:r,...a}=e;const o=(0,_e.Vd)(r)[0],i=t??o.label,l=n??(e=>e.docs.find((t=>t.id===e.mainDocId)))(o).path;return(0,u.jsx)(oe,{...a,label:i,to:l})},docsVersionDropdown:function(e){let{mobile:t,docsPluginId:n,dropdownActiveClassDisabled:r,dropdownItemsBefore:a,dropdownItemsAfter:o,...i}=e;const{search:c,hash:d}=(0,l.zy)(),p=(0,Ee.zK)(n),f=(0,Ee.jh)(n),{savePreferredVersionName:m}=(0,Ce.g1)(n),h=[...a,...f.map((e=>{const t=p.alternateDocVersions[e.name]??Te(e);return{label:e.label,to:`${t.path}${c}${d}`,isActive:()=>e===p.activeVersion,onClick:()=>m(e.name)}})),...o],g=(0,_e.Vd)(n)[0],b=t&&h.length>1?(0,s.T)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):g.label,y=t&&h.length>1?void 0:Te(g).path;return h.length<=1?(0,u.jsx)(oe,{...i,mobile:t,label:b,to:y,isActive:r?()=>!1:void 0}):(0,u.jsx)(fe,{...i,mobile:t,label:b,to:y,items:h,isActive:r?()=>!1:void 0})}};function je(e){let{type:t,...n}=e;const r=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(t,n),a=Ae[r];if(!a)throw new Error(`No NavbarItem component found for type "${t}".`);return(0,u.jsx)(a,{...n})}function Ne(){const e=(0,j.M)(),t=(0,w.p)().navbar.items;return(0,u.jsx)("ul",{className:"menu__list",children:t.map(((t,n)=>(0,r.createElement)(je,{mobile:!0,...t,onClick:()=>e.toggle(),key:n})))})}function Re(e){return(0,u.jsx)("button",{...e,type:"button",className:"clean-btn navbar-sidebar__back",children:(0,u.jsx)(s.A,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)",children:"\u2190 Back to main menu"})})}function Le(){const e=0===(0,w.p)().navbar.items.length,t=D();return(0,u.jsxs)(u.Fragment,{children:[!e&&(0,u.jsx)(Re,{onClick:()=>t.hide()}),t.content]})}function Pe(){const e=(0,j.M)();var t;return void 0===(t=e.shown)&&(t=!0),(0,r.useEffect)((()=>(document.body.style.overflow=t?"hidden":"visible",()=>{document.body.style.overflow="visible"})),[t]),e.shouldRender?(0,u.jsx)(M,{header:(0,u.jsx)(Y,{}),primaryMenu:(0,u.jsx)(Ne,{}),secondaryMenu:(0,u.jsx)(Le,{})}):null}const Oe={navbarHideable:"navbarHideable_m1mJ",navbarHidden:"navbarHidden_jGov"};function Ie(e){return(0,u.jsx)("div",{role:"presentation",...e,className:(0,a.A)("navbar-sidebar__backdrop",e.className)})}function De(e){let{children:t}=e;const{navbar:{hideOnScroll:n,style:o}}=(0,w.p)(),i=(0,j.M)(),{navbarRef:l,isNavbarVisible:d}=function(e){const[t,n]=(0,r.useState)(e),a=(0,r.useRef)(!1),o=(0,r.useRef)(0),i=(0,r.useCallback)((e=>{null!==e&&(o.current=e.getBoundingClientRect().height)}),[]);return(0,N.Mq)(((t,r)=>{let{scrollY:i}=t;if(!e)return;if(i=l?n(!1):i+c{if(!e)return;const r=t.location.hash;if(r?document.getElementById(r.substring(1)):void 0)return a.current=!0,void n(!1);n(!0)})),{navbarRef:i,isNavbarVisible:t}}(n);return(0,u.jsxs)("nav",{ref:l,"aria-label":(0,s.T)({id:"theme.NavBar.navAriaLabel",message:"Main",description:"The ARIA label for the main navigation"}),className:(0,a.A)("navbar","navbar--fixed-top",n&&[Oe.navbarHideable,!d&&Oe.navbarHidden],{"navbar--dark":"dark"===o,"navbar--primary":"primary"===o,"navbar-sidebar--show":i.shown}),children:[t,(0,u.jsx)(Ie,{onClick:i.toggle}),(0,u.jsx)(Pe,{})]})}var Me=n(440);const Fe={errorBoundaryError:"errorBoundaryError_a6uf",errorBoundaryFallback:"errorBoundaryFallback_VBag"};function ze(e){return(0,u.jsx)("button",{type:"button",...e,children:(0,u.jsx)(s.A,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again rendering when the React error boundary captures an error",children:"Try again"})})}function Be(e){let{error:t}=e;const n=(0,Me.getErrorCausalChain)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,u.jsx)("p",{className:Fe.errorBoundaryError,children:n})}class $e extends r.Component{componentDidCatch(e,t){throw this.props.onError(e,t)}render(){return this.props.children}}const Ue="right";function qe(e){let{width:t=30,height:n=30,className:r,...a}=e;return(0,u.jsx)("svg",{className:r,width:t,height:n,viewBox:"0 0 30 30","aria-hidden":"true",...a,children:(0,u.jsx)("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"})})}function He(){const{toggle:e,shown:t}=(0,j.M)();return(0,u.jsx)("button",{onClick:e,"aria-label":(0,s.T)({id:"theme.docs.sidebar.toggleSidebarButtonAriaLabel",message:"Toggle navigation bar",description:"The ARIA label for hamburger menu button of mobile navigation"}),"aria-expanded":t,className:"navbar__toggle clean-btn",type:"button",children:(0,u.jsx)(qe,{})})}const Ge={colorModeToggle:"colorModeToggle_DEke"};function We(e){let{items:t}=e;return(0,u.jsx)(u.Fragment,{children:t.map(((e,t)=>(0,u.jsx)($e,{onError:t=>new Error(`A theme navbar item failed to render.\nPlease double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:\n${JSON.stringify(e,null,2)}`,{cause:t}),children:(0,u.jsx)(je,{...e})},t)))})}function Ve(e){let{left:t,right:n}=e;return(0,u.jsxs)("div",{className:"navbar__inner",children:[(0,u.jsx)("div",{className:"navbar__items",children:t}),(0,u.jsx)("div",{className:"navbar__items navbar__items--right",children:n})]})}function Qe(){const e=(0,j.M)(),t=(0,w.p)().navbar.items,[n,r]=function(e){function t(e){return"left"===(e.position??Ue)}return[e.filter(t),e.filter((e=>!t(e)))]}(t),a=t.find((e=>"search"===e.type));return(0,u.jsx)(Ve,{left:(0,u.jsxs)(u.Fragment,{children:[!e.disabled&&(0,u.jsx)(He,{}),(0,u.jsx)(Q,{}),(0,u.jsx)(We,{items:n})]}),right:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(We,{items:r}),(0,u.jsx)(W,{className:Ge.colorModeToggle}),!a&&(0,u.jsx)(Se,{children:(0,u.jsx)(xe,{})})]})})}function Ke(){return(0,u.jsx)(De,{children:(0,u.jsx)(Qe,{})})}function Ye(e){let{item:t}=e;const{to:n,href:r,label:a,prependBaseUrlToHref:o,...i}=t,l=(0,Z.A)(n),s=(0,Z.A)(r,{forcePrependBaseUrl:!0});return(0,u.jsxs)(J.A,{className:"footer__link-item",...r?{href:o?s:r}:{to:l},...i,children:[a,r&&!(0,X.A)(r)&&(0,u.jsx)(te.A,{})]})}function Je(e){let{item:t}=e;return t.html?(0,u.jsx)("li",{className:"footer__item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)("li",{className:"footer__item",children:(0,u.jsx)(Ye,{item:t})},t.href??t.to)}function Ze(e){let{column:t}=e;return(0,u.jsxs)("div",{className:"col footer__col",children:[(0,u.jsx)("div",{className:"footer__title",children:t.title}),(0,u.jsx)("ul",{className:"footer__items clean-list",children:t.items.map(((e,t)=>(0,u.jsx)(Je,{item:e},t)))})]})}function Xe(e){let{columns:t}=e;return(0,u.jsx)("div",{className:"row footer__links",children:t.map(((e,t)=>(0,u.jsx)(Ze,{column:e},t)))})}function et(){return(0,u.jsx)("span",{className:"footer__link-separator",children:"\xb7"})}function tt(e){let{item:t}=e;return t.html?(0,u.jsx)("span",{className:"footer__link-item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)(Ye,{item:t})}function nt(e){let{links:t}=e;return(0,u.jsx)("div",{className:"footer__links text--center",children:(0,u.jsx)("div",{className:"footer__links",children:t.map(((e,n)=>(0,u.jsxs)(r.Fragment,{children:[(0,u.jsx)(tt,{item:e}),t.length!==n+1&&(0,u.jsx)(et,{})]},n)))})})}function rt(e){let{links:t}=e;return function(e){return"title"in e[0]}(t)?(0,u.jsx)(Xe,{columns:t}):(0,u.jsx)(nt,{links:t})}var at=n(1122);const ot={footerLogoLink:"footerLogoLink_BH7S"};function it(e){let{logo:t}=e;const{withBaseUrl:n}=(0,Z.h)(),r={light:n(t.src),dark:n(t.srcDark??t.src)};return(0,u.jsx)(at.A,{className:(0,a.A)("footer__logo",t.className),alt:t.alt,sources:r,width:t.width,height:t.height,style:t.style})}function lt(e){let{logo:t}=e;return t.href?(0,u.jsx)(J.A,{href:t.href,className:ot.footerLogoLink,target:t.target,children:(0,u.jsx)(it,{logo:t})}):(0,u.jsx)(it,{logo:t})}function st(e){let{copyright:t}=e;return(0,u.jsx)("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:t}})}function ct(e){let{style:t,links:n,logo:r,copyright:o}=e;return(0,u.jsx)("footer",{className:(0,a.A)("footer",{"footer--dark":"dark"===t}),children:(0,u.jsxs)("div",{className:"container container-fluid",children:[n,(r||o)&&(0,u.jsxs)("div",{className:"footer__bottom text--center",children:[r&&(0,u.jsx)("div",{className:"margin-bottom--sm",children:r}),o]})]})})}function ut(){const{footer:e}=(0,w.p)();if(!e)return null;const{copyright:t,links:n,logo:r,style:a}=e;return(0,u.jsx)(ct,{style:a,links:n&&n.length>0&&(0,u.jsx)(rt,{links:n}),logo:r&&(0,u.jsx)(lt,{logo:r}),copyright:t&&(0,u.jsx)(st,{copyright:t})})}const dt=r.memo(ut),pt=(0,R.fM)([F.a,x.oq,N.Tv,Ce.VQ,i.Jx,function(e){let{children:t}=e;return(0,u.jsx)(L.y_,{children:(0,u.jsx)(j.e,{children:(0,u.jsx)(O,{children:t})})})}]);function ft(e){let{children:t}=e;return(0,u.jsx)(pt,{children:t})}var mt=n(1107);function ht(e){let{error:t,tryAgain:n}=e;return(0,u.jsx)("main",{className:"container margin-vert--xl",children:(0,u.jsx)("div",{className:"row",children:(0,u.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,u.jsx)(mt.A,{as:"h1",className:"hero__title",children:(0,u.jsx)(s.A,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed",children:"This page crashed."})}),(0,u.jsx)("div",{className:"margin-vert--lg",children:(0,u.jsx)(ze,{onClick:n,className:"button button--primary shadow--lw"})}),(0,u.jsx)("hr",{}),(0,u.jsx)("div",{className:"margin-vert--md",children:(0,u.jsx)(Be,{error:t})})]})})})}const gt={mainWrapper:"mainWrapper_z2l0"};function bt(e){const{children:t,noFooter:n,wrapperClassName:r,title:l,description:s}=e;return(0,b.J)(),(0,u.jsxs)(ft,{children:[(0,u.jsx)(i.be,{title:l,description:s}),(0,u.jsx)(v,{}),(0,u.jsx)(A,{}),(0,u.jsx)(Ke,{}),(0,u.jsx)("div",{id:d,className:(0,a.A)(g.G.wrapper.main,gt.mainWrapper,r),children:(0,u.jsx)(o.A,{fallback:e=>(0,u.jsx)(ht,{...e}),children:t})}),!n&&(0,u.jsx)(dt,{})]})}},3465:(e,t,n)=>{"use strict";n.d(t,{A:()=>u});n(6540);var r=n(5489),a=n(6025),o=n(4586),i=n(6342),l=n(1122),s=n(4848);function c(e){let{logo:t,alt:n,imageClassName:r}=e;const o={light:(0,a.A)(t.src),dark:(0,a.A)(t.srcDark||t.src)},i=(0,s.jsx)(l.A,{className:t.className,sources:o,height:t.height,width:t.width,alt:n,style:t.style});return r?(0,s.jsx)("div",{className:r,children:i}):i}function u(e){const{siteConfig:{title:t}}=(0,o.A)(),{navbar:{title:n,logo:l}}=(0,i.p)(),{imageClassName:u,titleClassName:d,...p}=e,f=(0,a.A)(l?.href||"/"),m=n?"":t,h=l?.alt??m;return(0,s.jsxs)(r.A,{to:f,...p,...l?.target&&{target:l.target},children:[l&&(0,s.jsx)(c,{logo:l,alt:h,imageClassName:u}),null!=n&&(0,s.jsx)("b",{className:d,children:n})]})}},1463:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});n(6540);var r=n(5260),a=n(4848);function o(e){let{locale:t,version:n,tag:o}=e;const i=t;return(0,a.jsxs)(r.A,{children:[t&&(0,a.jsx)("meta",{name:"docusaurus_locale",content:t}),n&&(0,a.jsx)("meta",{name:"docusaurus_version",content:n}),o&&(0,a.jsx)("meta",{name:"docusaurus_tag",content:o}),i&&(0,a.jsx)("meta",{name:"docsearch:language",content:i}),n&&(0,a.jsx)("meta",{name:"docsearch:version",content:n}),o&&(0,a.jsx)("meta",{name:"docsearch:docusaurus_tag",content:o})]})}},1122:(e,t,n)=>{"use strict";n.d(t,{A:()=>u});var r=n(6540),a=n(4164),o=n(2303),i=n(5293);const l={themedComponent:"themedComponent_mlkZ","themedComponent--light":"themedComponent--light_NVdE","themedComponent--dark":"themedComponent--dark_xIcU"};var s=n(4848);function c(e){let{className:t,children:n}=e;const c=(0,o.A)(),{colorMode:u}=(0,i.G)();return(0,s.jsx)(s.Fragment,{children:(c?"dark"===u?["dark"]:["light"]:["light","dark"]).map((e=>{const o=n({theme:e,className:(0,a.A)(t,l.themedComponent,l[`themedComponent--${e}`])});return(0,s.jsx)(r.Fragment,{children:o},e)}))})}function u(e){const{sources:t,className:n,alt:r,...a}=e;return(0,s.jsx)(c,{className:n,children:e=>{let{theme:n,className:o}=e;return(0,s.jsx)("img",{src:t[n],alt:r,className:o,...a})}})}},1422:(e,t,n)=>{"use strict";n.d(t,{N:()=>b,u:()=>c});var r=n(6540),a=n(8193),o=n(205),i=n(3109),l=n(4848);const s="ease-in-out";function c(e){let{initialState:t}=e;const[n,a]=(0,r.useState)(t??!1),o=(0,r.useCallback)((()=>{a((e=>!e))}),[]);return{collapsed:n,setCollapsed:a,toggleCollapsed:o}}const u={display:"none",overflow:"hidden",height:"0px"},d={display:"block",overflow:"visible",height:"auto"};function p(e,t){const n=t?u:d;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function f(e){let{collapsibleRef:t,collapsed:n,animation:a}=e;const o=(0,r.useRef)(!1);(0,r.useEffect)((()=>{const e=t.current;function r(){const t=e.scrollHeight,n=a?.duration??function(e){if((0,i.O)())return 1;const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(t);return{transition:`height ${n}ms ${a?.easing??s}`,height:`${t}px`}}function l(){const t=r();e.style.transition=t.transition,e.style.height=t.height}if(!o.current)return p(e,n),void(o.current=!0);return e.style.willChange="height",function(){const t=requestAnimationFrame((()=>{n?(l(),requestAnimationFrame((()=>{e.style.height=u.height,e.style.overflow=u.overflow}))):(e.style.display="block",requestAnimationFrame((()=>{l()})))}));return()=>cancelAnimationFrame(t)}()}),[t,n,a])}function m(e){if(!a.A.canUseDOM)return e?u:d}function h(e){let{as:t="div",collapsed:n,children:a,animation:o,onCollapseTransitionEnd:i,className:s,disableSSRStyle:c}=e;const u=(0,r.useRef)(null);return f({collapsibleRef:u,collapsed:n,animation:o}),(0,l.jsx)(t,{ref:u,style:c?void 0:m(n),onTransitionEnd:e=>{"height"===e.propertyName&&(p(u.current,n),i?.(n))},className:s,children:a})}function g(e){let{collapsed:t,...n}=e;const[a,i]=(0,r.useState)(!t),[s,c]=(0,r.useState)(t);return(0,o.A)((()=>{t||i(!0)}),[t]),(0,o.A)((()=>{a&&c(t)}),[a,t]),a?(0,l.jsx)(h,{...n,collapsed:s}):null}function b(e){let{lazy:t,...n}=e;const r=t?g:h;return(0,l.jsx)(r,{...n})}},5041:(e,t,n)=>{"use strict";n.d(t,{Mj:()=>h,oq:()=>m});var r=n(6540),a=n(2303),o=n(9466),i=n(9532),l=n(6342),s=n(4848);const c=(0,o.Wf)("docusaurus.announcement.dismiss"),u=(0,o.Wf)("docusaurus.announcement.id"),d=()=>"true"===c.get(),p=e=>c.set(String(e)),f=r.createContext(null);function m(e){let{children:t}=e;const n=function(){const{announcementBar:e}=(0,l.p)(),t=(0,a.A)(),[n,o]=(0,r.useState)((()=>!!t&&d()));(0,r.useEffect)((()=>{o(d())}),[]);const i=(0,r.useCallback)((()=>{p(!0),o(!0)}),[]);return(0,r.useEffect)((()=>{if(!e)return;const{id:t}=e;let n=u.get();"annoucement-bar"===n&&(n="announcement-bar");const r=t!==n;u.set(t),r&&p(!1),!r&&d()||o(!1)}),[e]),(0,r.useMemo)((()=>({isActive:!!e&&!n,close:i})),[e,n,i])}();return(0,s.jsx)(f.Provider,{value:n,children:t})}function h(){const e=(0,r.useContext)(f);if(!e)throw new i.dV("AnnouncementBarProvider");return e}},5293:(e,t,n)=>{"use strict";n.d(t,{G:()=>b,a:()=>g});var r=n(6540),a=n(8193),o=n(9532),i=n(9466),l=n(6342),s=n(4848);const c=r.createContext(void 0),u="theme",d=(0,i.Wf)(u),p={light:"light",dark:"dark"},f=e=>e===p.dark?p.dark:p.light,m=e=>a.A.canUseDOM?f(document.documentElement.getAttribute("data-theme")):f(e),h=e=>{d.set(f(e))};function g(e){let{children:t}=e;const n=function(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,l.p)(),[a,o]=(0,r.useState)(m(e));(0,r.useEffect)((()=>{t&&d.del()}),[t]);const i=(0,r.useCallback)((function(t,r){void 0===r&&(r={});const{persist:a=!0}=r;t?(o(t),a&&h(t)):(o(n?window.matchMedia("(prefers-color-scheme: dark)").matches?p.dark:p.light:e),d.del())}),[n,e]);(0,r.useEffect)((()=>{document.documentElement.setAttribute("data-theme",f(a))}),[a]),(0,r.useEffect)((()=>{if(t)return;const e=e=>{if(e.key!==u)return;const t=d.get();null!==t&&i(f(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)}),[t,i]);const s=(0,r.useRef)(!1);return(0,r.useEffect)((()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),r=()=>{window.matchMedia("print").matches||s.current?s.current=window.matchMedia("print").matches:i(null)};return e.addListener(r),()=>e.removeListener(r)}),[i,t,n]),(0,r.useMemo)((()=>({colorMode:a,setColorMode:i,get isDarkTheme(){return a===p.dark},setLightTheme(){i(p.light)},setDarkTheme(){i(p.dark)}})),[a,i])}();return(0,s.jsx)(c.Provider,{value:n,children:t})}function b(){const e=(0,r.useContext)(c);if(null==e)throw new o.dV("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},5597:(e,t,n)=>{"use strict";n.d(t,{VQ:()=>b,g1:()=>v});var r=n(6540),a=n(8295),o=n(7065),i=n(6342),l=n(1754),s=n(9532),c=n(9466),u=n(4848);const d=e=>`docs-preferred-version-${e}`,p={save:(e,t,n)=>{(0,c.Wf)(d(e),{persistence:t}).set(n)},read:(e,t)=>(0,c.Wf)(d(e),{persistence:t}).get(),clear:(e,t)=>{(0,c.Wf)(d(e),{persistence:t}).del()}},f=e=>Object.fromEntries(e.map((e=>[e,{preferredVersionName:null}])));const m=r.createContext(null);function h(){const e=(0,a.Gy)(),t=(0,i.p)().docs.versionPersistence,n=(0,r.useMemo)((()=>Object.keys(e)),[e]),[o,l]=(0,r.useState)((()=>f(n)));(0,r.useEffect)((()=>{l(function(e){let{pluginIds:t,versionPersistence:n,allDocsData:r}=e;function a(e){const t=p.read(e,n);return r[e].versions.some((e=>e.name===t))?{preferredVersionName:t}:(p.clear(e,n),{preferredVersionName:null})}return Object.fromEntries(t.map((e=>[e,a(e)])))}({allDocsData:e,versionPersistence:t,pluginIds:n}))}),[e,t,n]);return[o,(0,r.useMemo)((()=>({savePreferredVersion:function(e,n){p.save(e,t,n),l((t=>({...t,[e]:{preferredVersionName:n}})))}})),[t])]}function g(e){let{children:t}=e;const n=h();return(0,u.jsx)(m.Provider,{value:n,children:t})}function b(e){let{children:t}=e;return l.C5?(0,u.jsx)(g,{children:t}):(0,u.jsx)(u.Fragment,{children:t})}function y(){const e=(0,r.useContext)(m);if(!e)throw new s.dV("DocsPreferredVersionContextProvider");return e}function v(e){void 0===e&&(e=o.W);const t=(0,a.ht)(e),[n,i]=y(),{preferredVersionName:l}=n[e];return{preferredVersion:t.versions.find((e=>e.name===l))??null,savePreferredVersionName:(0,r.useCallback)((t=>{i.savePreferredVersion(e,t)}),[i,e])}}},4207:(e,t,n)=>{"use strict";n.d(t,{V:()=>s,t:()=>c});var r=n(6540),a=n(9532),o=n(4848);const i=Symbol("EmptyContext"),l=r.createContext(i);function s(e){let{children:t,name:n,items:a}=e;const i=(0,r.useMemo)((()=>n&&a?{name:n,items:a}:null),[n,a]);return(0,o.jsx)(l.Provider,{value:i,children:t})}function c(){const e=(0,r.useContext)(l);if(e===i)throw new a.dV("DocsSidebarProvider");return e}},2252:(e,t,n)=>{"use strict";n.d(t,{n:()=>l,r:()=>s});var r=n(6540),a=n(9532),o=n(4848);const i=r.createContext(null);function l(e){let{children:t,version:n}=e;return(0,o.jsx)(i.Provider,{value:n,children:t})}function s(){const e=(0,r.useContext)(i);if(null===e)throw new a.dV("DocsVersionProvider");return e}},9876:(e,t,n)=>{"use strict";n.d(t,{e:()=>f,M:()=>m});var r=n(6540),a=n(5600),o=n(4581),i=n(6347),l=n(9532);function s(e){!function(e){const t=(0,i.W6)(),n=(0,l._q)(e);(0,r.useEffect)((()=>t.block(((e,t)=>n(e,t)))),[t,n])}(((t,n)=>{if("POP"===n)return e(t,n)}))}var c=n(6342),u=n(4848);const d=r.createContext(void 0);function p(){const e=function(){const e=(0,a.YL)(),{items:t}=(0,c.p)().navbar;return 0===t.length&&!e.component}(),t=(0,o.l)(),n=!e&&"mobile"===t,[i,l]=(0,r.useState)(!1);s((()=>{if(i)return l(!1),!1}));const u=(0,r.useCallback)((()=>{l((e=>!e))}),[]);return(0,r.useEffect)((()=>{"desktop"===t&&l(!1)}),[t]),(0,r.useMemo)((()=>({disabled:e,shouldRender:n,toggle:u,shown:i})),[e,n,u,i])}function f(e){let{children:t}=e;const n=p();return(0,u.jsx)(d.Provider,{value:n,children:t})}function m(){const e=r.useContext(d);if(void 0===e)throw new l.dV("NavbarMobileSidebarProvider");return e}},5600:(e,t,n)=>{"use strict";n.d(t,{GX:()=>c,YL:()=>s,y_:()=>l});var r=n(6540),a=n(9532),o=n(4848);const i=r.createContext(null);function l(e){let{children:t}=e;const n=(0,r.useState)({component:null,props:null});return(0,o.jsx)(i.Provider,{value:n,children:t})}function s(){const e=(0,r.useContext)(i);if(!e)throw new a.dV("NavbarSecondaryMenuContentProvider");return e[0]}function c(e){let{component:t,props:n}=e;const o=(0,r.useContext)(i);if(!o)throw new a.dV("NavbarSecondaryMenuContentProvider");const[,l]=o,s=(0,a.Be)(n);return(0,r.useEffect)((()=>{l({component:t,props:s})}),[l,t,s]),(0,r.useEffect)((()=>()=>l({component:null,props:null})),[l]),null}},4090:(e,t,n)=>{"use strict";n.d(t,{w:()=>a,J:()=>o});var r=n(6540);const a="navigation-with-keyboard";function o(){(0,r.useEffect)((()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(a),"mousedown"===e.type&&document.body.classList.remove(a)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(a),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}}),[])}},4581:(e,t,n)=>{"use strict";n.d(t,{l:()=>l});var r=n(6540),a=n(8193);const o={desktop:"desktop",mobile:"mobile",ssr:"ssr"},i=996;function l(){const[e,t]=(0,r.useState)((()=>"ssr"));return(0,r.useEffect)((()=>{function e(){t(function(){if(!a.A.canUseDOM)throw new Error("getWindowSize() should only be called after React hydration");return window.innerWidth>i?o.desktop:o.mobile}())}return e(),window.addEventListener("resize",e),()=>{window.removeEventListener("resize",e)}}),[]),e}},7559:(e,t,n)=>{"use strict";n.d(t,{G:()=>r});const r={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",unlistedBanner:"theme-unlisted-banner",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{}}},3109:(e,t,n)=>{"use strict";function r(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}n.d(t,{O:()=>r})},1754:(e,t,n)=>{"use strict";n.d(t,{Nr:()=>f,w8:()=>g,C5:()=>p,B5:()=>E,Vd:()=>x,QB:()=>S,fW:()=>k,OF:()=>w,Y:()=>y});var r=n(6540),a=n(6347),o=n(2831),i=n(8295),l=n(5597),s=n(2252),c=n(4207);function u(e){return Array.from(new Set(e))}var d=n(9169);const p=!!i.Gy;function f(e){return"link"!==e.type||e.unlisted?"category"===e.type?function(e){if(e.href&&!e.linkUnlisted)return e.href;for(const t of e.items){const e=f(t);if(e)return e}}(e):void 0:e.href}const m=(e,t)=>void 0!==e&&(0,d.ys)(e,t),h=(e,t)=>e.some((e=>g(e,t)));function g(e,t){return"link"===e.type?m(e.href,t):"category"===e.type&&(m(e.href,t)||h(e.items,t))}function b(e,t){switch(e.type){case"category":return g(e,t)||e.items.some((e=>b(e,t)));case"link":return!e.unlisted||g(e,t);default:return!0}}function y(e,t){return(0,r.useMemo)((()=>e.filter((e=>b(e,t)))),[e,t])}function v(e){let{sidebarItems:t,pathname:n,onlyCategories:r=!1}=e;const a=[];return function e(t){for(const o of t)if("category"===o.type&&((0,d.ys)(o.href,n)||e(o.items))||"link"===o.type&&(0,d.ys)(o.href,n)){return r&&"category"!==o.type||a.unshift(o),!0}return!1}(t),a}function w(){const e=(0,c.t)(),{pathname:t}=(0,a.zy)(),n=(0,i.vT)()?.pluginData.breadcrumbs;return!1!==n&&e?v({sidebarItems:e.items,pathname:t}):null}function x(e){const{activeVersion:t}=(0,i.zK)(e),{preferredVersion:n}=(0,l.g1)(e),a=(0,i.r7)(e);return(0,r.useMemo)((()=>u([t,n,a].filter(Boolean))),[t,n,a])}function k(e,t){const n=x(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.sidebars?Object.entries(e.sidebars):[])),r=t.find((t=>t[0]===e));if(!r)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\nAvailable sidebar ids are:\n- ${t.map((e=>e[0])).join("\n- ")}`);return r[1]}),[e,n])}function S(e,t){const n=x(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.docs)),r=t.find((t=>t.id===e));if(!r){if(n.flatMap((e=>e.draftIds)).includes(e))return null;throw new Error(`Couldn't find any doc with id "${e}" in version${n.length>1?"s":""} "${n.map((e=>e.name)).join(", ")}".\nAvailable doc ids are:\n- ${u(t.map((e=>e.id))).join("\n- ")}`)}return r}),[e,n])}function E(e){let{route:t}=e;const n=(0,a.zy)(),r=(0,s.r)(),i=t.routes,l=i.find((e=>(0,a.B6)(n.pathname,e)));if(!l)return null;const c=l.sidebar,u=c?r.docsSidebars[c]:void 0;return{docElement:(0,o.v)(i),sidebarName:c,sidebarItems:u}}},1003:(e,t,n)=>{"use strict";n.d(t,{e3:()=>f,be:()=>d,Jx:()=>m});var r=n(6540),a=n(4164),o=n(5260),i=n(3102);function l(){const e=r.useContext(i.o);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}var s=n(6025),c=n(4586);var u=n(4848);function d(e){let{title:t,description:n,keywords:r,image:a,children:i}=e;const l=function(e){const{siteConfig:t}=(0,c.A)(),{title:n,titleDelimiter:r}=t;return e?.trim().length?`${e.trim()} ${r} ${n}`:n}(t),{withBaseUrl:d}=(0,s.h)(),p=a?d(a,{absolute:!0}):void 0;return(0,u.jsxs)(o.A,{children:[t&&(0,u.jsx)("title",{children:l}),t&&(0,u.jsx)("meta",{property:"og:title",content:l}),n&&(0,u.jsx)("meta",{name:"description",content:n}),n&&(0,u.jsx)("meta",{property:"og:description",content:n}),r&&(0,u.jsx)("meta",{name:"keywords",content:Array.isArray(r)?r.join(","):r}),p&&(0,u.jsx)("meta",{property:"og:image",content:p}),p&&(0,u.jsx)("meta",{name:"twitter:image",content:p}),i]})}const p=r.createContext(void 0);function f(e){let{className:t,children:n}=e;const i=r.useContext(p),l=(0,a.A)(i,t);return(0,u.jsxs)(p.Provider,{value:l,children:[(0,u.jsx)(o.A,{children:(0,u.jsx)("html",{className:l})}),n]})}function m(e){let{children:t}=e;const n=l(),r=`plugin-${n.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const o=`plugin-id-${n.plugin.id}`;return(0,u.jsx)(f,{className:(0,a.A)(r,o),children:t})}},9532:(e,t,n)=>{"use strict";n.d(t,{Be:()=>c,ZC:()=>l,_q:()=>i,dV:()=>s,fM:()=>u});var r=n(6540),a=n(205),o=n(4848);function i(e){const t=(0,r.useRef)(e);return(0,a.A)((()=>{t.current=e}),[e]),(0,r.useCallback)((function(){return t.current(...arguments)}),[])}function l(e){const t=(0,r.useRef)();return(0,a.A)((()=>{t.current=e})),t.current}class s extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?\w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function c(e){const t=Object.entries(e);return t.sort(((e,t)=>e[0].localeCompare(t[0]))),(0,r.useMemo)((()=>e),t.flat())}function u(e){return t=>{let{children:n}=t;return(0,o.jsx)(o.Fragment,{children:e.reduceRight(((e,t)=>(0,o.jsx)(t,{children:e})),n)})}}},9169:(e,t,n)=>{"use strict";n.d(t,{Dt:()=>l,ys:()=>i});var r=n(6540),a=n(8328),o=n(4586);function i(e,t){const n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function l(){const{baseUrl:e}=(0,o.A)().siteConfig;return(0,r.useMemo)((()=>function(e){let{baseUrl:t,routes:n}=e;function r(e){return e.path===t&&!0===e.exact}function a(e){return e.path===t&&!e.exact}return function e(t){if(0===t.length)return;return t.find(r)||e(t.filter(a).flatMap((e=>e.routes??[])))}(n)}({routes:a.A,baseUrl:e})),[e])}},3104:(e,t,n)=>{"use strict";n.d(t,{Mq:()=>p,Tv:()=>c,gk:()=>f});var r=n(6540),a=n(8193),o=n(2303),i=(n(205),n(9532)),l=n(4848);const s=r.createContext(void 0);function c(e){let{children:t}=e;const n=function(){const e=(0,r.useRef)(!0);return(0,r.useMemo)((()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}})),[])}();return(0,l.jsx)(s.Provider,{value:n,children:t})}function u(){const e=(0,r.useContext)(s);if(null==e)throw new i.dV("ScrollControllerProvider");return e}const d=()=>a.A.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function p(e,t){void 0===t&&(t=[]);const{scrollEventsEnabledRef:n}=u(),a=(0,r.useRef)(d()),o=(0,i._q)(e);(0,r.useEffect)((()=>{const e=()=>{if(!n.current)return;const e=d();o(e,a.current),a.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)}),[o,n,...t])}function f(){const e=(0,r.useRef)(null),t=(0,o.A)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function r(){const a=document.documentElement.scrollTop;(n&&a>e||!n&&at&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>e.current?.()}}},2967:(e,t,n)=>{"use strict";n.d(t,{Cy:()=>r,tU:()=>a});n(4586);const r="default";function a(e,t){return`docs-${e}-${t}`}},9466:(e,t,n)=>{"use strict";n.d(t,{Wf:()=>s});n(6540);const r="localStorage";function a(e){let{key:t,oldValue:n,newValue:r,storage:a}=e;if(n===r)return;const o=document.createEvent("StorageEvent");o.initStorageEvent("storage",!1,!1,t,n,r,window.location.href,a),window.dispatchEvent(o)}function o(e){if(void 0===e&&(e=r),"undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,i||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),i=!0),null}var t}let i=!1;const l={get:()=>null,set:()=>{},del:()=>{},listen:()=>()=>{}};function s(e,t){if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t,listen:t}}(e);const n=o(t?.persistence);return null===n?l:{get:()=>{try{return n.getItem(e)}catch(t){return console.error(`Docusaurus storage error, can't get key=${e}`,t),null}},set:t=>{try{const r=n.getItem(e);n.setItem(e,t),a({key:e,oldValue:r,newValue:t,storage:n})}catch(r){console.error(`Docusaurus storage error, can't set ${e}=${t}`,r)}},del:()=>{try{const t=n.getItem(e);n.removeItem(e),a({key:e,oldValue:t,newValue:null,storage:n})}catch(t){console.error(`Docusaurus storage error, can't delete key=${e}`,t)}},listen:t=>{try{const r=r=>{r.storageArea===n&&r.key===e&&t(r)};return window.addEventListener("storage",r),()=>window.removeEventListener("storage",r)}catch(r){return console.error(`Docusaurus storage error, can't listen for changes of key=${e}`,r),()=>{}}}}}},2131:(e,t,n)=>{"use strict";n.d(t,{o:()=>i});var r=n(4586),a=n(6347),o=n(440);function i(){const{siteConfig:{baseUrl:e,url:t,trailingSlash:n},i18n:{defaultLocale:i,currentLocale:l}}=(0,r.A)(),{pathname:s}=(0,a.zy)(),c=(0,o.applyTrailingSlash)(s,{trailingSlash:n,baseUrl:e}),u=l===i?e:e.replace(`/${l}/`,"/"),d=c.replace(e,"");return{createUrl:function(e){let{locale:n,fullyQualified:r}=e;return`${r?t:""}${function(e){return e===i?`${u}`:`${u}${e}/`}(n)}${d}`}}}},5062:(e,t,n)=>{"use strict";n.d(t,{$:()=>i});var r=n(6540),a=n(6347),o=n(9532);function i(e){const t=(0,a.zy)(),n=(0,o.ZC)(t),i=(0,o._q)(e);(0,r.useEffect)((()=>{n&&t!==n&&i({location:t,previousLocation:n})}),[i,t,n])}},6342:(e,t,n)=>{"use strict";n.d(t,{p:()=>a});var r=n(4586);function a(){return(0,r.A)().siteConfig.themeConfig}},2983:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){const{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[a]=e.split(/[#?]/),o="/"===a||a===r?a:(i=a,n?function(e){return e.endsWith("/")?e:`${e}/`}(i):function(e){return e.endsWith("/")?e.slice(0,-1):e}(i));var i;return e.replace(a,o)}},253:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=void 0,t.getErrorCausalChain=function e(t){return t.cause?[t,...e(t.cause)]:[t]}},440:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=t.applyTrailingSlash=t.blogPostContainerID=void 0,t.blogPostContainerID="__blog-post-container";var a=n(2983);Object.defineProperty(t,"applyTrailingSlash",{enumerable:!0,get:function(){return r(a).default}});var o=n(253);Object.defineProperty(t,"getErrorCausalChain",{enumerable:!0,get:function(){return o.getErrorCausalChain}})},1513:(e,t,n)=>{"use strict";n.d(t,{zR:()=>w,TM:()=>C,yJ:()=>f,sC:()=>A,AO:()=>p});var r=n(8168);function a(e){return"/"===e.charAt(0)}function o(e,t){for(var n=t,r=n+1,a=e.length;r=0;p--){var f=i[p];"."===f?o(i,p):".."===f?(o(i,p),d++):d&&(o(i,p),d--)}if(!c)for(;d--;d)i.unshift("..");!c||""===i[0]||i[0]&&a(i[0])||i.unshift("");var m=i.join("/");return n&&"/"!==m.substr(-1)&&(m+="/"),m};var l=n(1561);function s(e){return"/"===e.charAt(0)?e:"/"+e}function c(e){return"/"===e.charAt(0)?e.substr(1):e}function u(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function p(e){var t=e.pathname,n=e.search,r=e.hash,a=t||"/";return n&&"?"!==n&&(a+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(a+="#"===r.charAt(0)?r:"#"+r),a}function f(e,t,n,a){var o;"string"==typeof e?(o=function(e){var t=e||"/",n="",r="",a=t.indexOf("#");-1!==a&&(r=t.substr(a),t=t.substr(0,a));var o=t.indexOf("?");return-1!==o&&(n=t.substr(o),t=t.substr(0,o)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}}(e),o.state=t):(void 0===(o=(0,r.A)({},e)).pathname&&(o.pathname=""),o.search?"?"!==o.search.charAt(0)&&(o.search="?"+o.search):o.search="",o.hash?"#"!==o.hash.charAt(0)&&(o.hash="#"+o.hash):o.hash="",void 0!==t&&void 0===o.state&&(o.state=t));try{o.pathname=decodeURI(o.pathname)}catch(l){throw l instanceof URIError?new URIError('Pathname "'+o.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):l}return n&&(o.key=n),a?o.pathname?"/"!==o.pathname.charAt(0)&&(o.pathname=i(o.pathname,a.pathname)):o.pathname=a.pathname:o.pathname||(o.pathname="/"),o}function m(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,a){if(null!=e){var o="function"==typeof e?e(t,n):e;"string"==typeof o?"function"==typeof r?r(o,a):a(!0):a(!1!==o)}else a(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter((function(e){return e!==r}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),r=0;rt?n.splice(t,n.length-t,a):n.push(a),d({action:r,location:a,index:t,entries:n})}}))},replace:function(e,t){var r="REPLACE",a=f(e,t,h(),w.location);u.confirmTransitionTo(a,r,n,(function(e){e&&(w.entries[w.index]=a,d({action:r,location:a}))}))},go:v,goBack:function(){v(-1)},goForward:function(){v(1)},canGo:function(e){var t=w.index+e;return t>=0&&t{"use strict";var r=n(4363),a={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},o={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},l={};function s(e){return r.isMemo(e)?i:l[e.$$typeof]||a}l[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},l[r.Memo]=i;var c=Object.defineProperty,u=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,p=Object.getOwnPropertyDescriptor,f=Object.getPrototypeOf,m=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(m){var a=f(n);a&&a!==m&&e(t,a,r)}var i=u(n);d&&(i=i.concat(d(n)));for(var l=s(t),h=s(n),g=0;g{"use strict";e.exports=function(e,t,n,r,a,o,i,l){if(!e){var s;if(void 0===t)s=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,a,o,i,l],u=0;(s=new Error(t.replace(/%s/g,(function(){return c[u++]})))).name="Invariant Violation"}throw s.framesToPop=1,s}}},4634:e=>{e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},689:function(e){e.exports=function(){"use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},n=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1])||arguments[1],a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;t(this,e),this.ctx=n,this.iframes=r,this.exclude=a,this.iframesTimeout=o}return n(e,[{key:"getContexts",value:function(){var e=[];return(void 0!==this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:"string"==typeof this.ctx?Array.prototype.slice.call(document.querySelectorAll(this.ctx)):[this.ctx]:[]).forEach((function(t){var n=e.filter((function(e){return e.contains(t)})).length>0;-1!==e.indexOf(t)||n||e.push(t)})),e}},{key:"getIframeContents",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},r=void 0;try{var a=e.contentWindow;if(r=a.document,!a||!r)throw new Error("iframe inaccessible")}catch(o){n()}r&&t(r)}},{key:"isIframeBlank",value:function(e){var t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}},{key:"observeIframeLoad",value:function(e,t,n){var r=this,a=!1,o=null,i=function i(){if(!a){a=!0,clearTimeout(o);try{r.isIframeBlank(e)||(e.removeEventListener("load",i),r.getIframeContents(e,t,n))}catch(l){n()}}};e.addEventListener("load",i),o=setTimeout(i,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,t,n){try{"complete"===e.contentWindow.document.readyState?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch(r){n()}}},{key:"waitForIframes",value:function(e,t){var n=this,r=0;this.forEachIframe(e,(function(){return!0}),(function(e){r++,n.waitForIframes(e.querySelector("html"),(function(){--r||t()}))}),(function(e){e||t()}))}},{key:"forEachIframe",value:function(t,n,r){var a=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},i=t.querySelectorAll("iframe"),l=i.length,s=0;i=Array.prototype.slice.call(i);var c=function(){--l<=0&&o(s)};l||c(),i.forEach((function(t){e.matches(t,a.exclude)?c():a.onIframeReady(t,(function(e){n(t)&&(s++,r(e)),c()}),c)}))}},{key:"createIterator",value:function(e,t,n){return document.createNodeIterator(e,t,n,!1)}},{key:"createInstanceOnIframe",value:function(t){return new e(t.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,t,n){if(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING){if(null===t)return!0;if(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)return!0}return!1}},{key:"getIteratorNode",value:function(e){var t=e.previousNode();return{prevNode:t,node:(null===t||e.nextNode())&&e.nextNode()}}},{key:"checkIframeFilter",value:function(e,t,n,r){var a=!1,o=!1;return r.forEach((function(e,t){e.val===n&&(a=t,o=e.handled)})),this.compareNodeIframe(e,t,n)?(!1!==a||o?!1===a||o||(r[a].handled=!0):r.push({val:n,handled:!0}),!0):(!1===a&&r.push({val:n,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,t,n,r){var a=this;e.forEach((function(e){e.handled||a.getIframeContents(e.val,(function(e){a.createInstanceOnIframe(e).forEachNode(t,n,r)}))}))}},{key:"iterateThroughNodes",value:function(e,t,n,r,a){for(var o=this,i=this.createIterator(t,e,r),l=[],s=[],c=void 0,u=void 0,d=function(){var e=o.getIteratorNode(i);return u=e.prevNode,c=e.node};d();)this.iframes&&this.forEachIframe(t,(function(e){return o.checkIframeFilter(c,u,e,l)}),(function(t){o.createInstanceOnIframe(t).forEachNode(e,(function(e){return s.push(e)}),r)})),s.push(c);s.forEach((function(e){n(e)})),this.iframes&&this.handleOpenIframes(l,e,n,r),a()}},{key:"forEachNode",value:function(e,t,n){var r=this,a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},o=this.getContexts(),i=o.length;i||a(),o.forEach((function(o){var l=function(){r.iterateThroughNodes(e,o,t,n,(function(){--i<=0&&a()}))};r.iframes?r.waitForIframes(o,l):l()}))}}],[{key:"matches",value:function(e,t){var n="string"==typeof t?[t]:t,r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(r){var a=!1;return n.every((function(t){return!r.call(e,t)||(a=!0,!1)})),a}return!1}}]),e}(),o=function(){function o(e){t(this,o),this.ctx=e,this.ie=!1;var n=window.navigator.userAgent;(n.indexOf("MSIE")>-1||n.indexOf("Trident")>-1)&&(this.ie=!0)}return n(o,[{key:"log",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"debug",r=this.opt.log;this.opt.debug&&"object"===(void 0===r?"undefined":e(r))&&"function"==typeof r[n]&&r[n]("mark.js: "+t)}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createRegExp",value:function(e){return"disabled"!==this.opt.wildcards&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),"disabled"!==this.opt.wildcards&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e)}},{key:"createSynonymsRegExp",value:function(e){var t=this.opt.synonyms,n=this.opt.caseSensitive?"":"i",r=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var a in t)if(t.hasOwnProperty(a)){var o=t[a],i="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(a):this.escapeStr(a),l="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(o):this.escapeStr(o);""!==i&&""!==l&&(e=e.replace(new RegExp("("+this.escapeStr(i)+"|"+this.escapeStr(l)+")","gm"+n),r+"("+this.processSynomyms(i)+"|"+this.processSynomyms(l)+")"+r))}return e}},{key:"processSynomyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return(e=e.replace(/(?:\\)*\?/g,(function(e){return"\\"===e.charAt(0)?"?":"\x01"}))).replace(/(?:\\)*\*/g,(function(e){return"\\"===e.charAt(0)?"*":"\x02"}))}},{key:"createWildcardsRegExp",value:function(e){var t="withSpaces"===this.opt.wildcards;return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,(function(e,t,n){var r=n.charAt(t+1);return/[(|)\\]/.test(r)||""===r?e:e+"\0"}))}},{key:"createJoinersRegExp",value:function(e){var t=[],n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join("["+t.join("")+"]*"):e}},{key:"createDiacriticsRegExp",value:function(e){var t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["a\xe0\xe1\u1ea3\xe3\u1ea1\u0103\u1eb1\u1eaf\u1eb3\u1eb5\u1eb7\xe2\u1ea7\u1ea5\u1ea9\u1eab\u1ead\xe4\xe5\u0101\u0105","A\xc0\xc1\u1ea2\xc3\u1ea0\u0102\u1eb0\u1eae\u1eb2\u1eb4\u1eb6\xc2\u1ea6\u1ea4\u1ea8\u1eaa\u1eac\xc4\xc5\u0100\u0104","c\xe7\u0107\u010d","C\xc7\u0106\u010c","d\u0111\u010f","D\u0110\u010e","e\xe8\xe9\u1ebb\u1ebd\u1eb9\xea\u1ec1\u1ebf\u1ec3\u1ec5\u1ec7\xeb\u011b\u0113\u0119","E\xc8\xc9\u1eba\u1ebc\u1eb8\xca\u1ec0\u1ebe\u1ec2\u1ec4\u1ec6\xcb\u011a\u0112\u0118","i\xec\xed\u1ec9\u0129\u1ecb\xee\xef\u012b","I\xcc\xcd\u1ec8\u0128\u1eca\xce\xcf\u012a","l\u0142","L\u0141","n\xf1\u0148\u0144","N\xd1\u0147\u0143","o\xf2\xf3\u1ecf\xf5\u1ecd\xf4\u1ed3\u1ed1\u1ed5\u1ed7\u1ed9\u01a1\u1edf\u1ee1\u1edb\u1edd\u1ee3\xf6\xf8\u014d","O\xd2\xd3\u1ece\xd5\u1ecc\xd4\u1ed2\u1ed0\u1ed4\u1ed6\u1ed8\u01a0\u1ede\u1ee0\u1eda\u1edc\u1ee2\xd6\xd8\u014c","r\u0159","R\u0158","s\u0161\u015b\u0219\u015f","S\u0160\u015a\u0218\u015e","t\u0165\u021b\u0163","T\u0164\u021a\u0162","u\xf9\xfa\u1ee7\u0169\u1ee5\u01b0\u1eeb\u1ee9\u1eed\u1eef\u1ef1\xfb\xfc\u016f\u016b","U\xd9\xda\u1ee6\u0168\u1ee4\u01af\u1eea\u1ee8\u1eec\u1eee\u1ef0\xdb\xdc\u016e\u016a","y\xfd\u1ef3\u1ef7\u1ef9\u1ef5\xff","Y\xdd\u1ef2\u1ef6\u1ef8\u1ef4\u0178","z\u017e\u017c\u017a","Z\u017d\u017b\u0179"]:["a\xe0\xe1\u1ea3\xe3\u1ea1\u0103\u1eb1\u1eaf\u1eb3\u1eb5\u1eb7\xe2\u1ea7\u1ea5\u1ea9\u1eab\u1ead\xe4\xe5\u0101\u0105A\xc0\xc1\u1ea2\xc3\u1ea0\u0102\u1eb0\u1eae\u1eb2\u1eb4\u1eb6\xc2\u1ea6\u1ea4\u1ea8\u1eaa\u1eac\xc4\xc5\u0100\u0104","c\xe7\u0107\u010dC\xc7\u0106\u010c","d\u0111\u010fD\u0110\u010e","e\xe8\xe9\u1ebb\u1ebd\u1eb9\xea\u1ec1\u1ebf\u1ec3\u1ec5\u1ec7\xeb\u011b\u0113\u0119E\xc8\xc9\u1eba\u1ebc\u1eb8\xca\u1ec0\u1ebe\u1ec2\u1ec4\u1ec6\xcb\u011a\u0112\u0118","i\xec\xed\u1ec9\u0129\u1ecb\xee\xef\u012bI\xcc\xcd\u1ec8\u0128\u1eca\xce\xcf\u012a","l\u0142L\u0141","n\xf1\u0148\u0144N\xd1\u0147\u0143","o\xf2\xf3\u1ecf\xf5\u1ecd\xf4\u1ed3\u1ed1\u1ed5\u1ed7\u1ed9\u01a1\u1edf\u1ee1\u1edb\u1edd\u1ee3\xf6\xf8\u014dO\xd2\xd3\u1ece\xd5\u1ecc\xd4\u1ed2\u1ed0\u1ed4\u1ed6\u1ed8\u01a0\u1ede\u1ee0\u1eda\u1edc\u1ee2\xd6\xd8\u014c","r\u0159R\u0158","s\u0161\u015b\u0219\u015fS\u0160\u015a\u0218\u015e","t\u0165\u021b\u0163T\u0164\u021a\u0162","u\xf9\xfa\u1ee7\u0169\u1ee5\u01b0\u1eeb\u1ee9\u1eed\u1eef\u1ef1\xfb\xfc\u016f\u016bU\xd9\xda\u1ee6\u0168\u1ee4\u01af\u1eea\u1ee8\u1eec\u1eee\u1ef0\xdb\xdc\u016e\u016a","y\xfd\u1ef3\u1ef7\u1ef9\u1ef5\xffY\xdd\u1ef2\u1ef6\u1ef8\u1ef4\u0178","z\u017e\u017c\u017aZ\u017d\u017b\u0179"],r=[];return e.split("").forEach((function(a){n.every((function(n){if(-1!==n.indexOf(a)){if(r.indexOf(n)>-1)return!1;e=e.replace(new RegExp("["+n+"]","gm"+t),"["+n+"]"),r.push(n)}return!0}))})),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gim,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var t=this,n="!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~\xa1\xbf",r=this.opt.accuracy,a="string"==typeof r?r:r.value,o="string"==typeof r?[]:r.limiters,i="";switch(o.forEach((function(e){i+="|"+t.escapeStr(e)})),a){case"partially":default:return"()("+e+")";case"complementary":return"()([^"+(i="\\s"+(i||this.escapeStr(n)))+"]*"+e+"[^"+i+"]*)";case"exactly":return"(^|\\s"+i+")("+e+")(?=$|\\s"+i+")"}}},{key:"getSeparatedKeywords",value:function(e){var t=this,n=[];return e.forEach((function(e){t.opt.separateWordSearch?e.split(" ").forEach((function(e){e.trim()&&-1===n.indexOf(e)&&n.push(e)})):e.trim()&&-1===n.indexOf(e)&&n.push(e)})),{keywords:n.sort((function(e,t){return t.length-e.length})),length:n.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var t=this;if(!Array.isArray(e)||"[object Object]"!==Object.prototype.toString.call(e[0]))return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var n=[],r=0;return e.sort((function(e,t){return e.start-t.start})).forEach((function(e){var a=t.callNoMatchOnInvalidRanges(e,r),o=a.start,i=a.end;a.valid&&(e.start=o,e.length=i-o,n.push(e),r=i)})),n}},{key:"callNoMatchOnInvalidRanges",value:function(e,t){var n=void 0,r=void 0,a=!1;return e&&void 0!==e.start?(r=(n=parseInt(e.start,10))+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&r-t>0&&r-n>0?a=!0:(this.log("Ignoring invalid or overlapping range: "+JSON.stringify(e)),this.opt.noMatch(e))):(this.log("Ignoring invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:n,end:r,valid:a}}},{key:"checkWhitespaceRanges",value:function(e,t,n){var r=void 0,a=!0,o=n.length,i=t-o,l=parseInt(e.start,10)-i;return(r=(l=l>o?o:l)+parseInt(e.length,10))>o&&(r=o,this.log("End range automatically set to the max value of "+o)),l<0||r-l<0||l>o||r>o?(a=!1,this.log("Invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)):""===n.substring(l,r).replace(/\s+/g,"")&&(a=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:l,end:r,valid:a}}},{key:"getTextNodes",value:function(e){var t=this,n="",r=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,(function(e){r.push({start:n.length,end:(n+=e.textContent).length,node:e})}),(function(e){return t.matchesExclude(e.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}),(function(){e({value:n,nodes:r})}))}},{key:"matchesExclude",value:function(e){return a.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,t,n){var r=this.opt.element?this.opt.element:"mark",a=e.splitText(t),o=a.splitText(n-t),i=document.createElement(r);return i.setAttribute("data-markjs","true"),this.opt.className&&i.setAttribute("class",this.opt.className),i.textContent=a.textContent,a.parentNode.replaceChild(i,a),o}},{key:"wrapRangeInMappedTextNode",value:function(e,t,n,r,a){var o=this;e.nodes.every((function(i,l){var s=e.nodes[l+1];if(void 0===s||s.start>t){if(!r(i.node))return!1;var c=t-i.start,u=(n>i.end?i.end:n)-i.start,d=e.value.substr(0,i.start),p=e.value.substr(u+i.start);if(i.node=o.wrapRangeInTextNode(i.node,c,u),e.value=d+p,e.nodes.forEach((function(t,n){n>=l&&(e.nodes[n].start>0&&n!==l&&(e.nodes[n].start-=u),e.nodes[n].end-=u)})),n-=u,a(i.node.previousSibling,i.start),!(n>i.end))return!1;t=i.end}return!0}))}},{key:"wrapMatches",value:function(e,t,n,r,a){var o=this,i=0===t?0:t+1;this.getTextNodes((function(t){t.nodes.forEach((function(t){t=t.node;for(var a=void 0;null!==(a=e.exec(t.textContent))&&""!==a[i];)if(n(a[i],t)){var l=a.index;if(0!==i)for(var s=1;s{"use strict";n.r(t)},1043:(e,t,n)=>{"use strict";n.r(t)},5947:function(e,t,n){var r,a;r=function(){var e,t,n={version:"0.2.0"},r=n.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
'};function a(e,t,n){return en?n:e}function o(e){return 100*(-1+e)}function i(e,t,n){var a;return(a="translate3d"===r.positionUsing?{transform:"translate3d("+o(e)+"%,0,0)"}:"translate"===r.positionUsing?{transform:"translate("+o(e)+"%,0)"}:{"margin-left":o(e)+"%"}).transition="all "+t+"ms "+n,a}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(r[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=a(e,r.minimum,1),n.status=1===e?null:e;var o=n.render(!t),c=o.querySelector(r.barSelector),u=r.speed,d=r.easing;return o.offsetWidth,l((function(t){""===r.positionUsing&&(r.positionUsing=n.getPositioningCSS()),s(c,i(e,u,d)),1===e?(s(o,{transition:"none",opacity:1}),o.offsetWidth,setTimeout((function(){s(o,{transition:"all "+u+"ms linear",opacity:0}),setTimeout((function(){n.remove(),t()}),u)}),u)):setTimeout(t,u)})),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout((function(){n.status&&(n.trickle(),e())}),r.trickleSpeed)};return r.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*a(Math.random()*t,.1,.95)),t=a(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*r.trickleRate)},e=0,t=0,n.promise=function(r){return r&&"resolved"!==r.state()?(0===t&&n.start(),e++,t++,r.always((function(){0==--t?(e=0,n.done()):n.set((e-t)/e)})),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=r.template;var a,i=t.querySelector(r.barSelector),l=e?"-100":o(n.status||0),c=document.querySelector(r.parent);return s(i,{transition:"all 0 linear",transform:"translate3d("+l+"%,0,0)"}),r.showSpinner||(a=t.querySelector(r.spinnerSelector))&&f(a),c!=document.body&&u(c,"nprogress-custom-parent"),c.appendChild(t),t},n.remove=function(){d(document.documentElement,"nprogress-busy"),d(document.querySelector(r.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&f(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var l=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),s=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(function(e,t){return t.toUpperCase()}))}function r(t){var n=document.body.style;if(t in n)return t;for(var r,a=e.length,o=t.charAt(0).toUpperCase()+t.slice(1);a--;)if((r=e[a]+o)in n)return r;return t}function a(e){return e=n(e),t[e]||(t[e]=r(e))}function o(e,t,n){t=a(t),e.style[t]=n}return function(e,t){var n,r,a=arguments;if(2==a.length)for(n in t)void 0!==(r=t[n])&&t.hasOwnProperty(n)&&o(e,n,r);else o(e,a[1],a[2])}}();function c(e,t){return("string"==typeof e?e:p(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=p(e),r=n+t;c(n,t)||(e.className=r.substring(1))}function d(e,t){var n,r=p(e);c(e,t)&&(n=r.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function p(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function f(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(a="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=a)},5302:(e,t,n)=>{var r=n(4634);e.exports=f,e.exports.parse=o,e.exports.compile=function(e,t){return l(o(e,t),t)},e.exports.tokensToFunction=l,e.exports.tokensToRegExp=p;var a=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function o(e,t){for(var n,r=[],o=0,i=0,l="",u=t&&t.delimiter||"/";null!=(n=a.exec(e));){var d=n[0],p=n[1],f=n.index;if(l+=e.slice(i,f),i=f+d.length,p)l+=p[1];else{var m=e[i],h=n[2],g=n[3],b=n[4],y=n[5],v=n[6],w=n[7];l&&(r.push(l),l="");var x=null!=h&&null!=m&&m!==h,k="+"===v||"*"===v,S="?"===v||"*"===v,E=n[2]||u,_=b||y;r.push({name:g||o++,prefix:h||"",delimiter:E,optional:S,repeat:k,partial:x,asterisk:!!w,pattern:_?c(_):w?".*":"[^"+s(E)+"]+?"})}}return i{e.exports&&(e.exports={core:{meta:{path:"components/prism-core.js",option:"mandatory"},core:"Core"},themes:{meta:{path:"themes/{id}.css",link:"index.html?theme={id}",exclusive:!0},prism:{title:"Default",option:"default"},"prism-dark":"Dark","prism-funky":"Funky","prism-okaidia":{title:"Okaidia",owner:"ocodia"},"prism-twilight":{title:"Twilight",owner:"remybach"},"prism-coy":{title:"Coy",owner:"tshedor"},"prism-solarizedlight":{title:"Solarized Light",owner:"hectormatos2011 "},"prism-tomorrow":{title:"Tomorrow Night",owner:"Rosey"}},languages:{meta:{path:"components/prism-{id}",noCSS:!0,examplesPath:"examples/prism-{id}",addCheckAll:!0},markup:{title:"Markup",alias:["html","xml","svg","mathml","ssml","atom","rss"],aliasTitles:{html:"HTML",xml:"XML",svg:"SVG",mathml:"MathML",ssml:"SSML",atom:"Atom",rss:"RSS"},option:"default"},css:{title:"CSS",option:"default",modify:"markup"},clike:{title:"C-like",option:"default"},javascript:{title:"JavaScript",require:"clike",modify:"markup",optional:"regex",alias:"js",option:"default"},abap:{title:"ABAP",owner:"dellagustin"},abnf:{title:"ABNF",owner:"RunDevelopment"},actionscript:{title:"ActionScript",require:"javascript",modify:"markup",owner:"Golmote"},ada:{title:"Ada",owner:"Lucretia"},agda:{title:"Agda",owner:"xy-ren"},al:{title:"AL",owner:"RunDevelopment"},antlr4:{title:"ANTLR4",alias:"g4",owner:"RunDevelopment"},apacheconf:{title:"Apache Configuration",owner:"GuiTeK"},apex:{title:"Apex",require:["clike","sql"],owner:"RunDevelopment"},apl:{title:"APL",owner:"ngn"},applescript:{title:"AppleScript",owner:"Golmote"},aql:{title:"AQL",owner:"RunDevelopment"},arduino:{title:"Arduino",require:"cpp",alias:"ino",owner:"dkern"},arff:{title:"ARFF",owner:"Golmote"},armasm:{title:"ARM Assembly",alias:"arm-asm",owner:"RunDevelopment"},arturo:{title:"Arturo",alias:"art",optional:["bash","css","javascript","markup","markdown","sql"],owner:"drkameleon"},asciidoc:{alias:"adoc",title:"AsciiDoc",owner:"Golmote"},aspnet:{title:"ASP.NET (C#)",require:["markup","csharp"],owner:"nauzilus"},asm6502:{title:"6502 Assembly",owner:"kzurawel"},asmatmel:{title:"Atmel AVR Assembly",owner:"cerkit"},autohotkey:{title:"AutoHotkey",owner:"aviaryan"},autoit:{title:"AutoIt",owner:"Golmote"},avisynth:{title:"AviSynth",alias:"avs",owner:"Zinfidel"},"avro-idl":{title:"Avro IDL",alias:"avdl",owner:"RunDevelopment"},awk:{title:"AWK",alias:"gawk",aliasTitles:{gawk:"GAWK"},owner:"RunDevelopment"},bash:{title:"Bash",alias:["sh","shell"],aliasTitles:{sh:"Shell",shell:"Shell"},owner:"zeitgeist87"},basic:{title:"BASIC",owner:"Golmote"},batch:{title:"Batch",owner:"Golmote"},bbcode:{title:"BBcode",alias:"shortcode",aliasTitles:{shortcode:"Shortcode"},owner:"RunDevelopment"},bbj:{title:"BBj",owner:"hyyan"},bicep:{title:"Bicep",owner:"johnnyreilly"},birb:{title:"Birb",require:"clike",owner:"Calamity210"},bison:{title:"Bison",require:"c",owner:"Golmote"},bnf:{title:"BNF",alias:"rbnf",aliasTitles:{rbnf:"RBNF"},owner:"RunDevelopment"},bqn:{title:"BQN",owner:"yewscion"},brainfuck:{title:"Brainfuck",owner:"Golmote"},brightscript:{title:"BrightScript",owner:"RunDevelopment"},bro:{title:"Bro",owner:"wayward710"},bsl:{title:"BSL (1C:Enterprise)",alias:"oscript",aliasTitles:{oscript:"OneScript"},owner:"Diversus23"},c:{title:"C",require:"clike",owner:"zeitgeist87"},csharp:{title:"C#",require:"clike",alias:["cs","dotnet"],owner:"mvalipour"},cpp:{title:"C++",require:"c",owner:"zeitgeist87"},cfscript:{title:"CFScript",require:"clike",alias:"cfc",owner:"mjclemente"},chaiscript:{title:"ChaiScript",require:["clike","cpp"],owner:"RunDevelopment"},cil:{title:"CIL",owner:"sbrl"},cilkc:{title:"Cilk/C",require:"c",alias:"cilk-c",owner:"OpenCilk"},cilkcpp:{title:"Cilk/C++",require:"cpp",alias:["cilk-cpp","cilk"],owner:"OpenCilk"},clojure:{title:"Clojure",owner:"troglotit"},cmake:{title:"CMake",owner:"mjrogozinski"},cobol:{title:"COBOL",owner:"RunDevelopment"},coffeescript:{title:"CoffeeScript",require:"javascript",alias:"coffee",owner:"R-osey"},concurnas:{title:"Concurnas",alias:"conc",owner:"jasontatton"},csp:{title:"Content-Security-Policy",owner:"ScottHelme"},cooklang:{title:"Cooklang",owner:"ahue"},coq:{title:"Coq",owner:"RunDevelopment"},crystal:{title:"Crystal",require:"ruby",owner:"MakeNowJust"},"css-extras":{title:"CSS Extras",require:"css",modify:"css",owner:"milesj"},csv:{title:"CSV",owner:"RunDevelopment"},cue:{title:"CUE",owner:"RunDevelopment"},cypher:{title:"Cypher",owner:"RunDevelopment"},d:{title:"D",require:"clike",owner:"Golmote"},dart:{title:"Dart",require:"clike",owner:"Golmote"},dataweave:{title:"DataWeave",owner:"machaval"},dax:{title:"DAX",owner:"peterbud"},dhall:{title:"Dhall",owner:"RunDevelopment"},diff:{title:"Diff",owner:"uranusjr"},django:{title:"Django/Jinja2",require:"markup-templating",alias:"jinja2",owner:"romanvm"},"dns-zone-file":{title:"DNS zone file",owner:"RunDevelopment",alias:"dns-zone"},docker:{title:"Docker",alias:"dockerfile",owner:"JustinBeckwith"},dot:{title:"DOT (Graphviz)",alias:"gv",optional:"markup",owner:"RunDevelopment"},ebnf:{title:"EBNF",owner:"RunDevelopment"},editorconfig:{title:"EditorConfig",owner:"osipxd"},eiffel:{title:"Eiffel",owner:"Conaclos"},ejs:{title:"EJS",require:["javascript","markup-templating"],owner:"RunDevelopment",alias:"eta",aliasTitles:{eta:"Eta"}},elixir:{title:"Elixir",owner:"Golmote"},elm:{title:"Elm",owner:"zwilias"},etlua:{title:"Embedded Lua templating",require:["lua","markup-templating"],owner:"RunDevelopment"},erb:{title:"ERB",require:["ruby","markup-templating"],owner:"Golmote"},erlang:{title:"Erlang",owner:"Golmote"},"excel-formula":{title:"Excel Formula",alias:["xlsx","xls"],owner:"RunDevelopment"},fsharp:{title:"F#",require:"clike",owner:"simonreynolds7"},factor:{title:"Factor",owner:"catb0t"},false:{title:"False",owner:"edukisto"},"firestore-security-rules":{title:"Firestore security rules",require:"clike",owner:"RunDevelopment"},flow:{title:"Flow",require:"javascript",owner:"Golmote"},fortran:{title:"Fortran",owner:"Golmote"},ftl:{title:"FreeMarker Template Language",require:"markup-templating",owner:"RunDevelopment"},gml:{title:"GameMaker Language",alias:"gamemakerlanguage",require:"clike",owner:"LiarOnce"},gap:{title:"GAP (CAS)",owner:"RunDevelopment"},gcode:{title:"G-code",owner:"RunDevelopment"},gdscript:{title:"GDScript",owner:"RunDevelopment"},gedcom:{title:"GEDCOM",owner:"Golmote"},gettext:{title:"gettext",alias:"po",owner:"RunDevelopment"},gherkin:{title:"Gherkin",owner:"hason"},git:{title:"Git",owner:"lgiraudel"},glsl:{title:"GLSL",require:"c",owner:"Golmote"},gn:{title:"GN",alias:"gni",owner:"RunDevelopment"},"linker-script":{title:"GNU Linker Script",alias:"ld",owner:"RunDevelopment"},go:{title:"Go",require:"clike",owner:"arnehormann"},"go-module":{title:"Go module",alias:"go-mod",owner:"RunDevelopment"},gradle:{title:"Gradle",require:"clike",owner:"zeabdelkhalek-badido18"},graphql:{title:"GraphQL",optional:"markdown",owner:"Golmote"},groovy:{title:"Groovy",require:"clike",owner:"robfletcher"},haml:{title:"Haml",require:"ruby",optional:["css","css-extras","coffeescript","erb","javascript","less","markdown","scss","textile"],owner:"Golmote"},handlebars:{title:"Handlebars",require:"markup-templating",alias:["hbs","mustache"],aliasTitles:{mustache:"Mustache"},owner:"Golmote"},haskell:{title:"Haskell",alias:"hs",owner:"bholst"},haxe:{title:"Haxe",require:"clike",optional:"regex",owner:"Golmote"},hcl:{title:"HCL",owner:"outsideris"},hlsl:{title:"HLSL",require:"c",owner:"RunDevelopment"},hoon:{title:"Hoon",owner:"matildepark"},http:{title:"HTTP",optional:["csp","css","hpkp","hsts","javascript","json","markup","uri"],owner:"danielgtaylor"},hpkp:{title:"HTTP Public-Key-Pins",owner:"ScottHelme"},hsts:{title:"HTTP Strict-Transport-Security",owner:"ScottHelme"},ichigojam:{title:"IchigoJam",owner:"BlueCocoa"},icon:{title:"Icon",owner:"Golmote"},"icu-message-format":{title:"ICU Message Format",owner:"RunDevelopment"},idris:{title:"Idris",alias:"idr",owner:"KeenS",require:"haskell"},ignore:{title:".ignore",owner:"osipxd",alias:["gitignore","hgignore","npmignore"],aliasTitles:{gitignore:".gitignore",hgignore:".hgignore",npmignore:".npmignore"}},inform7:{title:"Inform 7",owner:"Golmote"},ini:{title:"Ini",owner:"aviaryan"},io:{title:"Io",owner:"AlesTsurko"},j:{title:"J",owner:"Golmote"},java:{title:"Java",require:"clike",owner:"sherblot"},javadoc:{title:"JavaDoc",require:["markup","java","javadoclike"],modify:"java",optional:"scala",owner:"RunDevelopment"},javadoclike:{title:"JavaDoc-like",modify:["java","javascript","php"],owner:"RunDevelopment"},javastacktrace:{title:"Java stack trace",owner:"RunDevelopment"},jexl:{title:"Jexl",owner:"czosel"},jolie:{title:"Jolie",require:"clike",owner:"thesave"},jq:{title:"JQ",owner:"RunDevelopment"},jsdoc:{title:"JSDoc",require:["javascript","javadoclike","typescript"],modify:"javascript",optional:["actionscript","coffeescript"],owner:"RunDevelopment"},"js-extras":{title:"JS Extras",require:"javascript",modify:"javascript",optional:["actionscript","coffeescript","flow","n4js","typescript"],owner:"RunDevelopment"},json:{title:"JSON",alias:"webmanifest",aliasTitles:{webmanifest:"Web App Manifest"},owner:"CupOfTea696"},json5:{title:"JSON5",require:"json",owner:"RunDevelopment"},jsonp:{title:"JSONP",require:"json",owner:"RunDevelopment"},jsstacktrace:{title:"JS stack trace",owner:"sbrl"},"js-templates":{title:"JS Templates",require:"javascript",modify:"javascript",optional:["css","css-extras","graphql","markdown","markup","sql"],owner:"RunDevelopment"},julia:{title:"Julia",owner:"cdagnino"},keepalived:{title:"Keepalived Configure",owner:"dev-itsheng"},keyman:{title:"Keyman",owner:"mcdurdin"},kotlin:{title:"Kotlin",alias:["kt","kts"],aliasTitles:{kts:"Kotlin Script"},require:"clike",owner:"Golmote"},kumir:{title:"KuMir (\u041a\u0443\u041c\u0438\u0440)",alias:"kum",owner:"edukisto"},kusto:{title:"Kusto",owner:"RunDevelopment"},latex:{title:"LaTeX",alias:["tex","context"],aliasTitles:{tex:"TeX",context:"ConTeXt"},owner:"japborst"},latte:{title:"Latte",require:["clike","markup-templating","php"],owner:"nette"},less:{title:"Less",require:"css",optional:"css-extras",owner:"Golmote"},lilypond:{title:"LilyPond",require:"scheme",alias:"ly",owner:"RunDevelopment"},liquid:{title:"Liquid",require:"markup-templating",owner:"cinhtau"},lisp:{title:"Lisp",alias:["emacs","elisp","emacs-lisp"],owner:"JuanCaicedo"},livescript:{title:"LiveScript",owner:"Golmote"},llvm:{title:"LLVM IR",owner:"porglezomp"},log:{title:"Log file",optional:"javastacktrace",owner:"RunDevelopment"},lolcode:{title:"LOLCODE",owner:"Golmote"},lua:{title:"Lua",owner:"Golmote"},magma:{title:"Magma (CAS)",owner:"RunDevelopment"},makefile:{title:"Makefile",owner:"Golmote"},markdown:{title:"Markdown",require:"markup",optional:"yaml",alias:"md",owner:"Golmote"},"markup-templating":{title:"Markup templating",require:"markup",owner:"Golmote"},mata:{title:"Mata",owner:"RunDevelopment"},matlab:{title:"MATLAB",owner:"Golmote"},maxscript:{title:"MAXScript",owner:"RunDevelopment"},mel:{title:"MEL",owner:"Golmote"},mermaid:{title:"Mermaid",owner:"RunDevelopment"},metafont:{title:"METAFONT",owner:"LaeriExNihilo"},mizar:{title:"Mizar",owner:"Golmote"},mongodb:{title:"MongoDB",owner:"airs0urce",require:"javascript"},monkey:{title:"Monkey",owner:"Golmote"},moonscript:{title:"MoonScript",alias:"moon",owner:"RunDevelopment"},n1ql:{title:"N1QL",owner:"TMWilds"},n4js:{title:"N4JS",require:"javascript",optional:"jsdoc",alias:"n4jsd",owner:"bsmith-n4"},"nand2tetris-hdl":{title:"Nand To Tetris HDL",owner:"stephanmax"},naniscript:{title:"Naninovel Script",owner:"Elringus",alias:"nani"},nasm:{title:"NASM",owner:"rbmj"},neon:{title:"NEON",owner:"nette"},nevod:{title:"Nevod",owner:"nezaboodka"},nginx:{title:"nginx",owner:"volado"},nim:{title:"Nim",owner:"Golmote"},nix:{title:"Nix",owner:"Golmote"},nsis:{title:"NSIS",owner:"idleberg"},objectivec:{title:"Objective-C",require:"c",alias:"objc",owner:"uranusjr"},ocaml:{title:"OCaml",owner:"Golmote"},odin:{title:"Odin",owner:"edukisto"},opencl:{title:"OpenCL",require:"c",modify:["c","cpp"],owner:"Milania1"},openqasm:{title:"OpenQasm",alias:"qasm",owner:"RunDevelopment"},oz:{title:"Oz",owner:"Golmote"},parigp:{title:"PARI/GP",owner:"Golmote"},parser:{title:"Parser",require:"markup",owner:"Golmote"},pascal:{title:"Pascal",alias:"objectpascal",aliasTitles:{objectpascal:"Object Pascal"},owner:"Golmote"},pascaligo:{title:"Pascaligo",owner:"DefinitelyNotAGoat"},psl:{title:"PATROL Scripting Language",owner:"bertysentry"},pcaxis:{title:"PC-Axis",alias:"px",owner:"RunDevelopment"},peoplecode:{title:"PeopleCode",alias:"pcode",owner:"RunDevelopment"},perl:{title:"Perl",owner:"Golmote"},php:{title:"PHP",require:"markup-templating",owner:"milesj"},phpdoc:{title:"PHPDoc",require:["php","javadoclike"],modify:"php",owner:"RunDevelopment"},"php-extras":{title:"PHP Extras",require:"php",modify:"php",owner:"milesj"},"plant-uml":{title:"PlantUML",alias:"plantuml",owner:"RunDevelopment"},plsql:{title:"PL/SQL",require:"sql",owner:"Golmote"},powerquery:{title:"PowerQuery",alias:["pq","mscript"],owner:"peterbud"},powershell:{title:"PowerShell",owner:"nauzilus"},processing:{title:"Processing",require:"clike",owner:"Golmote"},prolog:{title:"Prolog",owner:"Golmote"},promql:{title:"PromQL",owner:"arendjr"},properties:{title:".properties",owner:"Golmote"},protobuf:{title:"Protocol Buffers",require:"clike",owner:"just-boris"},pug:{title:"Pug",require:["markup","javascript"],optional:["coffeescript","ejs","handlebars","less","livescript","markdown","scss","stylus","twig"],owner:"Golmote"},puppet:{title:"Puppet",owner:"Golmote"},pure:{title:"Pure",optional:["c","cpp","fortran"],owner:"Golmote"},purebasic:{title:"PureBasic",require:"clike",alias:"pbfasm",owner:"HeX0R101"},purescript:{title:"PureScript",require:"haskell",alias:"purs",owner:"sriharshachilakapati"},python:{title:"Python",alias:"py",owner:"multipetros"},qsharp:{title:"Q#",require:"clike",alias:"qs",owner:"fedonman"},q:{title:"Q (kdb+ database)",owner:"Golmote"},qml:{title:"QML",require:"javascript",owner:"RunDevelopment"},qore:{title:"Qore",require:"clike",owner:"temnroegg"},r:{title:"R",owner:"Golmote"},racket:{title:"Racket",require:"scheme",alias:"rkt",owner:"RunDevelopment"},cshtml:{title:"Razor C#",alias:"razor",require:["markup","csharp"],optional:["css","css-extras","javascript","js-extras"],owner:"RunDevelopment"},jsx:{title:"React JSX",require:["markup","javascript"],optional:["jsdoc","js-extras","js-templates"],owner:"vkbansal"},tsx:{title:"React TSX",require:["jsx","typescript"]},reason:{title:"Reason",require:"clike",owner:"Golmote"},regex:{title:"Regex",owner:"RunDevelopment"},rego:{title:"Rego",owner:"JordanSh"},renpy:{title:"Ren'py",alias:"rpy",owner:"HyuchiaDiego"},rescript:{title:"ReScript",alias:"res",owner:"vmarcosp"},rest:{title:"reST (reStructuredText)",owner:"Golmote"},rip:{title:"Rip",owner:"ravinggenius"},roboconf:{title:"Roboconf",owner:"Golmote"},robotframework:{title:"Robot Framework",alias:"robot",owner:"RunDevelopment"},ruby:{title:"Ruby",require:"clike",alias:"rb",owner:"samflores"},rust:{title:"Rust",owner:"Golmote"},sas:{title:"SAS",optional:["groovy","lua","sql"],owner:"Golmote"},sass:{title:"Sass (Sass)",require:"css",optional:"css-extras",owner:"Golmote"},scss:{title:"Sass (SCSS)",require:"css",optional:"css-extras",owner:"MoOx"},scala:{title:"Scala",require:"java",owner:"jozic"},scheme:{title:"Scheme",owner:"bacchus123"},"shell-session":{title:"Shell session",require:"bash",alias:["sh-session","shellsession"],owner:"RunDevelopment"},smali:{title:"Smali",owner:"RunDevelopment"},smalltalk:{title:"Smalltalk",owner:"Golmote"},smarty:{title:"Smarty",require:"markup-templating",optional:"php",owner:"Golmote"},sml:{title:"SML",alias:"smlnj",aliasTitles:{smlnj:"SML/NJ"},owner:"RunDevelopment"},solidity:{title:"Solidity (Ethereum)",alias:"sol",require:"clike",owner:"glachaud"},"solution-file":{title:"Solution file",alias:"sln",owner:"RunDevelopment"},soy:{title:"Soy (Closure Template)",require:"markup-templating",owner:"Golmote"},sparql:{title:"SPARQL",require:"turtle",owner:"Triply-Dev",alias:"rq"},"splunk-spl":{title:"Splunk SPL",owner:"RunDevelopment"},sqf:{title:"SQF: Status Quo Function (Arma 3)",require:"clike",owner:"RunDevelopment"},sql:{title:"SQL",owner:"multipetros"},squirrel:{title:"Squirrel",require:"clike",owner:"RunDevelopment"},stan:{title:"Stan",owner:"RunDevelopment"},stata:{title:"Stata Ado",require:["mata","java","python"],owner:"RunDevelopment"},iecst:{title:"Structured Text (IEC 61131-3)",owner:"serhioromano"},stylus:{title:"Stylus",owner:"vkbansal"},supercollider:{title:"SuperCollider",alias:"sclang",owner:"RunDevelopment"},swift:{title:"Swift",owner:"chrischares"},systemd:{title:"Systemd configuration file",owner:"RunDevelopment"},"t4-templating":{title:"T4 templating",owner:"RunDevelopment"},"t4-cs":{title:"T4 Text Templates (C#)",require:["t4-templating","csharp"],alias:"t4",owner:"RunDevelopment"},"t4-vb":{title:"T4 Text Templates (VB)",require:["t4-templating","vbnet"],owner:"RunDevelopment"},tap:{title:"TAP",owner:"isaacs",require:"yaml"},tcl:{title:"Tcl",owner:"PeterChaplin"},tt2:{title:"Template Toolkit 2",require:["clike","markup-templating"],owner:"gflohr"},textile:{title:"Textile",require:"markup",optional:"css",owner:"Golmote"},toml:{title:"TOML",owner:"RunDevelopment"},tremor:{title:"Tremor",alias:["trickle","troy"],owner:"darach",aliasTitles:{trickle:"trickle",troy:"troy"}},turtle:{title:"Turtle",alias:"trig",aliasTitles:{trig:"TriG"},owner:"jakubklimek"},twig:{title:"Twig",require:"markup-templating",owner:"brandonkelly"},typescript:{title:"TypeScript",require:"javascript",optional:"js-templates",alias:"ts",owner:"vkbansal"},typoscript:{title:"TypoScript",alias:"tsconfig",aliasTitles:{tsconfig:"TSConfig"},owner:"dkern"},unrealscript:{title:"UnrealScript",alias:["uscript","uc"],owner:"RunDevelopment"},uorazor:{title:"UO Razor Script",owner:"jaseowns"},uri:{title:"URI",alias:"url",aliasTitles:{url:"URL"},owner:"RunDevelopment"},v:{title:"V",require:"clike",owner:"taggon"},vala:{title:"Vala",require:"clike",optional:"regex",owner:"TemplarVolk"},vbnet:{title:"VB.Net",require:"basic",owner:"Bigsby"},velocity:{title:"Velocity",require:"markup",owner:"Golmote"},verilog:{title:"Verilog",owner:"a-rey"},vhdl:{title:"VHDL",owner:"a-rey"},vim:{title:"vim",owner:"westonganger"},"visual-basic":{title:"Visual Basic",alias:["vb","vba"],aliasTitles:{vba:"VBA"},owner:"Golmote"},warpscript:{title:"WarpScript",owner:"RunDevelopment"},wasm:{title:"WebAssembly",owner:"Golmote"},"web-idl":{title:"Web IDL",alias:"webidl",owner:"RunDevelopment"},wgsl:{title:"WGSL",owner:"Dr4gonthree"},wiki:{title:"Wiki markup",require:"markup",owner:"Golmote"},wolfram:{title:"Wolfram language",alias:["mathematica","nb","wl"],aliasTitles:{mathematica:"Mathematica",nb:"Mathematica Notebook"},owner:"msollami"},wren:{title:"Wren",owner:"clsource"},xeora:{title:"Xeora",require:"markup",alias:"xeoracube",aliasTitles:{xeoracube:"XeoraCube"},owner:"freakmaxi"},"xml-doc":{title:"XML doc (.net)",require:"markup",modify:["csharp","fsharp","vbnet"],owner:"RunDevelopment"},xojo:{title:"Xojo (REALbasic)",owner:"Golmote"},xquery:{title:"XQuery",require:"markup",owner:"Golmote"},yaml:{title:"YAML",alias:"yml",owner:"hason"},yang:{title:"YANG",owner:"RunDevelopment"},zig:{title:"Zig",owner:"RunDevelopment"}},plugins:{meta:{path:"plugins/{id}/prism-{id}",link:"plugins/{id}/"},"line-highlight":{title:"Line Highlight",description:"Highlights specific lines and/or line ranges."},"line-numbers":{title:"Line Numbers",description:"Line number at the beginning of code lines.",owner:"kuba-kubula"},"show-invisibles":{title:"Show Invisibles",description:"Show hidden characters such as tabs and line breaks.",optional:["autolinker","data-uri-highlight"]},autolinker:{title:"Autolinker",description:"Converts URLs and emails in code to clickable links. Parses Markdown links in comments."},wpd:{title:"WebPlatform Docs",description:'Makes tokens link to WebPlatform.org documentation. The links open in a new tab.'},"custom-class":{title:"Custom Class",description:"This plugin allows you to prefix Prism's default classes (.comment can become .namespace--comment) or replace them with your defined ones (like .editor__comment). You can even add new classes.",owner:"dvkndn",noCSS:!0},"file-highlight":{title:"File Highlight",description:"Fetch external files and highlight them with Prism. Used on the Prism website itself.",noCSS:!0},"show-language":{title:"Show Language",description:"Display the highlighted language in code blocks (inline code does not show the label).",owner:"nauzilus",noCSS:!0,require:"toolbar"},"jsonp-highlight":{title:"JSONP Highlight",description:"Fetch content with JSONP and highlight some interesting content (e.g. GitHub/Gists or Bitbucket API).",noCSS:!0,owner:"nauzilus"},"highlight-keywords":{title:"Highlight Keywords",description:"Adds special CSS classes for each keyword for fine-grained highlighting.",owner:"vkbansal",noCSS:!0},"remove-initial-line-feed":{title:"Remove initial line feed",description:"Removes the initial line feed in code blocks.",owner:"Golmote",noCSS:!0},"inline-color":{title:"Inline color",description:"Adds a small inline preview for colors in style sheets.",require:"css-extras",owner:"RunDevelopment"},previewers:{title:"Previewers",description:"Previewers for angles, colors, gradients, easing and time.",require:"css-extras",owner:"Golmote"},autoloader:{title:"Autoloader",description:"Automatically loads the needed languages to highlight the code blocks.",owner:"Golmote",noCSS:!0},"keep-markup":{title:"Keep Markup",description:"Prevents custom markup from being dropped out during highlighting.",owner:"Golmote",optional:"normalize-whitespace",noCSS:!0},"command-line":{title:"Command Line",description:"Display a command line with a prompt and, optionally, the output/response from the commands.",owner:"chriswells0"},"unescaped-markup":{title:"Unescaped Markup",description:"Write markup without having to escape anything."},"normalize-whitespace":{title:"Normalize Whitespace",description:"Supports multiple operations to normalize whitespace in code blocks.",owner:"zeitgeist87",optional:"unescaped-markup",noCSS:!0},"data-uri-highlight":{title:"Data-URI Highlight",description:"Highlights data-URI contents.",owner:"Golmote",noCSS:!0},toolbar:{title:"Toolbar",description:"Attach a toolbar for plugins to easily register buttons on the top of a code block.",owner:"mAAdhaTTah"},"copy-to-clipboard":{title:"Copy to Clipboard Button",description:"Add a button that copies the code block to the clipboard when clicked.",owner:"mAAdhaTTah",require:"toolbar",noCSS:!0},"download-button":{title:"Download Button",description:"A button in the toolbar of a code block adding a convenient way to download a code file.",owner:"Golmote",require:"toolbar",noCSS:!0},"match-braces":{title:"Match braces",description:"Highlights matching braces.",owner:"RunDevelopment"},"diff-highlight":{title:"Diff Highlight",description:"Highlights the code inside diff blocks.",owner:"RunDevelopment",require:"diff"},"filter-highlight-all":{title:"Filter highlightAll",description:"Filters the elements the highlightAll and highlightAllUnder methods actually highlight.",owner:"RunDevelopment",noCSS:!0},treeview:{title:"Treeview",description:"A language with special styles to highlight file system tree structures.",owner:"Golmote"}}})},8722:(e,t,n)=>{const r=n(6969),a=n(8380),o=new Set;function i(e){void 0===e?e=Object.keys(r.languages).filter((e=>"meta"!=e)):Array.isArray(e)||(e=[e]);const t=[...o,...Object.keys(Prism.languages)];a(r,e,t).load((e=>{if(!(e in r.languages))return void(i.silent||console.warn("Language does not exist: "+e));const t="./prism-"+e;delete n.c[n(3157).resolve(t)],delete Prism.languages[e],n(3157)(t),o.add(e)}))}i.silent=!1,e.exports=i},9700:()=>{!function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,a,o){if(n.language===r){var i=n.tokenStack=[];n.code=n.code.replace(a,(function(e){if("function"==typeof o&&!o(e))return e;for(var a,l=i.length;-1!==n.code.indexOf(a=t(r,l));)++l;return i[l]=e,a})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var a=0,o=Object.keys(n.tokenStack);!function i(l){for(var s=0;s=o.length);s++){var c=l[s];if("string"==typeof c||c.content&&"string"==typeof c.content){var u=o[a],d=n.tokenStack[u],p="string"==typeof c?c:c.content,f=t(r,u),m=p.indexOf(f);if(m>-1){++a;var h=p.substring(0,m),g=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=p.substring(m+f.length),y=[];h&&y.push.apply(y,i([h])),y.push(g),b&&y.push.apply(y,i([b])),"string"==typeof c?l.splice.apply(l,[s,1].concat(y)):c.content=y}}else c.content&&i(c.content)}return l}(n.tokens)}}}})}(Prism)},8692:(e,t,n)=>{var r={"./":8722};function a(e){var t=o(e);return n(t)}function o(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}a.keys=function(){return Object.keys(r)},a.resolve=o,e.exports=a,a.id=8692},3157:(e,t,n)=>{var r={"./":8722};function a(e){var t=o(e);return n(t)}function o(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}a.keys=function(){return Object.keys(r)},a.resolve=o,e.exports=a,a.id=3157},8380:e=>{"use strict";var t=function(){var e=function(){};function t(e,t){Array.isArray(e)?e.forEach(t):null!=e&&t(e,0)}function n(e){for(var t={},n=0,r=e.length;n "));var l={},s=e[r];if(s){function c(t){if(!(t in e))throw new Error(r+" depends on an unknown component "+t);if(!(t in l))for(var i in a(t,o),l[t]=!0,n[t])l[i]=!0}t(s.require,c),t(s.optional,c),t(s.modify,c)}n[r]=l,o.pop()}}return function(e){var t=n[e];return t||(a(e,r),t=n[e]),t}}function a(e){for(var t in e)return!0;return!1}return function(o,i,l){var s=function(e){var t={};for(var n in e){var r=e[n];for(var a in r)if("meta"!=a){var o=r[a];t[a]="string"==typeof o?{title:o}:o}}return t}(o),c=function(e){var n;return function(r){if(r in e)return r;if(!n)for(var a in n={},e){var o=e[a];t(o&&o.alias,(function(t){if(t in n)throw new Error(t+" cannot be alias for both "+a+" and "+n[t]);if(t in e)throw new Error(t+" cannot be alias of "+a+" because it is a component.");n[t]=a}))}return n[r]||r}}(s);i=i.map(c),l=(l||[]).map(c);var u=n(i),d=n(l);i.forEach((function e(n){var r=s[n];t(r&&r.require,(function(t){t in d||(u[t]=!0,e(t))}))}));for(var p,f=r(s),m=u;a(m);){for(var h in p={},m){var g=s[h];t(g&&g.modify,(function(e){e in d&&(p[e]=!0)}))}for(var b in d)if(!(b in u))for(var y in f(b))if(y in u){p[b]=!0;break}for(var v in m=p)u[v]=!0}var w={getIds:function(){var e=[];return w.load((function(t){e.push(t)})),e},load:function(t,n){return function(t,n,r,a){var o=a?a.series:void 0,i=a?a.parallel:e,l={},s={};function c(e){if(e in l)return l[e];s[e]=!0;var a,u=[];for(var d in t(e))d in n&&u.push(d);if(0===u.length)a=r(e);else{var p=i(u.map((function(e){var t=c(e);return delete s[e],t})));o?a=o(p,(function(){return r(e)})):r(e)}return l[e]=a}for(var u in n)c(u);var d=[];for(var p in s)d.push(l[p]);return i(d)}(f,u,t,n)}};return w}}();e.exports=t},2694:(e,t,n)=>{"use strict";var r=n(6925);function a(){}function o(){}o.resetWarningCache=a,e.exports=function(){function e(e,t,n,a,o,i){if(i!==r){var l=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw l.name="Invariant Violation",l}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:a};return n.PropTypes=n,n}},5556:(e,t,n)=>{e.exports=n(2694)()},6925:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},2551:(e,t,n)=>{"use strict";var r=n(6540),a=n(9982);function o(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n