Skip to content

Releases: tinyplex/tinybase

v4.0.1

Choose a tag to compare

@jamesgpearce jamesgpearce released this 28 Jul 04:00

This optimizes TinyBase's JSON stringification and updates dev and integration dependencies.

v4.0.0

Choose a tag to compare

@jamesgpearce jamesgpearce released this 30 Jun 16:08

This is the fifth and final beta for v4.0, a major release that provides Persister modules that connect TinyBase to SQLite databases (in both browser and server contexts), and CRDT frameworks that can provide synchronization and local-first reconciliation:

Module Function Storage
persister-sqlite3 createSqlite3Persister SQLite in Node, via sqlite3
persister-sqlite-wasm createSqliteWasmPersister SQLite in a browser, via sqlite-wasm
persister-cr-sqlite-wasm createCrSqliteWasmPersister SQLite CRDTs, via cr-sqlite-wasm
persister-yjs createYjsPersister Yjs CRDTs, via yjs
persister-automerge createSqliteWasmPersister Automerge CRDTs, via automerge-repo

SQLite databases

You can persist Store data to a database with either a JSON serialization or tabular mapping. (See the DatabasePersisterConfig documentation for more details).

For example, this creates a Persister object and saves and loads the Store to and from a local SQLite database. It uses an explicit tabular one-to-one mapping for the 'pets' table:

const sqlite3 = await sqlite3InitModule();
const db = new sqlite3.oo1.DB(':memory:', 'c');
const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
const persister = createSqliteWasmPersister(store, sqlite3, db, {
  mode: 'tabular',
  tables: {load: {pets: 'pets'}, save: {pets: 'pets'}},
});

await persister.save();
console.log(db.exec('SELECT * FROM pets;', {rowMode: 'object'}));
// -> [{_id: 'fido', species: 'dog'}]

db.exec(`INSERT INTO pets (_id, species) VALUES ('felix', 'cat')`);
await persister.load();
console.log(store.getTables());
// -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}}

persister.destroy();

CRDT Frameworks

CRDTs allow complex reconciliation and synchronization between clients. Yjs and Automerge are two popular examples. The API should be familiar! The following will persist a TinyBase Store to a Yjs document:

store.setTables({pets: {fido: {species: 'dog'}}});

const doc = new Y.Doc();
const yJsPersister = createYjsPersister(store, doc);

await yJsPersister.save();
// Store will be saved to the document.
console.log(doc.toJSON());
// -> {tinybase: {t: {pets: {fido: {species: 'dog'}}}, v: {}}}
yJsPersister.destroy();

The following is the equivalent for an Automerge document that will sync over the broadcast channel:

const docHandler = new AutomergeRepo.Repo({
  network: [new BroadcastChannelNetworkAdapter()],
}).create();
const automergePersister = createAutomergePersister(store, docHandler);

await automergePersister.save();
// Store will be saved to the document.
console.log(docHandler.doc);
// -> {tinybase: {t: {pets: {fido: {species: 'dog'}}}, v: {}}}
automergePersister.destroy();

store.delTables();

New methods

There are three new methods on the Store object. The getContent method lets you get the Store's Tables and Values in one call. The corresponding setContent method lets you set them simultaneously.

The new setTransactionChanges method lets you replay TransactionChanges (received at the end of a transaction via listeners) into a Store, allowing you to take changes from one Store and apply them to another.

Persisters now provide a schedule method that lets you queue up asynchronous tasks, such as when persisting data that requires complex sequences of actions.

Breaking changes

The way that data is provided to the DoRollback and TransactionListener callbacks at the end of a transaction has changed. Where previously they directly received content about changed Cell and Value content, they now receive functions that they can choose to call to receive that same data. This has a performance improvement, and your callback or listener can choose between concise TransactionChanges or more verbose TransactionLog structures for that data.

If you have build a custom persister, you will need to update your implementation. Most notably, the setPersisted function parameter is provided with a getContent function to get the content from the Store itself, rather than being passed pre-serialized JSON. It also receives information about the changes made during a transaction. The getPersisted function must return the content (or nothing) rather than JSON. startListeningToPersisted has been renamed addPersisterListener, and stopListeningToPersisted has been renamed delPersisterListener.

v3.3.0

Choose a tag to compare

@jamesgpearce jamesgpearce released this 15 Jun 01:45

This release allows you to track the Cell Ids used across a whole Table, regardless of which Row they are in.

In a Table (particularly in a Store without a TablesSchema), different Rows can use different Cells. Consider this Store, where each pet has a different set of Cell Ids:

const store = createStore();

store.setTable('pets', {
  fido: {species: 'dog'},
  felix: {species: 'cat', friendly: true},
  cujo: {legs: 4},
});

Prior to v3.3, you could only get the Cell Ids used in each Row at a time (with the getCellIds method). But you can now use the getTableCellIds method to get the union of all the Cell Ids used across the Table:

console.log(store.getCellIds('pets', 'fido')); // previously available
// -> ['species']

console.log(store.getTableCellIds('pets')); //    new in v3.3
// -> ['species', 'friendly', 'legs']

You can register a listener to track the Cell Ids used across a Table with the new addTableCellIdsListener method. Use cases for this might include knowing which headers to render when displaying a sparse Table in a user interface, or synchronizing data with relational or column-oriented database system.

There is also a corresponding useTableCellIds hook in the optional ui-react module for accessing these Ids reactively, and a useTableCellIdsListener hook for more advanced purposes.

Note that the bookkeeping behind these new accessors and listeners is efficient and should not be slowed by the number of Rows in the Table.

This release also passes a getIdChanges function to every Id-related listener that, when called, returns information about the Id changes, both additions and removals, during a transaction. See the TableIdsListener type, for example.

let listenerId = store.addRowIdsListener(
  'pets',
  (store, tableId, getIdChanges) => {
    console.log(getIdChanges());
  },
);

store.setRow('pets', 'lowly', {species: 'worm'});
// -> {lowly: 1}

store.delRow('pets', 'felix');
// -> {felix: -1}

store.delListener(listenerId).delTables();

v3.2.0

Choose a tag to compare

@jamesgpearce jamesgpearce released this 05 Jun 17:26

This release lets you add a listener to the start of a transaction, and detect that a set of changes are about to be made to a Store.

To use this, call the addStartTransactionListener method on your Store. The listener you add can itself mutate the data in the Store.

From this release onwards, listeners added with the existing addWillFinishTransactionListener method are also able to mutate data. Transactions added with the existing addDidFinishTransactionListener method cannot mutate data.

const store = createStore();

const startListenerId = store.addStartTransactionListener(() => {
  console.log('Start transaction');
  console.log(store.getTables());
  // Can mutate data
});

const willFinishListenerId = store.addWillFinishTransactionListener(() => {
  console.log('Will finish transaction');
  console.log(store.getTables());
  // Can mutate data
});

const didFinishListenerId = store.addDidFinishTransactionListener(() => {
  console.log('Did finish transaction');
  console.log(store.getTables());
  // Cannot mutate data
});

store.setTable('pets', {fido: {species: 'dog'}});
// -> 'Start transaction'
// -> {}
// -> 'Will finish transaction'
// -> {pets: {fido: {species: 'dog'}}}
// -> 'Did finish transaction'
// -> {pets: {fido: {species: 'dog'}}}

store
  .delListener(startListenerId)
  .delListener(willFinishListenerId)
  .delListener(didFinishListenerId);

This release also fixes a bug where using the explicit startTransaction method inside another listener could create infinite recursion.

v3.1.6

Choose a tag to compare

@jamesgpearce jamesgpearce released this 01 Jun 14:32

This adds types entry points to TinyBase's package.json so that the library can be used with TypeScript in the Vite bundler, for example.

If there are any import issues relating to this change, please file an issue.

v3.1.5

Choose a tag to compare

@jamesgpearce jamesgpearce released this 27 May 14:03

Upgrades all dependencies and fixes Puppeteer headless log spew

v3.1.4

Choose a tag to compare

@jamesgpearce jamesgpearce released this 23 May 04:18

This minor release fixes an issue where a Schema containing default Values can fail when using a persister initialized with no initial Values. Pretty niche but worth fixing!

v3.1.3

Choose a tag to compare

@jamesgpearce jamesgpearce released this 17 May 21:01

This is a minor release that addresses some documentation typos and upgrades the developer dependencies.

v3.1.2

Choose a tag to compare

@jamesgpearce jamesgpearce released this 05 May 14:43

This release optimizes the hot transformMap function, bringing minor speed and size improvements. It also update developer dependencies and fixes some docs typos.

v3.1.1

Choose a tag to compare

@jamesgpearce jamesgpearce released this 30 Apr 17:00

This new release adds a powerful schema-based type system to TinyBase.

If you define the shape and structure of your data with a TablesSchema or ValuesSchema, you can benefit from an enhanced developer experience when operating on it. For example:

// Import the 'with-schemas' definition:
import {createStore} from 'tinybase/with-schemas';

// Set a schema for a new Store:
const store = createStore().setValuesSchema({
  employees: {type: 'number'},
  open: {type: 'boolean', default: false},
});

// Benefit from inline TypeScript errors.
store.setValues({employees: 3}); //                      OK
store.setValues({employees: true}); //                   TypeScript error
store.setValues({employees: 3, website: 'pets.com'}); // TypeScript error

The schema-based typing is used comprehensively throughout every module - from the core Store interface all the way through to the ui-react module. See the new Schema-Based Typing guide for instructions on how to use it.

This now means that there are three progressive ways to use TypeScript with TinyBase:

  • Basic Type Support (since v1.0)
  • Schema-based Typing (since v3.1)
  • ORM-like type definitions (since v2.2)

These are each described in the new TinyBase and TypeScript guide.

Also in v3.1, the ORM-like type definition generation in the tools module has been extended to emit ui-react module definitions.

Finally, v3.1.1 adds a reuseRowIds parameter to the addRow method and the useAddRowCallback hook. It defaults to true, for backwards compatibility, but if set to false, new Row Ids will not be reused unless the whole Table is deleted.