Skip to content

Commit 25436e6

Browse files
committed
[docs] Synchronization guides
1 parent a34fd5e commit 25436e6

File tree

11 files changed

+369
-19
lines changed

11 files changed

+369
-19
lines changed

site/guides/04_persistence/1_an_intro_to_persistence.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# An Intro To Persistence
22

3-
The persister module lets you save and load Store data to and from different
3+
The persister module framework lets you save and load Store data to and from different
44
locations, or underlying storage types.
55

66
Remember that TinyBase Stores are in-memory data structures, so you will

site/guides/05_synchronization/1_an_intro_to_synchronization.md

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Using A MergeableStore
2+
3+
The basic building block of TinyBase's synchronization system is the
4+
MergeableStore interface.
5+
6+
## The Anatomy Of A MergeableStore
7+
8+
The MergeableStore interface is a sub-type of the regular Store - and it shares
9+
its underlying implementation.
10+
11+
This means that if you want to add synchronization to your app, all of your
12+
existing calls to the Store methods will be unchanged - you just need to use the
13+
createMergeableStore function to instantiate it, instead of the classic
14+
createStore function.
15+
16+
```js
17+
import {createMergeableStore} from 'tinybase';
18+
19+
const store1 = createMergeableStore('store1'); // !resetHlc
20+
store1.setCell('pets', 'fido', 'species', 'dog');
21+
22+
console.log(store1.getContent());
23+
// -> [{pets: {fido: {species: 'dog'}}}, {}]
24+
```
25+
26+
The difference, though, is that a MergeableStore records additional metadata as
27+
the data is changed so that potential conflicts between it and another instance
28+
can be reconciled. This metadata is intended to be opaque, but you can see it if
29+
you call the getMergeableContent method:
30+
31+
```js
32+
console.log(store1.getMergeableContent());
33+
// ->
34+
[
35+
[
36+
{
37+
pets: [
38+
{
39+
fido: [
40+
{species: ['dog', 'Nn1JUF-----FnHIC', 290599168]},
41+
'',
42+
2682656941,
43+
],
44+
},
45+
'',
46+
2102515304,
47+
],
48+
},
49+
'',
50+
3506229770,
51+
],
52+
[{}, '', 0],
53+
];
54+
```
55+
56+
Without going into the detail of this, the main point to understand is that each
57+
update gets a timestamp, based on a hybrid logical clock (HLC), and a hash. As a
58+
result, TinyBase is able to understand which parts of the data have changed, and
59+
which changes are the most recent. The resulting 'last write wins' (LWW)
60+
approach allows the MergeableStore to act as a Conflict-Free Replicated Data
61+
Type (CRDT).
62+
63+
(Notice we provided an explicit `uniqueId` when we initialized the
64+
MergeableStore: this is not normally required, but here it just ensures the
65+
hashes in the example are deterministic).
66+
67+
We can of course, create a second MergeableStore with different data:
68+
69+
```js
70+
const store2 = createMergeableStore();
71+
store2.setCell('pets', 'felix', 'species', 'cat');
72+
```
73+
74+
And now merge them together with the convenient merge method:
75+
76+
```js
77+
store1.merge(store2);
78+
79+
console.log(store1.getContent());
80+
// -> [{pets: {felix: {species: 'cat'}, fido: {species: 'dog'}}}, {}]
81+
82+
console.log(store2.getContent());
83+
// -> [{pets: {felix: {species: 'cat'}, fido: {species: 'dog'}}}, {}]
84+
```
85+
86+
Magic!
87+
88+
This all said, it's very unlikely you will need to use the numerous extra
89+
methods available on a MergeableStore (compared to a Store) since most of them
90+
exist to support synchronization behind the scenes.
91+
92+
In general, you'll just use a MergeableStore in the same was as you would have
93+
used a Store, and instead rely on the more approachable Synchronizer API for
94+
synchronization. We'll discuss this next in the Using A Synchronizer guide.
95+
96+
# Persisting A MergeableStore
97+
98+
Once important thing that you need to be aware of is that a MergeableStore
99+
cannot currently be persisted by every type of Persister available to a regular
100+
Store. This is partly because some are already designed to work with alternative
101+
third-party CRDT systems (like the YjsPersister and AutomergePersister), and
102+
partly because this extra metadata cannot be easily stored in a plain SQLite
103+
database.
104+
105+
The following Persister types _can_ be used to persist a MergeableStore:
106+
107+
| Persister | Storage |
108+
| ---------------- | --------------------------- |
109+
| SessionPersister | Browser session storage |
110+
| LocalPersister | Browser local storage |
111+
| FilePersister | Local file (where possible) |
112+
113+
The following database-oriented Persister types can be used to persist a
114+
MergeableStore, but _only_ in the 'JSON-serialization' mode:
115+
116+
| Persister | Storage |
117+
| ------------------- | ------------------------------------------------------------------------------------------------------ |
118+
| Sqlite3Persister | SQLite in Node, via [sqlite3](https://github.com/TryGhost/node-sqlite3) |
119+
| SqliteWasmPersister | SQLite in a browser, via [sqlite-wasm](https://github.com/tomayac/sqlite-wasm) |
120+
| ExpoSqlitePersister | SQLite in React Native, via [expo-sqlite](https://github.com/expo/expo/tree/main/packages/expo-sqlite) |
121+
122+
The following database-oriented Persister types _cannot_ currently be used to
123+
persist a MergeableStore:
124+
125+
| Persister | Storage |
126+
| --------------------- | ---------------------------------------------------------------------------------------- |
127+
| IndexedDbPersister | Browser IndexedDB |
128+
| RemotePersister | Remote server |
129+
| CrSqliteWasmPersister | SQLite CRDTs, via [cr-sqlite-wasm](https://github.com/vlcn-io/cr-sqlite) |
130+
| ElectricSqlPersister | Electric SQL, via [electric-sql](https://github.com/electric-sql/electric) |
131+
| LibSqlPersister | LibSQL for Turso, via [libsql-client](https://github.com/tursodatabase/libsql-client-ts) |
132+
| PowerSyncPersister | PowerSync, via [powersync-sdk](https://github.com/powersync-ja/powersync-js) |
133+
| YjsPersister | Yjs CRDTs, via [yjs](https://github.com/yjs/yjs) |
134+
| AutomergePersister | Automerge CRDTs, via [automerge-repo](https://github.com/automerge/automerge-repo) |
135+
| PartyKitPersister | [PartyKit](https://www.partykit.io/), via the persister-partykit-server module |
136+
137+
Next, let's see how to synchronize MergeableStore objects together with the
138+
synchronizers module. Please continue on to the Using A Synchronizer guide.

site/guides/05_synchronization/2_using_a_mergeablestore.md

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Using A Synchronizer
2+
3+
The synchronizer module framework lets you synchronize MergeableStore data
4+
between different devices, systems, or subsystems.
5+
6+
It contains the Synchronizer
7+
interface, describing objects which can be used to synchronize a MergeableStore.
8+
9+
Under the covers, a Synchronizer is actually a very specialized type of
10+
Persister that _only_ supports MergeableStore objects, and which has a startSync
11+
method and a stopSync method.
12+
13+
## Types Of Synchronizer
14+
15+
In TinyBase v5.0, there are three types of Synchronizer:
16+
17+
- The WsSynchronizer uses WebSockets to communicate between different systems.
18+
- The BroadcastChannelSynchronizer uses the browser's BroadcastChannel API to
19+
communicate between different tabs and workers.
20+
- The LocalSynchronizer demonstrates synchronization in memory on a single local
21+
system.
22+
23+
Of course it is also possible to create custom Synchronizer objects if you have
24+
a transmission medium that allows the synchronization messages to be sent
25+
reliably between clients.
26+
27+
## Synchronizing with WebSockets
28+
29+
A common pattern for synchronizing over the web is to use WebSockets. This
30+
allows multiple clients to pass lightweight messages to each other, facilitating
31+
efficient synchronization.
32+
33+
One thing to understand is that this set up will typically require a server.
34+
This can be a relatively 'thin server' - it does not need to store data of its
35+
own - but is needed to keep a list of clients that are being synchronized
36+
together, and route and broadcast messages between the clients.
37+
38+
TinyBase includes a simple implementation of such a server. You simply need to
39+
create it, instantiated with a configured WebSocketServer object from the `ws`
40+
package:
41+
42+
```js
43+
// On a server machine:
44+
import {WebSocketServer} from 'ws';
45+
import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server';
46+
const server = createWsServer(new WebSocketServer({port: 8048}));
47+
```
48+
49+
This sets up a WsServer object, listening on port 8048.
50+
51+
Each client then needs to create a WsSynchronizer object, instantiated with the
52+
MergeableStore being synchronized, and a WebSocket configured to connect to the
53+
aforementioned server:
54+
55+
```js
56+
// On the first client machine:
57+
import {WebSocket} from 'ws';
58+
import {createMergeableStore} from 'tinybase';
59+
import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';
60+
61+
const clientStore1 = createMergeableStore();
62+
const clientSynchronizer1 = await createWsSynchronizer(
63+
clientStore1,
64+
new WebSocket('ws://localhost:8048'),
65+
);
66+
```
67+
68+
This WsSynchronizer can then be started, and data manipulated as normal:
69+
70+
```js
71+
await clientSynchronizer1.startSync();
72+
clientStore1.setCell('pets', 'fido', 'species', 'dog');
73+
// ...
74+
```
75+
76+
Meanwhile, on another client, an empty MergeableStore and another WsSynchronizer
77+
can be created and started, connecting to the same server.
78+
79+
```js
80+
// On the second client machine:
81+
const clientStore2 = createMergeableStore();
82+
const clientSynchronizer2 = await createWsSynchronizer(
83+
clientStore2,
84+
new WebSocket('ws://localhost:8048'),
85+
);
86+
await clientSynchronizer2.startSync();
87+
```
88+
89+
Once the synchronization is started, the server will broker the messages being
90+
passed back and forward between the two clients, and the data will be
91+
synchronized. The empty second MergeableStore will be populated with the data
92+
from the first:
93+
94+
```js
95+
// ...
96+
console.log(clientStore2.getTables());
97+
// -> {pets: {fido: {species: 'dog'}}}
98+
```
99+
100+
And of course the synchronization is bi-directional:
101+
102+
```js
103+
clientStore2.setCell('pets', 'felix', 'species', 'cat');
104+
console.log(clientStore2.getTables());
105+
// -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}}
106+
```
107+
108+
```js
109+
// ...
110+
console.log(clientStore1.getTables());
111+
// -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}}
112+
```
113+
114+
When done, it's important to destroy a WsSynchronizer to close and tidy up the
115+
client WebSockets:
116+
117+
```js
118+
clientSynchronizer1.destroy();
119+
```
120+
121+
```js
122+
clientSynchronizer2.destroy();
123+
```
124+
125+
And, if shut down, the WsServer should also be explicitly destroyed to close its
126+
listeners:
127+
128+
```js
129+
server.destroy();
130+
```
131+
132+
## Synchronizing over the browser BroadcastChannel
133+
134+
There may be situations where you need to synchronize data between different
135+
parts of a browser. For example, you might have a transient in-memory
136+
MergeableStore driving your UI, but then another instance in a Service Worker
137+
that can be persisted to (say) IndexedDB or another medium.
138+
139+
To facilitate keeping these in sync, the BroadcastChannelSynchronizer lets you
140+
synchronize over the browser's BroadcastChannel API, common to each browser
141+
sub-system. You simply need to provide a distinguishing channel name that can be
142+
used to identify what the two parts should be using to send and receive
143+
messages.
144+
145+
For example, in the UI part of your app:
146+
147+
```js
148+
import {createBroadcastChannelSynchronizer} from 'tinybase/synchronizers/synchronizer-broadcast-channel';
149+
150+
const frontStore = createMergeableStore();
151+
const frontSynchronizer = createBroadcastChannelSynchronizer(
152+
frontStore,
153+
'syncChannel',
154+
);
155+
await frontSynchronizer.startSync();
156+
```
157+
158+
And then in the service worker:
159+
160+
```js
161+
const backStore = createMergeableStore();
162+
const backSynchronizer = createBroadcastChannelSynchronizer(
163+
backStore,
164+
'syncChannel',
165+
);
166+
await backSynchronizer.startSync();
167+
```
168+
169+
Since they both share the `syncChannel` channel name, the data of the two is now
170+
synchronized:
171+
172+
```js
173+
frontStore.setCell('pets', 'fido', 'species', 'dog');
174+
```
175+
176+
```js
177+
// ...
178+
console.log(backStore.getTables());
179+
// -> {pets: {fido: {species: 'dog'}}}
180+
```
181+
182+
And so on!
183+
184+
When finished, these synchronizers should also be explicitly destroyed to ensure
185+
the channel listeners are cleaned up:
186+
187+
```js
188+
frontSynchronizer.destroy();
189+
```
190+
191+
```js
192+
backSynchronizer.destroy();
193+
```
194+
195+
## Wrapping Up
196+
197+
The Synchronizer interface provides an easy way to keep multiple TinyBase
198+
MergeableStores in sync. The WebSocket and BroadcastChannel options above allow
199+
for numerous interesting and powerful app architectures - and they are not
200+
sufficient, consider exploring the createCustomSynchronizer function to develop
201+
your own!
202+
203+
We now move on to ways in which TinyBase can be used like a database, starting
204+
with the Using Metrics guide.

site/guides/05_synchronization/3_using_a_synchronizer.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

site/guides/05_synchronization/4_combining_persistence_and_synchronization.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

site/guides/05_synchronization/index.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,22 @@
33
These guides discuss how to merge and synchronize data in MergeableStore
44
instances using synchronization techniques.
55

6-
See also the Todo App v6 (collaboration) demo.
6+
The basic building block of TinyBase's synchronization system is the
7+
MergeableStore interface. This is a sub-type of the regular Store - so all your
8+
existing calls to the Store methods will be unchanged - but it records
9+
additional metadata as the data is changed so that potential conflicts can be
10+
reconciled. See the Using A MergeableStore guide for more details.
11+
12+
On top of this, the synchronizer module framework uses this metadata to let you
13+
synchronize MergeableStore data between different devices, systems, or
14+
subsystems. Synchronization can take place over WebSockets, the browser's
15+
BroadcastChannel API, or other custom media. See the Using A Synchronizer guide
16+
for more details.
17+
18+
It's possible - and in fact recommended! - to use both persistence and
19+
synchronization at the same time. You will often want to persist changes to your
20+
TinyBase data between browser reloads even when offline, for example, and then
21+
synchronize to other devices or a server once the device comes back online.
22+
23+
See also the Todo App v6 (collaboration) demo for a simple example of
24+
adding synchronization between clients to an app.

0 commit comments

Comments
 (0)