Skip to content

v10.1.0-beta.1

Pre-release
Pre-release

Choose a tag to compare

@jamesgpearce jamesgpearce released this 22 Oct 04:37
· 0 commits to main since this release

Authorizing Synchronization

A WsServer can now decide which clients may use a path, and what each of them may write - which, until now, was left to whatever sat in front of it. Pass an options object to the createWsServer function with an authorize function, and optionally canWriteCell and canWriteValue functions:

import {createMergeableStore} from 'tinybase';
import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';
import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server';
import {WebSocket, WebSocketServer} from 'ws';

const getRole = (request) =>
  new URL(request.url, 'http://localhost').searchParams.get('role');

const shopServer = createWsServer(new WebSocketServer({port: 8053}), {
  authorize: (pathId, request) =>
    getRole(request) == 'staff' || getRole(request) == 'customer'
      ? {context: {role: getRole(request)}}
      : undefined,
  canWriteCell: (pathId, tableId, rowId, cellId, cell, {role}) =>
    role == 'staff' || tableId == 'orders',
});

const staffStore = createMergeableStore();
staffStore.setCell('pets', 'fido', 'price', 5);
const staffSynchronizer = await createWsSynchronizer(
  staffStore,
  new WebSocket('ws://localhost:8053/petShop?role=staff'),
);
await staffSynchronizer.startSync();

const customerStore = createMergeableStore();
const customerSynchronizer = await createWsSynchronizer(
  customerStore,
  new WebSocket('ws://localhost:8053/petShop?role=customer'),
);
await customerSynchronizer.startSync();

customerStore.setCell('orders', 'order1', 'pet', 'fido');
customerStore.setCell('pets', 'fido', 'price', 1);
// ...

console.log(customerStore.getTables());
// -> {pets: {fido: {price: 5}}, orders: {order1: {pet: 'fido'}}}

await customerSynchronizer.destroy();
await staffSynchronizer.destroy();
await shopServer.destroy();

The authorize function receives the request that opened the client's WebSocket, and returns a ClientAccess object - which can make the client read-only, and carry a context for the other two functions - or undefined to refuse it. A change that a client may not make is neither merged nor passed on, and if it was newer than the server's own data, the server brings that client back into line rather than letting it silently drift. The WsServerDurableObject class has the same features, as authorize, canWriteCell, and canWriteValue methods to override.

Once a server authorizes its clients, each path's MergeableStore becomes the only peer they synchronize with, since clients can no longer be trusted to answer each other directly. Clients themselves need no changes. There is more in the Authorizing Clients section of the Using A Synchronizer guide.

Cheaper Synchronization

Synchronization now sends much less data. Compared with v10.0, over the WebSocket server, in the scenarios that TinyBase now measures in its tests:

  • A client reconnecting to a 10,000-Row Table that has one changed Row exchanges about 8kB rather than about 620kB. Peers now compare a large Table's Rows in buckets before comparing them one by one, and so only the buckets that differ are examined.
  • The first client on a path is served straight away, rather than waiting up to a whole request timeout (a second, by default) for the server's MergeableStore to hear back from it.
  • A synchronizer that is asked to fetch the same changes from a peer it is already fetching from now shares that work, which, for example, halves the traffic when the first client joins a path.
  • In a room of 20 clients, a server that authorizes its clients handles a new client with 6 messages rather than 82, since the other clients no longer answer it too.

Newer peers only use the new bucket messages with each other, so older clients and servers keep working as before.

Reliability And Hardening

  • A MergeableStore now keeps the content that its Persister saved while the device's clock was ahead, once that clock has been put back. Until now, it refused the whole of that content when it next loaded it, and then saved over it. A Persister or Synchronizer also now reports any mergeable content or changes that its MergeableStore refuses, with the new error code 18, through its onIgnoredError handler. That is how to learn of a peer whose clock is more than five minutes ahead.
  • A MergeableStore now applies the changes that it is sent from elsewhere, leaving out only those stamped too far ahead of its clock, where until now one such stamp refused them all. How far ahead is too far can be chosen with the new third parameter of the createMergeableStore function, which defaults to 300 seconds and can be Infinity. The getHlcFunctions function takes the same parameter.
  • The largest message that a WebSocket synchronizes, and the most that it lets wait to be sent, can now be chosen, where it was fixed at 16 MiB. Set the maxBufferSize option of the createWsServer function, and the new last parameter of the createWsSynchronizer function, to synchronize a path whose content is too large to send to a new client in one message.
  • Peers now converge after any of them come back online. A peer that loads takes what it lacks from whichever peer answers first, which may itself be behind. So a peer that is sent another's hashes and finds that it has changes the other lacks now tells it, once, so that it takes them too. Older peers understand this, though they do not do it themselves.
  • A Persister or Synchronizer that is asked to load while it is saving now loads once the saving has finished, where until now it did not load at all. This is what the guides tell a client to do when its WebSocket reconnects.
  • A WsServer that authorizes its clients now only passes on the changes that are newer than its own data. Until now, it could pass on an older value that it had not taken itself, which a client it had corrected would take.
  • A WsSynchronizer for a single MergeableStore now goes on receiving changes after its WebSocket reconnects in place, as a ReconnectingWebSocket does. Since v10.0, it stopped listening to the WebSocket when it first closed, so a client went on sending its own changes but never heard of anyone else's.
  • A Persister or Synchronizer that is saving automatically now saves a change that is made while it is loading, once it has loaded. Until now, that change was not saved until the next one was made, and so a Synchronizer could leave it unsent.
  • A Persister or Synchronizer now runs a load or save that is asked of it just as another finishes. Until now, it could be left waiting, with its promise unresolved, until the next one was asked for.
  • A MergeableStore now hashes an object or array Cell or Value in the same way whether it arrives through a Synchronizer or through the applyMergeableChanges method or merge method. Until now, two MergeableStore objects with the same content could have different hashes, and so would go on comparing it whenever they synchronized.
  • A WsServer that authorizes its clients now changes nothing, on the server or for other clients, when it refuses a change: the client that made it is alone in being sent the server's version. It also refuses a client whose Id is unusable or already in use on its path, and bounds what one WebSocket can hold or have awaiting authorization.
  • A transaction that is rolled back now restores the object and array Cells and Values that it changed or deleted.
  • An average, as a Metric or in a query group, is no longer lost when working it out overflows but the average itself would not.
  • Indexes now sort a Row again when it loses its sort key, forget the sort keys of a previous definition, keep a Slice in place when it empties and refills in one transaction, and only tell listeners of tied Rows that have moved.
  • Relationships now give every listener current linked lists, only tell listeners of the linked lists that have changed, and forget their remote Tables when destroyed.
  • Queries now undo a join that was made through another, once that one finds no Row. They pass the intermediate Row Id to the function of such a join, as documented, keep the result Rows of other groups when one group stops and starts meeting a having condition, and remove a group's aggregate when it is no longer a valid Cell. Result Cells also now keep the order in which they were selected, however their Rows arrived.
  • Checkpoints now have consistent Ids whenever a listener is called, tell listeners of the checkpoints that the setSize method forgets, and clear a label that the setCheckpoint method is given as undefined.
  • Deleting an Index, Metric, or Relationship that does not exist no longer tells the listeners to their Ids.
  • The MsSqlPersister now reports the SQL that it runs inside a transaction to its onSqlCommand callback, as the other database Persisters do, along with the beginning and end of the transaction itself.