├── .eslintrc.js
├── .github
├── dependabot.yml
└── workflows
│ ├── docs.yml
│ └── test.yml
├── .gitignore
├── .mocharc.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
├── .gitignore
├── .ruby-version
├── 404.html
├── Gemfile
├── Gemfile.lock
├── _config.yml
├── _sass
│ └── custom
│ │ └── custom.scss
├── adapters
│ ├── database.md
│ ├── index.md
│ ├── milestone.md
│ └── pub-sub.md
├── api
│ ├── agent.md
│ ├── backend.md
│ ├── connection.md
│ ├── doc.md
│ ├── index.md
│ ├── local-presence.md
│ ├── presence.md
│ ├── query.md
│ ├── sharedb-error.md
│ └── snapshot.md
├── document-history.md
├── getting-started.md
├── index.md
├── middleware
│ ├── actions.md
│ ├── index.md
│ ├── op-submission.md
│ └── registration.md
├── presence.md
├── projections.md
├── pub-sub.md
├── queries.md
└── types
│ ├── index.md
│ └── json0.md
├── examples
├── counter-json1-vite
│ ├── .gitignore
│ ├── index.html
│ ├── main.js
│ ├── package.json
│ ├── server.js
│ └── vite.config.js
├── counter-json1
│ ├── .gitignore
│ ├── README.md
│ ├── client.js
│ ├── demo.gif
│ ├── package.json
│ ├── server.js
│ └── static
│ │ └── index.html
├── counter
│ ├── .gitignore
│ ├── README.md
│ ├── client.js
│ ├── demo.gif
│ ├── package.json
│ ├── server.js
│ └── static
│ │ └── index.html
├── leaderboard
│ ├── .gitignore
│ ├── README.md
│ ├── client
│ │ ├── Body.jsx
│ │ ├── Leaderboard.jsx
│ │ ├── Player.jsx
│ │ ├── PlayerList.jsx
│ │ ├── PlayerSelector.jsx
│ │ ├── connection.js
│ │ └── index.jsx
│ ├── demo.gif
│ ├── package.json
│ ├── server
│ │ └── index.js
│ └── static
│ │ ├── index.html
│ │ └── leaderboard.css
├── rich-text-presence
│ ├── .gitignore
│ ├── README.md
│ ├── client.js
│ ├── package.json
│ ├── server.js
│ └── static
│ │ ├── index.html
│ │ └── style.css
├── rich-text
│ ├── .gitignore
│ ├── README.md
│ ├── client.js
│ ├── package.json
│ ├── server.js
│ └── static
│ │ └── index.html
└── textarea
│ ├── .gitignore
│ ├── README.md
│ ├── client.js
│ ├── package.json
│ ├── server.js
│ └── static
│ └── index.html
├── lib
├── agent.js
├── backend.js
├── client
│ ├── connection.js
│ ├── doc.js
│ ├── index.js
│ ├── presence
│ │ ├── doc-presence-emitter.js
│ │ ├── doc-presence.js
│ │ ├── local-doc-presence.js
│ │ ├── local-presence.js
│ │ ├── presence.js
│ │ ├── remote-doc-presence.js
│ │ └── remote-presence.js
│ ├── query.js
│ └── snapshot-request
│ │ ├── snapshot-request.js
│ │ ├── snapshot-timestamp-request.js
│ │ └── snapshot-version-request.js
├── db
│ ├── index.js
│ └── memory.js
├── emitter.js
├── error.js
├── index.js
├── logger
│ ├── index.js
│ └── logger.js
├── message-actions.js
├── milestone-db
│ ├── index.js
│ ├── memory.js
│ └── no-op.js
├── next-tick.js
├── op-stream.js
├── ot.js
├── projections.js
├── protocol.js
├── pubsub
│ ├── index.js
│ └── memory.js
├── query-emitter.js
├── read-snapshots-request.js
├── snapshot.js
├── stream-socket.js
├── submit-request.js
├── types.js
└── util.js
├── package.json
├── scripts
└── test-sharedb-mongo.sh
└── test
├── .jshintrc
├── BasicQueryableMemoryDB.js
├── agent.js
├── backend.js
├── client
├── connection.js
├── deserialized-type.js
├── doc.js
├── number-type.js
├── pending.js
├── presence
│ ├── doc-presence-emitter.js
│ ├── doc-presence.js
│ ├── presence-pauser.js
│ ├── presence-test-type.js
│ └── presence.js
├── projections.js
├── query-subscribe.js
├── query.js
├── snapshot-timestamp-request.js
├── snapshot-version-request.js
├── submit-json1.js
├── submit.js
└── subscribe.js
├── db-memory.js
├── db.js
├── error.js
├── logger.js
├── middleware.js
├── milestone-db-memory.js
├── milestone-db.js
├── next-tick.js
├── ot.js
├── projections.js
├── protocol.js
├── pubsub-memory.js
├── pubsub.js
├── setup.js
└── util.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has
2 | // been set, it will still error even if it's not applicable to that version number. Since Google sets these
3 | // rules, we have to turn them off ourselves.
4 | var DISABLED_ES6_OPTIONS = {
5 | 'no-var': 'off',
6 | 'prefer-rest-params': 'off'
7 | };
8 |
9 | var SHAREDB_RULES = {
10 | // Comma dangle is not supported in ES3
11 | 'comma-dangle': ['error', 'never'],
12 | // We control our own objects and prototypes, so no need for this check
13 | 'guard-for-in': 'off',
14 | // Google prescribes different indents for different cases. Let's just use 2 spaces everywhere. Note that we have
15 | // to override ESLint's default of 0 indents for this.
16 | indent: ['error', 2, {
17 | SwitchCase: 1
18 | }],
19 | // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code
20 | 'max-len': ['error',
21 | {
22 | code: 120,
23 | tabWidth: 2,
24 | ignoreUrls: true
25 | }
26 | ],
27 | // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused
28 | // variables
29 | 'no-unused-vars': ['error', {vars: 'all', args: 'after-used'}],
30 | // It's more readable to ensure we only have one statement per line
31 | 'max-statements-per-line': ['error', {max: 1}],
32 | // ES3 doesn't support spread
33 | 'prefer-spread': 'off',
34 | // as-needed quote props are easier to write
35 | 'quote-props': ['error', 'as-needed'],
36 | 'require-jsdoc': 'off',
37 | 'valid-jsdoc': 'off'
38 | };
39 |
40 | module.exports = {
41 | extends: 'google',
42 | parserOptions: {
43 | ecmaVersion: 3,
44 | allowReserved: true
45 | },
46 | rules: Object.assign(
47 | {},
48 | DISABLED_ES6_OPTIONS,
49 | SHAREDB_RULES
50 | ),
51 | ignorePatterns: [
52 | '/docs/'
53 | ],
54 | overrides: [
55 | {
56 | files: ['examples/counter-json1-vite/*.js'],
57 | parserOptions: {
58 | ecmaVersion: 6,
59 | sourceType: 'module',
60 | allowReserved: false
61 | },
62 | rules: {
63 | quotes: ['error', 'single']
64 | }
65 | }
66 | ]
67 | };
68 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Docs
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | paths:
8 | - docs/**
9 | pull_request:
10 | branches:
11 | - master
12 | paths:
13 | - docs/**
14 |
15 | jobs:
16 | docs:
17 | name: Jekyll
18 | runs-on: ubuntu-latest
19 | timeout-minutes: 10
20 | steps:
21 | - uses: actions/checkout@v4
22 | - uses: ruby/setup-ruby@v1
23 | with:
24 | ruby-version: '2.7'
25 | - name: Install
26 | run: cd docs && gem install bundler -v 2.4.22 && bundle install
27 | - name: Build
28 | run: cd docs && bundle exec jekyll build
29 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | test:
13 | name: Node ${{ matrix.node }}
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node:
18 | - 18
19 | - 20
20 | - 22
21 | services:
22 | mongodb:
23 | image: mongo:4.4
24 | ports:
25 | - 27017:27017
26 | timeout-minutes: 10
27 | steps:
28 | - uses: actions/checkout@v4
29 | - uses: actions/setup-node@v4
30 | with:
31 | node-version: ${{ matrix.node }}
32 | - name: Install
33 | run: npm install
34 | - name: Lint
35 | run: npm run lint
36 | - name: Test
37 | run: npm run test-cover
38 | - name: Coveralls
39 | uses: coverallsapp/github-action@master
40 | with:
41 | github-token: ${{ secrets.GITHUB_TOKEN }}
42 | flag-name: node-${{ matrix.node }}
43 | parallel: true
44 | - name: Test sharedb-mongo
45 | run: scripts/test-sharedb-mongo.sh
46 |
47 | finish:
48 | needs: test
49 | runs-on: ubuntu-latest
50 | steps:
51 | - name: Submit coverage
52 | uses: coverallsapp/github-action@master
53 | with:
54 | github-token: ${{ secrets.GITHUB_TOKEN }}
55 | parallel-finished: true
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | *.swp
3 | *.DS_Store
4 |
5 | # WebStorm
6 | .idea/
7 |
8 | # Emacs
9 | \#*\#
10 |
11 | # VS Code
12 | .vscode/
13 |
14 | # Logs
15 | logs
16 | *.log
17 | npm-debug.log*
18 |
19 | # Runtime data
20 | pids
21 | *.pid
22 | *.seed
23 | *.pid.lock
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Dependency directories
35 | node_modules
36 | package-lock.json
37 | jspm_packages
38 |
39 | # Don't commit generated JS bundles
40 | examples/**/static/dist/bundle.js
41 | examples/**/dist
42 |
--------------------------------------------------------------------------------
/.mocharc.yml:
--------------------------------------------------------------------------------
1 | reporter: spec
2 | check-leaks: true
3 | recursive: true
4 | file: test/setup.js
5 | globals:
6 | - MessageChannel # Set/unset to test nextTick()
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v2.0
2 |
3 | ### Breaking changes
4 |
5 | * Drop Node.js v10 support
6 |
7 | ## v1.0-beta
8 |
9 | ### Breaking changes
10 |
11 | * Add options argument to all public database adapter methods that read
12 | or write from snapshots or ops.
13 |
14 | * DB methods that get snapshots or ops no longer return metadata unless
15 | `{metadata: true}` option is passed.
16 |
17 | * Replace `source` argument with `options` in doc methods. Use `options.source`
18 | instead.
19 |
20 | * Backend streams now write objects intead of strings.
21 |
22 | * MemoryDB.prototype._querySync now returns `{snapshots: ..., extra: ...}`
23 | instead of just an array of snapshots.
24 |
25 | ### Non-breaking changes
26 |
27 | * Add options argument to backend.submit.
28 |
29 | * Add error codes to all errors.
30 |
31 | * Add `'updated'` event on queries which fires on all query result changes.
32 |
33 | * In clients, wrap errors in Error objects to they get passed through event
34 | emitters.
35 |
36 | * Sanitize stack traces when sending errors to client, but log them on the
37 | server.
38 |
39 | ## v0.11.37
40 |
41 | Beginning of changelog.
42 |
43 | If you're upgrading from ShareJS 0.7 or earlier,
44 | take a look at the [ShareJS upgrade guide](docs/upgrading-from-sharejs.md).
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Licensed under the standard MIT license:
2 |
3 | Copyright 2015 Nate Smith, Joseph Gentle
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ShareDB
2 |
3 | [](https://npmjs.org/package/sharedb)
4 | 
5 | [](https://coveralls.io/github/share/sharedb?branch=master)
6 |
7 | ShareDB is a realtime database backend based on [Operational Transformation
8 | (OT)](https://en.wikipedia.org/wiki/Operational_transformation) of JSON
9 | documents. It is the realtime backend for the [DerbyJS web application
10 | framework](http://derbyjs.com/).
11 |
12 | For help, questions, discussion and announcements, join the [ShareJS mailing
13 | list](https://groups.google.com/forum/?fromgroups#!forum/sharejs) or [read the documentation](https://share.github.io/sharedb/
14 | ).
15 |
16 | Please report any bugs you find to the [issue
17 | tracker](https://github.com/share/sharedb/issues).
18 |
19 | ## Features
20 |
21 | - Realtime synchronization of any JSON document
22 | - Concurrent multi-user collaboration
23 | - Synchronous editing API with asynchronous eventual consistency
24 | - Realtime query subscriptions
25 | - Simple integration with any database
26 | - Horizontally scalable with pub/sub integration
27 | - Projections to select desired fields from documents and operations
28 | - Middleware for implementing access control and custom extensions
29 | - Ideal for use in browsers or on the server
30 | - Offline change syncing upon reconnection
31 | - In-memory implementations of database and pub/sub for unit testing
32 | - Access to historic document versions
33 | - Realtime user presence syncing
34 |
35 | ## Documentation
36 |
37 | https://share.github.io/sharedb/
38 |
39 | ## Examples
40 |
41 | ### Counter
42 |
43 | [](examples/counter)
44 |
45 | ### Leaderboard
46 |
47 | [](examples/leaderboard)
48 |
49 | ## Development
50 |
51 | ### Documentation
52 |
53 | The documentation is stored as Markdown files, but sometimes it can be useful to run these locally. The docs are served using [Jekyll](https://jekyllrb.com/), and require Ruby >2.4.0 and [Bundler](https://bundler.io/):
54 |
55 | ```bash
56 | gem install jekyll bundler
57 | ```
58 |
59 | The docs can be built locally and served with live reload:
60 |
61 | ```bash
62 | npm run docs:install
63 | npm run docs:start
64 | ```
65 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | .sass-cache
3 | .jekyll-metadata
4 |
--------------------------------------------------------------------------------
/docs/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.7.3
2 |
--------------------------------------------------------------------------------
/docs/404.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
18 |
19 |
20 |
404
21 |
22 |
Page not found :(
23 |
The requested page could not be found.
24 |
25 |
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Hello! This is where you manage which Jekyll version is used to run.
4 | # When you want to use a different version, change it below, save the
5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
6 | #
7 | # bundle exec jekyll serve
8 | #
9 | # This will help ensure the proper Jekyll version is running.
10 | # Happy Jekylling!
11 |
12 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
13 | # uncomment the line below. To upgrade, run `bundle update github-pages`.
14 | gem "github-pages", "~> 228", group: :jekyll_plugins
15 |
16 | # If you have any plugins, put them here!
17 | group :jekyll_plugins do
18 | gem "jekyll-feed", "~> 0.15"
19 | end
20 |
21 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
22 | # and associated library.
23 | install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
24 | gem "tzinfo", "~> 1.2"
25 | gem "tzinfo-data"
26 | end
27 |
28 | # Performance-booster for watching directories on Windows
29 | gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform?
30 |
31 | # kramdown v2 ships without the gfm parser by default. If you're using
32 | # kramdown v1, comment out this line.
33 | gem "kramdown-parser-gfm"
34 |
35 | # Needed for Ruby 3: https://github.com/jekyll/jekyll/issues/8523
36 | gem "webrick", "~> 1.8"
37 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10 |
11 | # Site settings
12 | # These are used to personalize your new site. If you look in the HTML files,
13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
14 | # You can create any custom variable you would like, and they will be accessible
15 | # in the templates via {{ site.myvariable }}.
16 | title: ShareDB
17 | baseurl: "/sharedb" # the subpath of your site, e.g. /blog
18 | url: "" # the base hostname & protocol for your site, e.g. http://example.com
19 |
20 | aux_links:
21 | "ShareDB on GitHub":
22 | - "//github.com/share/sharedb"
23 |
24 | # Footer "Edit this page on GitHub" link text
25 | gh_edit_link: true # show or hide edit this page link
26 | gh_edit_link_text: "Edit this page on GitHub"
27 | gh_edit_repository: "https://github.com/share/sharedb" # the github URL for your repo
28 | gh_edit_branch: "master" # the branch that your docs is served from
29 | gh_edit_source: "docs" # the source that your files originate from
30 | gh_edit_view_mode: "tree" # "tree" or "edit" if you want the user to jump into the editor immediately
31 |
32 | # Build settings
33 | markdown: kramdown
34 | remote_theme: just-the-docs/just-the-docs
35 | permalink: /:path/:name
36 |
37 | # Exclude from processing.
38 | # The following items will not be processed, by default. Create a custom list
39 | # to override the default setting.
40 | # exclude:
41 | # - Gemfile
42 | # - Gemfile.lock
43 | # - node_modules
44 | # - vendor/bundle/
45 | # - vendor/cache/
46 | # - vendor/gems/
47 | # - vendor/ruby/
48 |
49 | # Global copy
50 | copy:
51 | events: This class extends [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter), and can be used with the normal methods such as [`on()`](https://nodejs.org/api/events.html#events_emitter_on_eventname_listener) and [`off()`](https://nodejs.org/api/events.html#events_emitter_off_eventname_listener).
52 |
--------------------------------------------------------------------------------
/docs/_sass/custom/custom.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Add call-out support: https://github.com/pmarsceill/just-the-docs/issues/171#issuecomment-538794741
3 | */
4 | $callouts: (
5 | info: ($blue-300, rgba($blue-000, .2), 'Note'),
6 | warn: ($yellow-300, rgba($yellow-000, .2), 'Note'),
7 | danger: ($red-300, rgba($red-000, .2), 'Note')
8 | );
9 |
10 | @each $class, $props in $callouts {
11 | .#{$class} {
12 | background: nth($props, 2);
13 | border-left: $border-radius solid nth($props, 1);
14 | border-radius: $border-radius;
15 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
16 | padding: .8rem;
17 |
18 | &::before {
19 | color: nth($props, 1);
20 | content: nth($props, 3);
21 | display: block;
22 | font-weight: bold;
23 | font-size: .75em;
24 | padding-bottom: .125rem;
25 | }
26 |
27 | br {
28 | content: '';
29 | display: block;
30 | margin-top: .5rem;
31 | }
32 | }
33 | }
34 |
35 | .label-grey {
36 | background: rgba($grey-dk-000, 1);
37 | }
38 |
--------------------------------------------------------------------------------
/docs/adapters/database.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Database adapters
3 | nav_order: 1
4 | layout: default
5 | parent: Adapters
6 | ---
7 |
8 | # Database adapters
9 | {: .no_toc }
10 |
11 | 1. TOC
12 | {:toc}
13 |
14 | The database adapter is responsible for persisting document contents and ops.
15 |
16 | ## Available adapters
17 |
18 | ### MemoryDB
19 |
20 | ShareDB ships with an in-memory, non-persistent database. This is useful for testing. It has no query support.
21 |
22 | {: .warn }
23 | `MemoryDB` does not persist its data between app restarts, and is **not** suitable for use in a Production environment.
24 |
25 | ### ShareDBMongo
26 |
27 | [`sharedb-mongo`](https://github.com/share/sharedb-mongo) is backed by MongoDB, with full query support.
28 |
29 | ### ShareDBMingoMemory
30 |
31 | [`sharedb-mingo-memory`](https://github.com/share/sharedb-mingo-memory) is an in-memory database that implements a subset of Mongo operations, including queries. This can be useful for testing against a MongoDB-like ShareDB instance.
32 |
33 | ### ShareDBPostgres
34 |
35 | [`sharedb-postgres`](https://github.com/share/sharedb-postgres) is backed by PostgreSQL, and has no query support.
36 |
37 | ## Usage
38 |
39 | An instance of a database adapter should be provided to the [`Backend()` constructor]({{ site.baseurl }}{% link api/backend.md %}#backend-constructor)'s `db` option:
40 |
41 | ```js
42 | const backend = new Backend({
43 | db: new MemoryDB(),
44 | })
45 | ```
46 |
--------------------------------------------------------------------------------
/docs/adapters/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Adapters
3 | nav_order: 4
4 | layout: default
5 | has_children: true
6 | ---
7 |
8 | # Adapters
9 |
10 | ShareDB tries to stay database-agnostic. Because of this, any data persisted to the database must be done through an appropriate database adapter.
11 |
--------------------------------------------------------------------------------
/docs/adapters/milestone.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Milestone adapters
3 | nav_order: 3
4 | layout: default
5 | parent: Adapters
6 | ---
7 |
8 | # Milestone adapters
9 | {: .no_toc }
10 |
11 | 1. TOC
12 | {:toc}
13 |
14 | The milestone adapter is responsible for storing periodic snapshots of documents, primarily in order to speed up [document history]({{ site.baseurl }}{% link document-history.md %}).
15 |
16 | ## Available adapters
17 |
18 | ### ShareDBMilestoneMongo
19 |
20 | [`sharedb-milestone-mongo`](https://github.com/share/sharedb-milestone-mongo) runs on MongoDB.
21 |
22 | ## Usage
23 |
24 | An instance of a milestone adapter should be provided to the [`Backend()` constructor]({{ site.baseurl }}{% link api/backend.md %}#backend-constructor)'s `milestoneDb` option:
25 |
26 | ```js
27 | const backend = new Backend({
28 | milestoneDb: new ShareDBMilestoneMongo(),
29 | })
30 | ```
31 |
32 | ## Requesting snapshots
33 |
34 | Adapters will define default snapshot behaviour. However, this logic can be overridden using the `saveMilestoneSnapshot` option in [middleware]({{ site.baseurl }}{% link middleware/index.md %}).
35 |
36 | Setting `context.saveMilestoneSnapshot` to `true` will request a snapshot be saved, and setting it to `false` means a snapshot will not be saved.
37 |
38 | {: .info }
39 | If `context.saveMilestoneSnapshot` is left to its default value of `null`, it will assume the default behaviour defined by the adapter.
40 |
41 | ```js
42 | shareDb.use('commit', (context, next) => {
43 | switch (context.collection) {
44 | case 'foo':
45 | // Save every 100 versions for collection 'foo'
46 | context.saveMilestoneSnapshot = context.snapshot.v % 100 === 0;
47 | break;
48 | case 'bar':
49 | case 'baz':
50 | // Save every 500 versions for collections 'bar' and 'baz'
51 | context.saveMilestoneSnapshot = context.snapshot.v % 500 === 0;
52 | break;
53 | default:
54 | // Don't save any milestones for collections not named here.
55 | context.saveMilestoneSnapshot = false;
56 | }
57 |
58 | next();
59 | });
60 | ```
61 |
--------------------------------------------------------------------------------
/docs/adapters/pub-sub.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Pub/Sub adapters
3 | nav_order: 2
4 | layout: default
5 | parent: Adapters
6 | ---
7 |
8 | # Pub/Sub adapters
9 | {: .no_toc }
10 |
11 | 1. TOC
12 | {:toc}
13 |
14 | The pub/sub adapter is responsible for notifying other ShareDB instances of changes to data.
15 |
16 | ## Available adapters
17 |
18 | ### MemoryPubSub
19 |
20 | ShareDB ships with an in-memory Pub/Sub, which can be used for a single, standalone ShareDB instance.
21 |
22 | {: .info }
23 | Unlike the [database adapter]({{ site.baseurl }}{% link adapters/database.md %}), the in-memory Pub/Sub adapter **is** suitable for use in a Production environment, where only a single, standalone ShareDB instance is being used.
24 |
25 | ### ShareDBRedisPubSub
26 |
27 | [`sharedb-redis-pubsub`](https://github.com/share/sharedb-redis-pubsub) runs on Redis.
28 |
29 | ### ShareDBWSBusPubSub
30 |
31 | [`sharedb-wsbus-pubsub`](https://github.com/dmapper/sharedb-wsbus-pubsub) runs on ws-bus.
32 |
33 | ## Usage
34 |
35 | An instance of a pub/sub adapter should be provided to the [`Backend()` constructor]({{ site.baseurl }}{% link api/backend.md %}#backend-constructor)'s `pubsub` option:
36 |
37 | ```js
38 | const backend = new Backend({
39 | pubsub: new MemoryPubSub(),
40 | })
41 | ```
42 |
--------------------------------------------------------------------------------
/docs/api/agent.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Agent
3 | layout: default
4 | parent: API
5 | ---
6 |
7 | # Agent
8 |
9 | {: .no_toc }
10 |
11 | 1. TOC
12 | {:toc}
13 |
14 | An `Agent` is the representation of a client's [`Connection`]({{ site.baseurl }}{% link api/connection.md %}) state on the server.
15 |
16 | The `Agent` instance will be made available in all [middleware]({{ site.baseurl }}{% link middleware/index.md %}) contexts, where [`agent.custom`](#custom--object) can be particularly useful for storing custom context.
17 |
18 | {: .info }
19 | If the `Connection` was created through [`backend.connect()`]({{ site.baseurl }}{% link api/backend.md %}#connect()) (i.e. the client is running on the server), then the `Agent` associated with a `Connection` can be accessed through [`connection.agent`]({{ site.baseurl }}{% link api/connection.md %}#agent--agent).
20 |
21 | ## Properties
22 |
23 | ### `custom` -- Object
24 |
25 | > An object that consumers can use to pass information around through the middleware.
26 |
27 | {: .info }
28 | The `agent.custom` is passed onto the `options` field in [database adapter]({{ site.baseurl }}{% link adapters/database.md %}) calls as `options.agentCustom`. This allows further customisation at the database level e.g. [in `sharedb-mongo` middleware](https://github.com/share/sharedb-mongo#middlewares).
29 |
30 | ### `backend` -- [Backend]({{ site.baseurl }}{% link api/backend.md %})
31 |
32 | > The [`Backend`]({{ site.baseurl }}{% link api/backend.md %}) instance that created this `Agent`
33 |
--------------------------------------------------------------------------------
/docs/api/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: API
3 | nav_order: 99
4 | layout: default
5 | has_children: true
6 | ---
7 |
8 | # API
9 |
--------------------------------------------------------------------------------
/docs/api/local-presence.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: LocalPresence
3 | layout: default
4 | parent: API
5 | ---
6 |
7 | # LocalPresence
8 | {: .no_toc }
9 |
10 | 1. TOC
11 | {:toc}
12 |
13 | `LocalPresence` represents the [presence]({{ site.baseurl }}{% link presence.md %}) of the local client. For example, this might be the position of a caret in a text document; which field has been highlighted in a complex JSON object; etc.
14 |
15 | Local presence is created from a parent [`Presence`]({{ site.baseurl }}{% link api/presence.md %}) instance using [`presence.create()`]({{ site.baseurl }}{% link api/presence.md %}#create).
16 |
17 | ## Methods
18 |
19 | ### submit()
20 |
21 | Update the local representation of presence, and broadcast that presence to any other presence subscribers.
22 |
23 | ```javascript
24 | localPresence.submit(presence [, callback])
25 | ```
26 |
27 | `presence` -- Object
28 |
29 | > The presence object to broadcast. The structure of `presence` will depend on the [type]({{ site.baseurl }}{% link types/index.md %})
30 |
31 | {: .info }
32 | > A value of `null` will be interpreted as the client no longer being present
33 |
34 | {: .d-inline-block }
35 |
36 | `callback` -- Function
37 |
38 | Optional
39 | {: .label .label-grey }
40 |
41 | > ```js
42 | > function(error) { ... }
43 | > ```
44 |
45 | > A callback that will be called once the presence has been sent
46 |
47 | ### send()
48 |
49 | Send the current value of presence to other subscribers, without updating it. This is like [`submit()`](#submit) but without changing the value.
50 |
51 | This can be useful if local presence is set to periodically expire (e.g. after a period of inactivity).
52 |
53 | ```javascript
54 | localPresence.send([callback])
55 | ```
56 |
57 | {: .d-inline-block }
58 |
59 | `callback` -- Function
60 |
61 | Optional
62 | {: .label .label-grey }
63 |
64 | > ```js
65 | > function(error) { ... }
66 | > ```
67 |
68 | > A callback that will be called once the presence has been sent
69 |
70 | ### destroy()
71 |
72 | Inform all remote clients that this presence is now `null`, and deletes itself for garbage collection.
73 |
74 | ```javascript
75 | localPresence.destroy([callback])
76 | ```
77 |
78 | {: .d-inline-block }
79 |
80 | `callback` -- Function
81 |
82 | Optional
83 | {: .label .label-grey }
84 |
85 | > ```js
86 | > function(error) { ... }
87 | > ```
88 |
89 | > A callback that will be called once the presence has been destroyed
90 |
--------------------------------------------------------------------------------
/docs/api/presence.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Presence
3 | layout: default
4 | parent: API
5 | ---
6 |
7 | # Presence
8 | {: .no_toc }
9 |
10 | 1. TOC
11 | {:toc}
12 |
13 | Representation of the [presence]({{ site.baseurl }}{% link presence.md %}) data associated with a given channel.
14 |
15 | A `Presence` instance can be obtained with [`connection.getPresence()`]({{ site.baseurl }}{% link api/connection.md %}#getpresence) or [`connection.getDocPresence()`]({{ site.baseurl }}{% link api/connection.md %}#getdocpresence).
16 |
17 | If created with [`connection.getDocPresence()`]({{ site.baseurl }}{% link api/connection.md %}#getdocpresence), this will represent the presence data associated with a given [`Doc`]({{ site.baseurl }}{% link api/doc.md %}).
18 |
19 | ## Properties
20 |
21 | ### `remotePresences` -- Object
22 |
23 | > Map of remote presence IDs to their values
24 |
25 | ### `localPresences` -- Object
26 |
27 | > Map of local presence IDs to their [`LocalPresence`]({{ site.baseurl }}{% link api/local-presence.md %}) instances
28 |
29 | ## Methods
30 |
31 | ### subscribe()
32 |
33 | Subscribe to presence updates from other clients.
34 |
35 | ```javascript
36 | presence.subscribe([callback])
37 | ```
38 |
39 | {: .warn }
40 | Presence can be submitted without subscribing, but remote clients will not be able to re-request presence from an unsubscribed client.
41 |
42 | {: .d-inline-block }
43 |
44 | `callback` -- Function
45 |
46 | Optional
47 | {: .label .label-grey }
48 |
49 | > ```js
50 | > function(error) { ... }
51 | > ```
52 |
53 | > A callback that will be called once the presence has been subscribed
54 |
55 | ### unsubscribe()
56 |
57 | Unsubscribe from presence updates from remote clients.
58 |
59 | ```javascript
60 | presence.unsubscribe([callback])
61 | ```
62 |
63 | {: .d-inline-block }
64 |
65 | `callback` -- Function
66 |
67 | Optional
68 | {: .label .label-grey }
69 |
70 | > ```js
71 | > function(error) { ... }
72 | > ```
73 |
74 | > A callback that will be called once the presence has been unsubscribed
75 |
76 | ### create()
77 |
78 | Create an instance of [`LocalPresence`]({{ site.baseurl }}{% link api/local-presence.md %}), which can be used to represent the client's presence. Many -- or none -- such local presences may exist on a `Presence` instance.
79 |
80 | ```javascript
81 | presence.create([presenceId])
82 | ```
83 |
84 | {: .d-inline-block }
85 |
86 | `presenceId` -- string
87 |
88 | Optional
89 | {: .label .label-grey }
90 |
91 | > A unique ID representing the local presence. If omitted, a random ID will be assigned
92 |
93 | {: .warn }
94 | > Depending on use-case, the same client may have **multiple** presences, so a user or client ID may not be appropriate to use as a presence ID.
95 |
96 | Return value
97 |
98 | > A new [`LocalPresence`]({{ site.baseurl }}{% link api/local-presence.md %}) instance
99 |
100 | ### destroy()
101 |
102 | Clear all [`LocalPresence`]({{ site.baseurl }}{% link api/local-presence.md %}) instances associated with this `Presence`, setting them all to have a value of `null`, and sending the update to remote subscribers.
103 |
104 | Also deletes this `Presence` instance for garbage-collection.
105 |
106 | ```javascript
107 | presence.destroy([callback])
108 | ```
109 |
110 | {: .d-inline-block }
111 |
112 | `callback` -- Function
113 |
114 | Optional
115 | {: .label .label-grey }
116 |
117 | > ```js
118 | > function(error) { ... }
119 | > ```
120 |
121 | > A callback that will be called once the presence has been destroyed
122 |
123 | ## Events
124 |
125 | {{ site.copy.events }}
126 |
127 | ### `'receive'`
128 |
129 | An update from a remote presence client has been received.
130 |
131 | ```javascript
132 | presence.on('receive', function(id, value) { ... });
133 | ```
134 |
135 | `id` -- string
136 |
137 | > The ID of the remote presence
138 |
139 | {: .warn }
140 | > The same client may have multiple presence IDs
141 |
142 | `value` -- Object
143 |
144 | > The presence value. The structure of this object will depend on the [type]({{ site.baseurl }}{% link types/index.md %})
145 |
146 | {: .info }
147 | > A `null` value means the remote client is no longer present in the document (e.g. they disconnected)
148 |
149 | ### `'error'`
150 |
151 | An error has occurred.
152 |
153 | ```javascript
154 | presence.on('error', function(error) { ... })
155 | ```
156 |
157 | `error` -- [ShareDBError]({{ site.baseurl }}{% link api/sharedb-error.md %})
158 |
159 | > The error that occurred
160 |
--------------------------------------------------------------------------------
/docs/api/query.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Query
3 | layout: default
4 | parent: API
5 | ---
6 |
7 | # Query
8 | {: .no_toc }
9 |
10 | 1. TOC
11 | {:toc}
12 |
13 | Representation of a query made through [`connection.createFetchQuery()`]({{ site.baseurl }}{% link api/connection.md %}#createfetchquery) or [`connection.createSubscribeQuery()`]({{ site.baseurl }}{% link api/connection.md %}#createsubscribequery).
14 |
15 | ## Properties
16 |
17 | ### `ready` -- boolean
18 |
19 | > Represents if results are ready and available in [`results`](#results)
20 |
21 | ### `results` -- Array
22 |
23 | > Query results, as an array of [`Doc`]({{ site.baseurl }}{% link api/doc.md %}) instances
24 |
25 | ### `extra` -- Object
26 |
27 | > Extra query results that are not an array of `Doc`s. Available for certain [database adapters]({{ site.baseurl }}{% link adapters/database.md %}) and queries
28 |
29 | ## Methods
30 |
31 | ### destroy()
32 |
33 | Unsubscribe and stop firing events.
34 |
35 | ```js
36 | query.destroy([callback])
37 | ```
38 |
39 | {: .d-inline-block }
40 |
41 | `callback` -- Function
42 |
43 | Optional
44 | {: .label .label-grey }
45 |
46 | > ```js
47 | > function(error) { ... }
48 | > ```
49 |
50 | > A callback that will be called when the query has been destroyed
51 |
52 | ## Events
53 |
54 | {{ site.copy.events }}
55 |
56 | ### `'ready'`
57 |
58 | The initial query results were loaded from the server. Triggered on [`connection.createFetchQuery()`]({{ site.baseurl }}{% link api/connection.md %}#createfetchquery) or [`connection.createSubscribeQuery()`]({{ site.baseurl }}{% link api/connection.md %}#createsubscribequery).
59 |
60 | ```js
61 | query.on('ready', function() { ... })
62 | ```
63 |
64 | ### `'changed'`
65 |
66 | The subscribed query results have changed. Fires only after a sequence of diffs are handled.
67 |
68 | ```js
69 | query.on('changed', function(results) { ... })
70 | ```
71 |
72 | `results` -- Array
73 |
74 | > Query results, as an array of [`Doc`]({{ site.baseurl }}{% link api/doc.md %}) instances
75 |
76 | ### `'insert'`
77 |
78 | A contiguous sequence of documents were added to the query results array.
79 |
80 | ```js
81 | query.on('insert', function(docs, index) { ... })
82 | ```
83 |
84 | `docs` -- Array
85 |
86 | > Array of inserted [`Doc`]({{ site.baseurl }}{% link api/doc.md %})s
87 |
88 | `index` -- number
89 |
90 | > The index at which the documents were inserted
91 |
92 | ### `'move'`
93 |
94 | A contiguous sequence of documents moved position in the query results array.
95 |
96 | ```js
97 | query.on('move', function(docs, from, to) { ... })
98 | ```
99 |
100 | `docs` -- Array
101 |
102 | > Array of moved [`Doc`]({{ site.baseurl }}{% link api/doc.md %})s
103 |
104 | `from` -- number
105 |
106 | > The index the documents were moved from
107 |
108 | `to` -- number
109 |
110 | > The index the documents were moved to
111 |
112 | ### `'remove'`
113 |
114 | A contiguous sequence of documents were removed from the query results array.
115 |
116 | ```js
117 | query.on('remove', function(docs, index) { ... })
118 | ```
119 |
120 | `docs` -- Array
121 |
122 | > Array of removed [`Doc`]({{ site.baseurl }}{% link api/doc.md %})s
123 |
124 | `index` -- number
125 |
126 | > The index at which the documents were removed
127 |
128 | ### `'extra'`
129 |
130 | The [`extra`](#extra--object) property was changed.
131 |
132 | ```js
133 | query.on('extra', function(extra) { ... })
134 | ```
135 |
136 | `extra` -- Object
137 |
138 | > The updated [`extra`](#extra--object) value
139 |
140 | ### `'error'`
141 |
142 | There was an error receiving updates to a subscription.
143 |
144 | ```js
145 | query.on('error', function(error) { ... })
146 | ```
147 |
148 | `error` -- [ShareDBError]({{ site.baseurl }}{% link api/sharedb-error.md %})
149 |
150 | > The error that occurred
--------------------------------------------------------------------------------
/docs/api/sharedb-error.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: ShareDBError
3 | layout: default
4 | parent: API
5 | ---
6 |
7 | # ShareDBError
8 | {: .no_toc }
9 |
10 | 1. TOC
11 | {:toc}
12 |
13 | Representation of an error, with a machine-parsable [code](#error-codes).
14 |
15 | ## Properties
16 |
17 | ### `code` -- string
18 |
19 | > A machine-parsable [code](#error-codes) representing the type of error
20 |
21 | ### `message` -- string
22 |
23 | > A human-readable message providing more detail about the error
24 |
25 | {: .warn }
26 | > Consumer code should never rely on the value of `message`, which may be fragile.
27 |
28 | ## Error codes
29 |
30 | ### `ERR_OP_SUBMIT_REJECTED`
31 |
32 | > The op submitted by the client has been rejected by the server for a non-critical reason.
33 |
34 | > When the client receives this code, it will attempt to roll back the rejected op, leaving the client in a usable state.
35 |
36 | > This error might be used as part of standard control flow. For example, consumers may define a middleware that validates document structure, and rejects operations that do not conform to this schema using this error code to reset the client to a valid state.
37 |
38 | ### `ERR_PENDING_OP_REMOVED_BY_OP_SUBMIT_REJECTED`
39 |
40 | > This may happen if server rejected op with ERR_OP_SUBMIT_REJECTED and the type is not invertible or there are some pending ops after the create op was rejected with ERR_OP_SUBMIT_REJECTED
41 |
42 | ### `ERR_OP_ALREADY_SUBMITTED`
43 |
44 | > The same op has been received by the server twice.
45 |
46 | > This is non-critical, and part of normal control flow, and is sent as an error in order to short-circuit the op processing. It is eventually swallowed by the server, and shouldn't need further handling.
47 |
48 | ### `ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND`
49 |
50 | > The ops needed to transform the submitted op up to the current version of the snapshot could not be found.
51 |
52 | > If a client on an old version of a document submits an op, that op needs to be transformed by all the ops that have been applied to the document in the meantime. If the server cannot fetch these ops from the database, then this error is returned.
53 |
54 | > The most common case of this would be ops being deleted from the database. For example, let's assume we have a TTL set up on the ops in our database. Let's also say we have a client that is so out-of-date that the op corresponding to its version has been deleted by the TTL policy. If this client then attempts to submit an op, the server will not be able to find the ops required to transform the op to apply to the current version of the snapshot.
55 |
56 | > Other causes of this error may be dropping the ops collection all together, or having the database corrupted in some other way.
57 |
58 | ### `ERR_MAX_SUBMIT_RETRIES_EXCEEDED`
59 |
60 | > The number of retries defined by the [`maxSubmitRetries`]({{ site.baseurl }}{% link api/backend.md %}#options) option has been exceeded by a submission.
61 |
62 | ### `ERR_DOC_ALREADY_CREATED`
63 |
64 | > The creation request has failed, because the document was already created by another client.
65 |
66 | > This can happen when two clients happen to simultaneously try to create the same document, and is potentially recoverable by simply fetching the already-created document.
67 |
68 | ### `ERR_DOC_WAS_DELETED`
69 |
70 | > The deletion request has failed, because the document was already deleted by another client.
71 |
72 | > This can happen when two clients happen to simultaneously try to delete the same document. Given that the end result is the same, this error can potentially just be ignored.
73 |
74 | ### `ERR_DOC_TYPE_NOT_RECOGNIZED`
75 |
76 | > The specified document [type]({{ site.baseurl }}{% link types/index.md %}) has not been registered with ShareDB.
77 |
78 | > This error can usually be remedied by remembering to [register]({{ site.baseurl }}{% link types/index.md %}#installing-other-types) any types you need.
79 |
80 | ### `ERR_DEFAULT_TYPE_MISMATCH`
81 |
82 | > The default type being used by the client does not match the default type expected by the server.
83 |
84 | > This will typically only happen when using a different default type to the built-in `json0` used by ShareDB by default (e.g. if using a fork). The exact same type must be used by both the client and the server, and should be registered as the default type:
85 |
86 | > ```javascript
87 | > var ShareDB = require('sharedb');
88 | > var forkedJson0 = require('forked-json0');
89 | >
90 | > // Make sure to also do this on your client
91 | > ShareDB.types.defaultType = forkedJson0.type;
92 | > ```
93 |
94 | ### `ERR_OP_NOT_ALLOWED_IN_PROJECTION`
95 |
96 | > The submitted op is not valid when applied to the projection.
97 |
98 | > This may happen if the op targets some property that is not included in the projection.
99 |
100 | ### `ERR_TYPE_CANNOT_BE_PROJECTED`
101 |
102 | > The document's type cannot be projected. [`json0`]({{ site.baseurl }}{% link types/json0.md %}) is currently the only type that supports projections.
103 |
104 | ### `ERR_NO_OP`
105 |
106 | > The submitted op resulted in a no-op, possibly after transformation by a remote op.
107 |
108 | > This is normal behavior and the client should swallow this error without bumping doc version.
--------------------------------------------------------------------------------
/docs/api/snapshot.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Snapshot
3 | layout: default
4 | parent: API
5 | ---
6 |
7 | # Snapshot
8 | {: .no_toc }
9 |
10 | 1. TOC
11 | {:toc}
12 |
13 | Represents a **read-only** ShareDB document at a particular version number.
14 |
15 | {: .info }
16 | Snapshots can **not** be used to manipulate the current version of the document stored in the database. That should be achieved by using a [`Doc`]({{ site.baseurl }}{% link api/doc.md %}).
17 |
18 | ## Properties
19 |
20 | ### `type` -- string
21 |
22 | > The URI of the document [type]({{ site.baseurl }}{% link types/index.md %})
23 |
24 | {: .info }
25 | > Document types can change between versions if the document is deleted, and created again.
26 |
27 | ### `data` -- Object
28 |
29 | > The snapshot data
30 |
31 | ### `v` -- number
32 |
33 | > The snapshot version
34 |
--------------------------------------------------------------------------------
/docs/document-history.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Document history
3 | nav_order: 9
4 | layout: default
5 | ---
6 |
7 | # Document history
8 |
9 | Since -- by default -- ShareDB stores all of the submitted operations, these operations can be used to reconstruct a document at any point in its history.
10 |
11 | ShareDB exposes two methods for this:
12 |
13 | - [`connection.fetchSnapshot()`]({{ site.baseurl }}{% link api/connection.md %}#fetchsnapshot) -- fetches a snapshot by version number
14 | - [`connection.fetchSnapshotByTimestamp()`]({{ site.baseurl }}{% link api/connection.md %}#fetchsnapshotbytimestamp) -- fetches a snapshot by UNIX timestamp
15 |
16 | {: .info }
17 | ShareDB doesn't support "branching" a document. Any historical snapshots fetched will be read-only.
18 |
19 | ## Milestone snapshots
20 |
21 | Since OT types are only optionally reversible, ShareDB rebuilds its historic snapshots by replaying ops all the way from creation to the requested version.
22 |
23 | Once documents reach a high version, rebuilding a document like this can get slow. In order to facilitate this, ShareDB supports Milestone Snapshots -- snapshots that are periodically saved, so that ShareDB can jump to the nearest snapshot, and rebuild from there.
24 |
25 | In order to benefit from this performance improvement, a [milestone adapter]({{ site.baseurl }}{% link adapters/milestone.md %}) should be configured.
26 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting started
3 | nav_order: 2
4 | layout: default
5 | ---
6 |
7 | # Getting started
8 | {: .no_toc }
9 |
10 | 1. TOC
11 | {:toc}
12 |
13 | ## Installation
14 |
15 | ShareDB is distributed through [npm](https://www.npmjs.com/package/sharedb):
16 |
17 | ```bash
18 | npm install --save sharedb
19 | ```
20 |
21 | If your server and client have separate dependencies, ShareDB should be added as a dependency to **both** packages.
22 |
23 |
24 | You may also wish to install other [OT types]({{ site.baseurl }}{% link types/index.md %}).
25 |
26 | ## Examples
27 |
28 | There are [working examples](https://github.com/share/sharedb/tree/master/examples) in the git repository.
29 |
30 | ## Usage
31 |
32 | ### Server
33 |
34 | The following is an example using [Express](https://expressjs.com/) and [ws](https://github.com/websockets/ws).
35 |
36 | The ShareDB backend expects an instance of a [`Stream`](https://nodejs.org/api/stream.html), so this example also uses [`@teamwork/websocket-json-stream`](https://www.npmjs.com/package/@teamwork/websocket-json-stream) to turn a `WebSocket` into a `Stream`.
37 |
38 | ```js
39 | var express = require('express')
40 | var WebSocket = require('ws')
41 | var http = require('http')
42 | var ShareDB = require('sharedb')
43 | var WebSocketJSONStream = require('@teamwork/websocket-json-stream')
44 |
45 | var app = express()
46 | var server = http.createServer(app)
47 | var webSocketServer = new WebSocket.Server({server: server})
48 |
49 | var backend = new ShareDB()
50 | webSocketServer.on('connection', (webSocket) => {
51 | var stream = new WebSocketJSONStream(webSocket)
52 | backend.listen(stream)
53 | })
54 |
55 | server.listen(8080)
56 | ```
57 |
58 | This server will accept any WebSocket connection on port 8080, and bind it to ShareDB.
59 |
60 |
61 |
62 |
63 |
64 | ### Client
65 |
66 | This client example uses [`reconnecting-websocket`](https://www.npmjs.com/package/reconnecting-websocket) to reconnect clients after a socket is closed.
67 |
68 | Try running the [working example](https://github.com/share/sharedb/tree/master/examples/counter) to see this in action.
69 |
70 | ```js
71 | var ReconnectingWebSocket = require('reconnecting-websocket')
72 | var Connection = require('sharedb/lib/client').Connection
73 |
74 | var socket = new ReconnectingWebSocket('ws://localhost:8080', [], {
75 | // ShareDB handles dropped messages, and buffering them while the socket
76 | // is closed has undefined behavior
77 | maxEnqueuedMessages: 0
78 | })
79 | var connection = new Connection(socket)
80 |
81 | var doc = connection.get('doc-collection', 'doc-id')
82 |
83 | doc.subscribe((error) => {
84 | if (error) return console.error(error)
85 |
86 | // If doc.type is undefined, the document has not been created, so let's create it
87 | if (!doc.type) {
88 | doc.create({counter: 0}, (error) => {
89 | if (error) console.error(error)
90 | })
91 | }
92 | });
93 |
94 | doc.on('op', (op) => {
95 | console.log('count', doc.data.counter)
96 | })
97 |
98 | window.increment = () => {
99 | // Increment the counter by 1
100 | doc.submitOp([{p: ['counter'], na: 1}])
101 | }
102 | ```
103 |
104 |
105 |
106 |
107 | {: .info }
108 | This example uses the `json0` type (ShareDB's default type).
109 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | nav_order: 1
4 | layout: default
5 | ---
6 |
7 | # Introduction
8 |
9 | ShareDB is a full-stack library for realtime JSON document collaboration. It provides a Node.js server for coordinating and committing edits from multiple clients. It also provides a JavaScript client for manipulating documents, which can be run either in Node.js or in the browser.
10 |
11 |
12 | The underlying conflict management is handled through [Operational Transformation (OT)](https://en.wikipedia.org/wiki/Operational_transformation). The implementation of this strategy is delegated to ShareDB's type plugins.
13 |
14 | ## Features
15 |
16 | - Realtime synchronization of any JSON document
17 | - Concurrent multi-user collaboration
18 | - Synchronous editing API with asynchronous eventual consistency
19 | - Realtime query subscriptions
20 |
21 | - Simple integration with any database
22 |
23 | - Horizontally scalable with pub/sub integration
24 | - Projections to select desired fields from documents and operations
25 | - Middleware for implementing access control and custom extensions
26 | - Ideal for use in browsers or on the server
27 | - Offline change syncing upon reconnection
28 | - In-memory implementations of database and pub/sub for unit testing
29 |
30 | - Access to historic document versions
31 |
32 | - Realtime user presence syncing
33 |
--------------------------------------------------------------------------------
/docs/middleware/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Middleware
3 | nav_order: 5
4 | layout: default
5 | has_children: true
6 |
7 | ---
8 |
9 | # Middleware
10 |
11 | Middleware enables consumers to hook into the ShareDB server pipeline. Objects can be asynchronously manipulated as they flow through ShareDB.
12 |
13 |
14 |
15 | This can be particularly useful for authentication, or adding metadata.
16 |
--------------------------------------------------------------------------------
/docs/middleware/registration.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Registration
3 | nav_order: 1
4 | layout: default
5 | parent: Middleware
6 | ---
7 |
8 | # Registering middleware
9 |
10 | Middleware is registered on the server with [`backend.use()`]({{ site.baseurl }}{% link api/backend.md %}#use):
11 |
12 | ```js
13 | backend.use(action, function(context, next) {
14 | // Do something with the context
15 |
16 | // Call next when ready. Can optionally pass an error to stop
17 | // the current action, and return the error to the client
18 | next(error)
19 | })
20 | ```
21 |
22 | Valid `action`s and their corresponding `context` shape can be found [here]({{ site.baseurl }}{% link middleware/actions.md %}).
23 |
--------------------------------------------------------------------------------
/docs/presence.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Presence
3 | nav_order: 10
4 | layout: default
5 | ---
6 |
7 | # Presence
8 |
9 | ShareDB supports sharing "presence": transient information about a client's whereabouts in a given document. For example, this might be their position in a text document; their mouse pointer coordinates on the screen; or a selected field in a form.
10 |
11 | {: .info }
12 | Presence needs to be enabled in the [`Backend`]({{ site.baseurl }}{% link api/backend.md %}).
13 |
14 | ## Usage
15 |
16 | ### Untyped presence
17 |
18 | Presence can be used independently of a document (for example, sharing a mouse pointer position).
19 |
20 | In this case, clients just need to subscribe to a common channel using [`connection.getPresence()`]({{ site.baseurl }}{% link api/connection.md %}#getpresence) to get a [`Presence`]({{ site.baseurl }}{% link api/presence.md %}) instance:
21 |
22 | ```js
23 | const presence = connection.getPresence('my-channel')
24 | presence.subscribe()
25 |
26 | presence.on('receive', (presenceId, update) => {
27 | if (update === null) {
28 | // The remote client is no longer present in the document
29 | } else {
30 | // Handle the new value by updating UI, etc.
31 | }
32 | })
33 | ```
34 |
35 | In order to send presence information to other clients, a [`LocalPresence`]({{ site.baseurl }}{% link api/local-presence.md %}) should be created. The presence object can take any arbitrary value
36 |
37 | ```js
38 | const localPresence = presence.create()
39 | // The presence value can take any shape
40 | localPresence.submit({foo: 'bar'})
41 | ```
42 |
43 | {: .info }
44 | Multiple local presences can be created from a single `presence` instance, which can be used to represent columnar text cursors, multi-touch input, etc.
45 |
46 | ### Typed presence
47 |
48 | Presence can be coupled to a particular document by getting a [`DocPresence`]({{ site.baseurl }}{% link api/presence.md %}) instance with [`connection.getDocPresence()`]({{ site.baseurl }}{% link api/doc.md %}#getdocpresence).
49 |
50 | The special thing about a `DocPresence` (as opposed to a `Presence`) instance is that `DocPresence` will automatically handle synchronisation issues. Since presence and ops are submitted independently of one another, they can arrive out-of-sync, which might make a text cursor jitter, for example. `DocPresence` will handle these cases, and make sure the correct presence is always applied to the correct version of a document.
51 |
52 | Support depends on the [type]({{ site.baseurl }}{% link types/index.md %}) being used.
53 |
54 | {: .info }
55 | Currently, only `rich-text` supports presence information
56 |
57 | Clients subscribe to a particular [`Doc`]({{ site.baseurl }}{% link api/doc.md %}) instead of a channel:
58 |
59 | ```js
60 | const presence = connection.getDocPresence(collection, id)
61 | presence.subscribe()
62 |
63 | presence.on('receive', (presenceId, update) => {
64 | if (update === null) {
65 | // The remote client is no longer present in the document
66 | } else {
67 | // Handle the new value by updating UI, etc.
68 | }
69 | })
70 | ```
71 |
72 | The shape of the presence value will be defined by the [type]({{ site.baseurl }}{% link types/index.md %}):
73 |
74 | ```js
75 | const localPresence = presence.create()
76 | // The presence value depends on the type
77 | localPresence.submit(value)
78 | ```
79 |
--------------------------------------------------------------------------------
/docs/projections.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Projections
3 | nav_order: 7
4 | layout: default
5 | ---
6 |
7 | # Projections
8 |
9 | Some [types]({{ site.baseurl }}{% link types/index.md %}) support exposing a projection of a real collection, with a specified set of allowed fields.
10 |
11 | {: .info }
12 | Currently, only [`json0`]({{ site.baseurl }}{% link types/json0.md %}) supports projections.
13 |
14 | Once configured, the projected collection looks just like a real collection -- except documents only have the fields that have been specified.
15 |
16 | Operations on the projected collection work, but only a small portion of the data can be seen and altered.
17 |
18 | ## Usage
19 |
20 | Projections are configured using [`backend.addProjection()`]({{ site.baseurl }}{% link api/backend.md %}#addprojection). For example, imagine we have a collection `users` with lots of information that should not be leaked. To add a projection `names`, which only has access to the `firstName` and `lastName` properties on a user:
21 |
22 | ```js
23 | backend.addProjection('names', 'users', {firstName: true, lastName: true})
24 | ```
25 |
26 | Once the projection has been defined, it can be interacted with like a "normal" collection:
27 |
28 | ```js
29 | const doc = connection.get('names', '123')
30 | doc.fetch(() => {
31 | // Only doc.data.firstName and doc.data.lastName will be present
32 | });
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/pub-sub.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Pub/Sub
3 | nav_order: 6
4 | layout: default
5 | ---
6 |
7 | # Pub/Sub
8 |
9 | Pub/Sub is used to communicate between multiple ShareDB instances. To communicate with more than one ShareDB instance, a [pub/sub adapter]({{ site.baseurl }}{% link adapters/pub-sub.md %}) should be configured.
10 |
--------------------------------------------------------------------------------
/docs/types/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: OT Types
3 | nav_order: 3
4 | layout: default
5 | has_children: true
6 | ---
7 |
8 | # OT Types
9 | {: .no_toc }
10 |
11 | 1. TOC
12 | {:toc}
13 |
14 | ShareDB provides a realtime collaborative platform based on [Operational Transformation (OT)](https://en.wikipedia.org/wiki/Operational_transformation). However, ShareDB itself is only part of the solution. ShareDB provides a lot of the machinery for handling ops, but does not provide the actual implementation for transforming ops.
15 |
16 | Transforming and handling ops is delegated to an underlying OT type.
17 |
18 | ShareDB ships with a single, default type -- [`json0`]({{ site.baseurl }}{% link types/json0.md %}).
19 |
20 | ## Registering types
21 |
22 | In order to use other OT types with ShareDB, they must first be registered.
23 |
24 | {: .warn }
25 | Types must be registered on **both** the server **and** the client.
26 |
27 | ### Server
28 |
29 | ```js
30 | const Backend = require('sharedb')
31 | const richText = require('rich-text')
32 |
33 | Backend.types.register(richText.type)
34 | ```
35 |
36 | ### Client
37 |
38 | ```js
39 | const Client = require('sharedb/lib/client')
40 | const richText = require('rich-text')
41 |
42 | Client.types.register(richText.type)
43 | ```
44 |
45 | ## Using types
46 |
47 | A [registered](#registering-types) type can be used by specifying its name or URI when creating a [`Doc`]({{ site.baseurl }}{% link api/doc.md %}):
48 |
49 | ```js
50 | doc.create([{insert: 'Lorem'}], 'http://sharejs.org/types/rich-text/v1')
51 | // The Doc will now use the type that it was created with when submitting more ops
52 | doc.submitOp([{retain: 5}, {insert: ' ipsum'}])
53 | ```
54 |
55 | {: .warn }
56 | The short-hand name can also be used (e.g. `'rich-text'`), but these don't have to be unique, so types may clash if multiple types with the same name have been registered. Best practice is to use the URI.
57 |
--------------------------------------------------------------------------------
/docs/types/json0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: JSON0
3 | layout: default
4 | nav_order: 2
5 | parent: OT Types
6 | ---
7 |
8 | # JSON0
9 |
10 | [`json0`](https://github.com/ottypes/json0) is ShareDB's default type. This means that if no other type is specified in [`doc.create()`]({{ site.baseurl }}{% link api/doc.md %}), `json0` will be used by default.
11 |
--------------------------------------------------------------------------------
/examples/counter-json1-vite/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/examples/counter-json1-vite/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ShareDB Counter (ottype json1 with Vite)
6 |
7 |
8 |
9 | You clicked times.
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/examples/counter-json1-vite/main.js:
--------------------------------------------------------------------------------
1 | import ReconnectingWebSocket from 'reconnecting-websocket';
2 | import {json1} from 'sharedb-client-browser/dist/ot-json1-umd.cjs';
3 | import sharedb from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
4 |
5 | // Open WebSocket connection to ShareDB server
6 | var socket = new ReconnectingWebSocket('ws://' + window.location.host + '/ws', [], {
7 | // ShareDB handles dropped messages, and buffering them while the socket
8 | // is closed has undefined behavior
9 | maxEnqueuedMessages: 0
10 | });
11 | sharedb.types.register(json1.type);
12 | var connection = new sharedb.Connection(socket);
13 |
14 | // Create local Doc instance mapped to 'examples' collection document with id 'counter'
15 | var doc = connection.get('examples', 'counter');
16 |
17 | // Get initial value of document and subscribe to changes
18 | doc.subscribe(showNumbers);
19 | // When document changes (by this client or any other, or the server),
20 | // update the number on the page
21 | doc.on('op', showNumbers);
22 |
23 | function showNumbers() {
24 | document.querySelector('#num-clicks').textContent = doc.data.numClicks;
25 | };
26 |
27 | // When clicking on the '+1' button, change the number in the local
28 | // document and sync the change to the server and other connected
29 | // clients
30 | function increment() {
31 | // Increment `doc.data.numClicks`. See
32 | // https://github.com/ottypes/json1/blob/master/spec.md for list of valid operations.
33 | doc.submitOp(['numClicks', {ena: 1}]);
34 | }
35 |
36 | var button = document.querySelector('button.increment');
37 | button.addEventListener('click', increment);
38 |
--------------------------------------------------------------------------------
/examples/counter-json1-vite/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-json1-vite",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "start": "node server.js"
11 | },
12 | "dependencies": {
13 | "@teamwork/websocket-json-stream": "^2.0.0",
14 | "express": "^4.18.2",
15 | "ot-json1": "^1.0.2",
16 | "reconnecting-websocket": "^4.4.0",
17 | "sharedb": "^3.3.1",
18 | "sharedb-client-browser": "^4.2.1",
19 | "ws": "^8.13.0"
20 | },
21 | "devDependencies": {
22 | "vite": "^4.2.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/counter-json1-vite/server.js:
--------------------------------------------------------------------------------
1 | import http from 'http';
2 | import express from 'express';
3 | import ShareDB from 'sharedb';
4 | import {WebSocketServer} from 'ws';
5 | import WebSocketJSONStream from '@teamwork/websocket-json-stream';
6 | import json1 from 'ot-json1';
7 |
8 | ShareDB.types.register(json1.type);
9 | var backend = new ShareDB();
10 | createDoc(startServer);
11 |
12 | // Create initial document then fire callback
13 | function createDoc(callback) {
14 | var connection = backend.connect();
15 | var doc = connection.get('examples', 'counter');
16 | doc.fetch(function(err) {
17 | if (err) throw err;
18 | if (doc.type === null) {
19 | doc.create({numClicks: 0}, json1.type.uri, callback);
20 | return;
21 | }
22 | callback();
23 | });
24 | }
25 |
26 | function startServer() {
27 | // Create a web server to serve files and listen to WebSocket connections
28 | var app = express();
29 | app.use(express.static('dist'));
30 | var server = http.createServer(app);
31 |
32 | // Connect any incoming WebSocket connection to ShareDB
33 | var wss = new WebSocketServer({server: server, path: '/ws'});
34 | wss.on('connection', function(ws) {
35 | var stream = new WebSocketJSONStream(ws);
36 | backend.listen(stream);
37 | });
38 |
39 | server.listen(8080);
40 | console.log('Listening on http://localhost:8080');
41 | }
42 |
--------------------------------------------------------------------------------
/examples/counter-json1-vite/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vite';
2 |
3 | export default defineConfig({
4 | server: {
5 | proxy: {
6 | // Proxy websockets to ws://localhost:8080 for `npm run dev`
7 | '/ws': {
8 | target: 'ws://localhost:8080',
9 | ws: true
10 | }
11 | }
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/examples/counter-json1/.gitignore:
--------------------------------------------------------------------------------
1 | static/dist/
2 |
--------------------------------------------------------------------------------
/examples/counter-json1/README.md:
--------------------------------------------------------------------------------
1 | # Simple realtime client/server sync with ShareDB (ottype json1)
2 |
3 | 
4 |
5 | This is a simple websocket server that exposes the ShareDB protocol,
6 | with a client showing an incrementing number that is sychronized
7 | across all open browser tabs.
8 |
9 | In this demo, data is not persisted. To persist data, run a Mongo
10 | server and initialize ShareDB with the
11 | [ShareDBMongo](https://github.com/share/sharedb-mongo) database adapter.
12 |
13 | ## Install dependencies
14 | ```
15 | npm install
16 | ```
17 |
18 | ## Build JavaScript bundle and run server
19 | ```
20 | npm run build && npm start
21 | ```
22 |
23 | ## Run app in browser
24 | Load [http://localhost:8080](http://localhost:8080)
25 |
--------------------------------------------------------------------------------
/examples/counter-json1/client.js:
--------------------------------------------------------------------------------
1 | var ReconnectingWebSocket = require('reconnecting-websocket');
2 | var sharedb = require('sharedb/lib/client');
3 | var json1 = require('ot-json1');
4 |
5 | // Open WebSocket connection to ShareDB server
6 | var socket = new ReconnectingWebSocket('ws://' + window.location.host, [], {
7 | // ShareDB handles dropped messages, and buffering them while the socket
8 | // is closed has undefined behavior
9 | maxEnqueuedMessages: 0
10 | });
11 | sharedb.types.register(json1.type);
12 | var connection = new sharedb.Connection(socket);
13 |
14 | // Create local Doc instance mapped to 'examples' collection document with id 'counter'
15 | var doc = connection.get('examples', 'counter');
16 |
17 | // Get initial value of document and subscribe to changes
18 | doc.subscribe(showNumbers);
19 | // When document changes (by this client or any other, or the server),
20 | // update the number on the page
21 | doc.on('op', showNumbers);
22 |
23 | function showNumbers() {
24 | document.querySelector('#num-clicks').textContent = doc.data.numClicks;
25 | };
26 |
27 | // When clicking on the '+1' button, change the number in the local
28 | // document and sync the change to the server and other connected
29 | // clients
30 | function increment() {
31 | // Increment `doc.data.numClicks`. See
32 | // https://github.com/ottypes/json1/blob/master/spec.md for list of valid operations.
33 | doc.submitOp(['numClicks', {ena: 1}]);
34 | }
35 |
36 | // Expose to index.html
37 | global.increment = increment;
38 |
--------------------------------------------------------------------------------
/examples/counter-json1/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/share/sharedb/a8f997bdae6bb6f502b8f55db81525c6e0236670/examples/counter-json1/demo.gif
--------------------------------------------------------------------------------
/examples/counter-json1/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sharedb-example-counter-json1",
3 | "version": "1.0.0",
4 | "description": "A simple client/server app using ShareDB (ottype json1) and WebSockets",
5 | "main": "server.js",
6 | "scripts": {
7 | "build": "browserify client.js -o static/dist/bundle.js",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node server.js"
10 | },
11 | "author": "Dmitry Kharitonov (https://dkharitonov.me/)",
12 | "contributors": [
13 | "Dmitry Kharitonov (https://dkharitonov.me/)",
14 | "Avital Oliver (https://aoliver.org/)",
15 | "Marko Bregant (https://bregant.si/)"
16 | ],
17 | "license": "MIT",
18 | "dependencies": {
19 | "@teamwork/websocket-json-stream": "^2.0.0",
20 | "express": "^4.18.2",
21 | "ot-json1": "^1.0.2",
22 | "reconnecting-websocket": "^4.4.0",
23 | "sharedb": "^3.3.0",
24 | "ws": "^8.12.1"
25 | },
26 | "devDependencies": {
27 | "browserify": "^17.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/counter-json1/server.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var express = require('express');
3 | var ShareDB = require('sharedb');
4 | var WebSocket = require('ws');
5 | var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
6 | var json1 = require('ot-json1');
7 |
8 | ShareDB.types.register(json1.type);
9 | var backend = new ShareDB();
10 | createDoc(startServer);
11 |
12 | // Create initial document then fire callback
13 | function createDoc(callback) {
14 | var connection = backend.connect();
15 | var doc = connection.get('examples', 'counter');
16 | doc.fetch(function(err) {
17 | if (err) throw err;
18 | if (doc.type === null) {
19 | doc.create({numClicks: 0}, json1.type.uri, callback);
20 | return;
21 | }
22 | callback();
23 | });
24 | }
25 |
26 | function startServer() {
27 | // Create a web server to serve files and listen to WebSocket connections
28 | var app = express();
29 | app.use(express.static('static'));
30 | var server = http.createServer(app);
31 |
32 | // Connect any incoming WebSocket connection to ShareDB
33 | var wss = new WebSocket.Server({server: server});
34 | wss.on('connection', function(ws) {
35 | var stream = new WebSocketJSONStream(ws);
36 | backend.listen(stream);
37 | });
38 |
39 | server.listen(8080);
40 | console.log('Listening on http://localhost:8080');
41 | }
42 |
--------------------------------------------------------------------------------
/examples/counter-json1/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ShareDB Counter (ottype json1)
4 |
5 |
6 | You clicked times.
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/examples/counter/.gitignore:
--------------------------------------------------------------------------------
1 | static/dist/
2 |
--------------------------------------------------------------------------------
/examples/counter/README.md:
--------------------------------------------------------------------------------
1 | # Simple realtime client/server sync with ShareDB
2 |
3 | 
4 |
5 | This is a simple websocket server that exposes the ShareDB protocol,
6 | with a client showing an incrementing number that is sychronized
7 | across all open browser tabs.
8 |
9 | In this demo, data is not persisted. To persist data, run a Mongo
10 | server and initialize ShareDB with the
11 | [ShareDBMongo](https://github.com/share/sharedb-mongo) database adapter.
12 |
13 | ## Install dependencies
14 | ```
15 | npm install
16 | ```
17 |
18 | ## Build JavaScript bundle and run server
19 | ```
20 | npm run build && npm start
21 | ```
22 |
23 | ## Run app in browser
24 | Load [http://localhost:8080](http://localhost:8080)
25 |
--------------------------------------------------------------------------------
/examples/counter/client.js:
--------------------------------------------------------------------------------
1 | var ReconnectingWebSocket = require('reconnecting-websocket');
2 | var sharedb = require('sharedb/lib/client');
3 |
4 | // Open WebSocket connection to ShareDB server
5 | var socket = new ReconnectingWebSocket('ws://' + window.location.host, [], {
6 | // ShareDB handles dropped messages, and buffering them while the socket
7 | // is closed has undefined behavior
8 | maxEnqueuedMessages: 0
9 | });
10 | var connection = new sharedb.Connection(socket);
11 |
12 | // Create local Doc instance mapped to 'examples' collection document with id 'counter'
13 | var doc = connection.get('examples', 'counter');
14 |
15 | // Get initial value of document and subscribe to changes
16 | doc.subscribe(showNumbers);
17 | // When document changes (by this client or any other, or the server),
18 | // update the number on the page
19 | doc.on('op', showNumbers);
20 |
21 | function showNumbers() {
22 | document.querySelector('#num-clicks').textContent = doc.data.numClicks;
23 | };
24 |
25 | // When clicking on the '+1' button, change the number in the local
26 | // document and sync the change to the server and other connected
27 | // clients
28 | function increment() {
29 | // Increment `doc.data.numClicks`. See
30 | // https://github.com/ottypes/json0 for list of valid operations.
31 | doc.submitOp([{p: ['numClicks'], na: 1}]);
32 | }
33 |
34 | // Expose to index.html
35 | global.increment = increment;
36 |
--------------------------------------------------------------------------------
/examples/counter/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/share/sharedb/a8f997bdae6bb6f502b8f55db81525c6e0236670/examples/counter/demo.gif
--------------------------------------------------------------------------------
/examples/counter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sharedb-example-counter",
3 | "version": "1.0.0",
4 | "description": "A simple client/server app using ShareDB and WebSockets",
5 | "main": "server.js",
6 | "scripts": {
7 | "build": "browserify client.js -o static/dist/bundle.js",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node server.js"
10 | },
11 | "author": "Dmitry Kharitonov (https://dkharitonov.me/)",
12 | "contributors": [
13 | "Dmitry Kharitonov (https://dkharitonov.me/)",
14 | "Avital Oliver (https://aoliver.org/)"
15 | ],
16 | "license": "MIT",
17 | "dependencies": {
18 | "@teamwork/websocket-json-stream": "^2.0.0",
19 | "express": "^4.18.2",
20 | "reconnecting-websocket": "^4.4.0",
21 | "sharedb": "^3.3.0",
22 | "ws": "^8.12.1"
23 | },
24 | "devDependencies": {
25 | "browserify": "^17.0.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/counter/server.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var express = require('express');
3 | var ShareDB = require('sharedb');
4 | var WebSocket = require('ws');
5 | var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
6 |
7 | var backend = new ShareDB();
8 | createDoc(startServer);
9 |
10 | // Create initial document then fire callback
11 | function createDoc(callback) {
12 | var connection = backend.connect();
13 | var doc = connection.get('examples', 'counter');
14 | doc.fetch(function(err) {
15 | if (err) throw err;
16 | if (doc.type === null) {
17 | doc.create({numClicks: 0}, callback);
18 | return;
19 | }
20 | callback();
21 | });
22 | }
23 |
24 | function startServer() {
25 | // Create a web server to serve files and listen to WebSocket connections
26 | var app = express();
27 | app.use(express.static('static'));
28 | var server = http.createServer(app);
29 |
30 | // Connect any incoming WebSocket connection to ShareDB
31 | var wss = new WebSocket.Server({server: server});
32 | wss.on('connection', function(ws) {
33 | var stream = new WebSocketJSONStream(ws);
34 | backend.listen(stream);
35 | });
36 |
37 | server.listen(8080);
38 | console.log('Listening on http://localhost:8080');
39 | }
40 |
--------------------------------------------------------------------------------
/examples/counter/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ShareDB Counter
4 |
5 |
6 | You clicked times.
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/examples/leaderboard/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 |
--------------------------------------------------------------------------------
/examples/leaderboard/README.md:
--------------------------------------------------------------------------------
1 | # Leaderboard
2 |
3 | 
4 |
5 | This is a port of [Leaderboard](https://github.com/percolatestudio/react-leaderboard) to
6 | ShareDB.
7 |
8 | In this demo, data is not persisted. To persist data, run a Mongo
9 | server and initialize ShareDB with the
10 | [ShareDBMongo](https://github.com/share/sharedb-mongo) database adapter.
11 |
12 | ## Install dependencies
13 |
14 | Make sure you're in the `examples/leaderboard` folder so that it uses the `package.json` located here).
15 | ```
16 | npm install
17 | ```
18 |
19 | ## Build JavaScript bundle and run server
20 | ```
21 | npm run build && npm start
22 | ```
23 |
24 | Finally, open the example app in the browser. It runs on port 8080 by default:
25 | [http://localhost:8080](http://localhost:8080)
26 |
27 | For testing out the real-time aspects of this demo, you'll want to open two browser windows!
28 |
--------------------------------------------------------------------------------
/examples/leaderboard/client/Body.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Leaderboard = require('./Leaderboard.jsx');
3 |
4 | function Body() {
5 | return (
6 |