Repository navigation
v10.1.0-beta.1
Pre-releaseAuthorizing 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-
RowTablethat has one changedRowexchanges about 8kB rather than about 620kB. Peers now compare a largeTable'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
MergeableStoreto 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
MergeableStorenow keeps the content that itsPersistersaved 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. APersisterorSynchronizeralso now reports any mergeable content or changes that itsMergeableStorerefuses, with the new error code 18, through itsonIgnoredErrorhandler. That is how to learn of a peer whose clock is more than five minutes ahead. - A
MergeableStorenow 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 thecreateMergeableStorefunction, which defaults to 300 seconds and can beInfinity. ThegetHlcFunctionsfunction 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
maxBufferSizeoption of thecreateWsServerfunction, and the new last parameter of thecreateWsSynchronizerfunction, 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
PersisterorSynchronizerthat 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
WsServerthat 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
WsSynchronizerfor a singleMergeableStorenow 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
PersisterorSynchronizerthat 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 aSynchronizercould leave it unsent. - A
PersisterorSynchronizernow 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
MergeableStorenow hashes an object or arrayCellorValuein the same way whether it arrives through aSynchronizeror through theapplyMergeableChangesmethod ormergemethod. Until now, twoMergeableStoreobjects with the same content could have different hashes, and so would go on comparing it whenever they synchronized. - A
WsServerthat 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 whoseIdis 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
Valuesthat it changed or deleted. - An average, as a
Metricor in a query group, is no longer lost when working it out overflows but the average itself would not. Indexesnow sort aRowagain when it loses its sort key, forget the sort keys of a previous definition, keep aSlicein place when it empties and refills in one transaction, and only tell listeners of tied Rows that have moved.Relationshipsnow give every listener current linked lists, only tell listeners of the linked lists that have changed, and forget their remoteTableswhen destroyed.Queriesnow undo a join that was made through another, once that one finds noRow. They pass the intermediateRowIdto the function of such a join, as documented, keep the result Rows of other groups when one group stops and starts meeting ahavingcondition, and remove a group's aggregate when it is no longer a validCell. Result Cells also now keep the order in which they were selected, however their Rows arrived.Checkpointsnow have consistentIdswhenever a listener is called, tell listeners of the checkpoints that thesetSizemethod forgets, and clear a label that thesetCheckpointmethod is given asundefined.- Deleting an
Index,Metric, orRelationshipthat does not exist no longer tells the listeners to theirIds. - The
MsSqlPersisternow reports the SQL that it runs inside a transaction to itsonSqlCommandcallback, as the other database Persisters do, along with the beginning and end of the transaction itself.