Skip to content

Commit

Permalink
Merge pull request #641 from derbyjs/racer-doc-improvements
Browse files Browse the repository at this point in the history
[docs] Improvements to Racer model docs
  • Loading branch information
ericyhwang authored Jun 19, 2024
2 parents da237be + 84a1432 commit bdddccc
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 119 deletions.
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The site is viewable at `http://localhost:4000/derby/`.

## With Ruby in Docker container

One-time container creation:
One-time container creation, with current directory pointing at this repo's root:

```
docker run --name derby-docs-ruby -v "$(pwd)/docs:/derby-docs" -p 127.0.0.1:4000:4000 ruby:2.7 bash -c 'cd derby-docs && bundle install && bundle exec jekyll serve -H 0.0.0.0 -P 4000 --trace'
Expand Down
9 changes: 9 additions & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ theme: just-the-docs
color_scheme: derby-light # just-the-docs theme customization
permalink: /:path/:name

# just-the-docs customization
callouts:
warning-red:
title: Warning
color: red
warning-yellow:
title: Warning
color: yellow

# Front matter defaults
defaults:
-
Expand Down
4 changes: 2 additions & 2 deletions docs/apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ The server includes an app with a standard Node.js require statement. It can
then use the `app.router()` method to create a router middleware for Express
that handles all of the app's routes.

The server also needs to create a `store` object, which is what creates models,
The server also needs to create a `backend` object, which is what creates models,
coordinates data syncing, and interfaces with databases. Stores are created via
the `derby.createStore()` method. See [Backends](models/backends).
the `derby.createBackend()` method. See [Backends](models/backends).

> A typical setup can be seen in the [derby-starter](https://github.com/derbyjs/derby-starter/blob/master/lib/server.js) project, which is a node module for getting started with Derby.
>
Expand Down
79 changes: 54 additions & 25 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,84 @@ has_children: true

# Models

DerbyJS models are provided by [Racer](https://github.com/derbyjs/racer), a realtime model synchronization engine. By building on ShareDB, Racer enables multiple users and services to interact with the same data objects with realtime conflict resolution. Racer models have a simple getter/setter and event interface for writing application logic.
DerbyJS's models are provided by [Racer](https://github.com/derbyjs/racer), a realtime model synchronization engine built for Derby. By building on ShareDB, Racer enables multiple users and services to interact with the same data objects with realtime conflict resolution.

Racer models can also be used without Derby, such as in backend code to interact with ShareDB-based data, or even in frontend code with another UI framework.

A model's data can be thought of as a JSON object tree - see the [Paths documentation](models/paths) for details.

Racer models provide functionality useful for writing real-time application logic:
- Methods to [load data into the model](backends#loading-data-into-a-model), including via [database queries](queries)
- Null-safe [getter methods](getters) and [mutator (setter) methods](mutators)
- [Reactive functions](reactive-functions) for automatically producing output data whenever any input data changes
- Built-in reactive [filter and sort](filters-sorts) functions
- [Data change events](events) for more complex situations not covered by pure reactive functions

## Racer and ShareDB

Racer provides a single interface for working with local data stored in memory and remote data synced via ShareDB. It works equally well on the server and the browser, and it is designed to be used independently from DerbyJS. This is useful when writing migrations, data-only services, or integrating with different view libraries.

Remotely synced data is stored via [ShareDB](https://github.com/share/sharedb), which means that different clients can modify the same data at the same time. ShareDB uses [Operational Transformation (OT)](https://en.wikipedia.org/wiki/Operational_transformation) to automatically resolve conflicts in realtime or offline.

On the server, Racer provides a `store`, which configures a connection to a database and pub/sub adapter. Every store connected to the same database and pub/sub system is synchronized in realtime.
On the server, Racer provides a `backend` that extends the [ShareDB Backend](https://share.github.io/sharedb/api/backend). It configures a connection to a database and pub/sub adapter. Every backend connected to the same database and pub/sub system is synchronized in realtime.

Stores create `model` objects. Models have a synchronous interface similar to interacting directly with objects. They maintain their own copy of a subset of the global state. This subset is defined via [subscriptions](backends#loading-data-into-a-model) to certain queries or documents. Models perform operations independently, and they automatically synchronize their state.
Backends create `model` objects. Models have a synchronous interface similar to interacting directly with objects. They maintain their own copy of a subset of the global state. This subset is defined via [subscriptions](backends#loading-data-into-a-model) to certain queries or documents. Models perform operations independently, and they automatically synchronize their state.

Models emit events when their contents are updated, which DerbyJS uses to update the view in realtime.

## Creating models

Derby provides a model when calling application routes. On the server, it creates an empty model from the `store` associated with an app. When the server renders the page, the model is serialized. It is then reinitialized into the same state on the client. This model object is passed to app routes rendered on the client.
On the server, `backend.modelMiddleware()` provides an Express-compatible middleware function that, when run during request handling, creates a new empty model for the request and attaches it to `req.model`. Custom middleware can be added between the `modelMiddleware()` and the application routes to customize the model's data.

When the server runs an application route, it uses `req.model` to render the page. The model state is serialized into the server-side rendered page, and then in the browser, the model is reinitialized into the same state. This model object is passed to app routes rendered on the client.

```js
// Middleware to add req.model on each request
expressApp.use(backend.modelMiddleware());

Derby uses the model supplied by the store.modelMiddleware by calling `req.getModel()`. To pass data from server-side express middleware or routes, the model can be retrieved via this same method and data can be set on it before passing control to the app router.
// Subsequent middleware can use the model
expressApp.use((req, res, next) => {
req.model.set('_session.userId', 'test-user');
next();
});

If you would like to get or set data outside of the app on the server, you can create models directly via `store.createModel()`.
// Derby application routes use req.model for rendering
expressApp.use(derbyApp.router());
```

> `model = store.createModel(options)`
If you would like to get or set data on the server outside of the context of a request, you can create models directly via `backend.createModel()`.

> `model = backend.createModel(options)`
> * `options:`
> * `fetchOnly` Set to true to make model.subscribe calls perform a fetch instead
> * `model` Returns a model instance associated with the given store
> * `model` Returns a model instance associated with the given backend
## Store
## Closing models

Typically, a project will have only one store, even if it has multiple apps. It is possible to have multiple stores, but a model can be associated with only a single store, and a page can have only a single model.
Models created by `modelMiddleware()` are automatically closed when the Express request ends.

> `store = derby.createStore(options)`
> * `options` See the [Backends](backends) section for information on configuration
> * `store` Returns a Racer store instance
To close a manually-created model, you can use `model.close()`. The `close()` method will wait for all pending operations to finish before closing the model.

### Methods
> `backend.close([callback])`
> * `callback` - `() => void` - Optional callback, called once the model has finished closing.
> `middleware = store.modelMiddleware()`
> * `middleware` Returns a connect middleware function
> `closePromise = backend.closePromised()`
> * Returns a `Promise<void>` that resolves when the model has finished closing. The promise will never be rejected.
The model middleware adds a `req.getModel()` function which can be called to create or get a model (if one was already created) for a given request. It also closes this model automatically at the end of the request.
## Backend

Model's created from `req.getModel()` specify the option `{fetchOnly: true}`. This means that calls to `model.subscribe()` actually only fetch data and don't subscribe. This is more efficient during server-side rendering, since the model is only created for long enough to handle the route and render the page. The model then gets subscribed when it initializes in the browser.
Typically, a project will have only one backend, even if it has multiple apps. It is possible to have multiple backends, but a model can be associated with only a single backend, and a page can have only a single model.

```js
var expressApp = express();
expressApp.use(store.modelMiddleware());
> `backend = derby.createBackend(options)`
> `backend = racer.createBackend(options)`
> * `options` See the [Backends](backends) section for information on configuration
> * `backend` Returns a Racer backend instance
expressApp.get('/', function(req, res, next) {
var model = req.getModel();
});
```
### Methods

> `middleware = backend.modelMiddleware()`
> * `middleware` Returns an Express-compatible middleware function
The model middleware creates a new model for each request and adds a `req.model` reference to that model. It also closes this model automatically at the end of the request.

Models created by `modelMiddleware()` use the `{fetchOnly: true}` option. That means during server-side rendering, `model.subscribe()` doesn't actually register with the pub/sub system, which is more efficient since the model is only open for the short lifetime of the request. It's still tracked as a subscription so that when the model is re-initialized in the browser, the browser can register the actual subscriptions.
63 changes: 48 additions & 15 deletions docs/models/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,31 +92,64 @@ It is not possible to set or delete an entire collection, or get the list of col

## Loading data into a model

The `subscribe`, `fetch`, `unsubscribe`, and `unfetch` methods are used to load and unload data from ShareJS. These methods don't return data directly. Rather, they load the data into a model. Once loaded, the data are then accessed via model getter methods.
The `subscribe`, `fetch`, `unsubscribe`, and `unfetch` methods are used to load and unload data from the database. These methods don't return data directly. Rather, they load the data into a model. Once loaded, the data are then accessed via model getter methods.

`subscribe` and `fetch` both return data initially, but subscribe also registers with pub/sub on the server to receive ongoing updates as the data change.
`subscribe` and `fetch` both load the requested data into the model. Subscribe also registers with pub/sub, automatically applying remote updates to the locally loaded data.

> `model.subscribe(items..., callback(err))`
> `model.fetch(items..., callback(err))`
> `model.unsubscribe(items..., callback(err))`
> `model.unfetch(items..., callback(err))`
> * `items` Accepts one or more subscribe-able items, including a document path, scoped model, or query
> * `callback` Calls back once all of the data for each query and document has been loaded or when an error is encountered
> `model.subscribe(items, callback)`
> `model.fetch(items, callback)`
> `model.unsubscribe(items, callback)`
> `model.unfetch(items, callback)`
> * `items` - `string | ChildModel | Query | Array<string | ChildModel | Query>` - Specifier(s) for one or more loadable items, such as a document path, scoped model, or query
> * `callback` - `(error?: Error) => void` - Calls back once all of the data for each item has been loaded or when an error is encountered
There are also promise-based versions of the methods, available since [email protected].

> `model.subscribePromised(items)`
> `model.fetchPromised(items)`
> `model.unsubscribePromised(items)`
> `model.unfetchPromised(items)`
> * These each return a `Promise<void>` that is resolved when the requested item(s) are loaded or rejected on errors.
Avoid subscribing or fetching queries by document id like `model.query('users', {_id: xxx})`. You can achieve the same result passing `'users.xxx'` or `model.at('users.xxx')` to subscribe or fetch, and it is much more efficient.

If you only have one argument in your call to subscribe or fetch, you can also call `subscribe`, `fetch`, `unsubscribe`, and `unfetch` on the query or scoped model directly.

```js
var user = model.at('users.' + userId);
var todosQuery = model.query('todos', {creatorId: userId});
model.subscribe(user, todosQuery, function(err) {
// Subscribing to a single document with a path string.
const userPath = `users.${userId}`;
model.subscribe(userPath, (error) => {
if (err) return next(err);
console.log(user.get(), todosQuery.get());
page.render();
console.log(model.get(userPath));
});
// Subscribing to two things at once: a document via child model and a query.
const userModel = model.at(userPath);
const todosQuery = model.query('todos', {creatorId: userId});
model.subscribe([userModel, todosQuery], function(err) {
if (err) return next(err);
console.log(userModel.get(), todosQuery.get());
});

// Promise-based API
model.subscribePromised(userPath).then(
() => {
console.log(model.get(userPath));
},
(error) => { next(error); }
);
// Promise-based API with async/await
try {
await model.subscribePromised([userModel, todosQuery]);
console.log(userModel.get(), todosQuery.get());
} catch (error) {
// Handle subscribe error
}
```

Racer internally keeps track of the context in which you call subscribe or fetch, and it counts the number of times that each item is subscribed or fetched. To actually unload a document from the model, you must call the unsubscribe method the same number of times that subscribe is called and the unfetch method the same number of times that fetch is called. However, you generally don't need to worry about calling unsubscribe and unfetch manually.
Racer internally keeps track of the context in which you call subscribe or fetch, and it counts the number of times that each item is subscribed or fetched. To actually unload a document from the model, you must call the unsubscribe method the same number of times that subscribe is called and the unfetch method the same number of times that fetch is called.

However, you generally don't need to worry about calling unsubscribe and unfetch manually. Instead, the `model.unload()` method can be called to unsubscribe and unfetch from all of the subscribes and fetches performed since the last call to unload.

Derby unloads all contexts when doing a full page render, right before invoking route handlers. By default, the actual unsubscribe and unfetch happens after a short delay, so if something gets resubscribed during routing, the item will never end up getting unsubscribed and it will callback immediately.

Instead, the `model.unload()` method can be called to unsubscribe and unfetch from all of the subscribes and fetches performed since the last call to unload. Derby calls this method on every full page render right before entering a route. By default, the actual unsubscribe and unfetch happens after a short delay, so if something gets resubscribed during routing, the item will never end up getting unsubscribed and it will callback immediately.
See the [Contexts documentation](contexts) for more details.
Loading

0 comments on commit bdddccc

Please sign in to comment.