Wโ
All docs
๐
Sign Up/Sign In
tinybase.org/guides/
Public Link
Apr 6, 2025, 9:23:10 AM - complete - 287.9 kB
Starting URLs:
https://tinybase.org/guides/
## Page: https://tinybase.org/guides/ This series of guides helps explain the concepts behind TinyBase and is designed to complement the more comprehensive API documentation. ## The Basics These guides cover the very basics of TinyBase. Read more. * Getting Started * Creating A Store * Writing To Stores * Reading From Stores * Listening To Stores * Transactions * Importing TinyBase * TinyBase And TypeScript * Architectural Options ## Building UIs These guides cover how to use the `ui-react` module and use React hooks and components to easily build reactive user interfaces with TinyBase. Read more. * Getting Started With ui-react * Using React Hooks * Using React Components * Using React DOM Components * Using Context ## Schemas These guides discuss how to set up a `ValuesSchema` or `TablesSchema` on a `Store` so that certain structures of data are assured. Read more. * Using Schemas * Schema-Based Typing * Mutating Data With Listeners ## Persistence These guides discuss how to load and save data to a `Store` from a persistence layer. Read more. * An Intro To Persistence * Database Persistence * Third-Party CRDT Persistence * Custom Persistence ## Synchronization These guides discuss how to merge and synchronize data in `MergeableStore` instances using synchronization techniques. Read more. * Using A MergeableStore * Using A Synchronizer ## Integrations There are plenty of other projects, products, and platforms that TinyBase can work with and alongside. Read more. * Cloudflare Durable Objects ## Using Metrics These guides discuss how to define `Metrics` that aggregate values together. Read more. * An Intro To Metrics * Building A UI With Metrics * Advanced Metric Definition ## Using Indexes These guides discuss how to define `Indexes` that allow fast access to matching `Row` objects. Read more. * An Intro To Indexes * Building A UI With Indexes * Advanced Index Definition ## Using Relationships These guides discuss how to define `Relationships` that connect Rows together between `Table` objects. Read more. * An Intro To Relationships * Building A UI With Relationships * Advanced Relationship Definitions ## Using Checkpoints These guides discuss how to use `Checkpoints` that allow you to build undo and redo functionality. Read more. * An Intro To Checkpoints * Building A UI With Checkpoints ## Using Queries These guides discuss how to define queries that let you select specific `Row` and `Cell` combinations from each `Table`, and benefit from powerful features like grouping and aggregation. Read more. * An Intro To Queries * TinyQL * Building A UI With Queries ## Inspecting Data If you are using TinyBase with React, you can use its web-based inspector, the `Inspector` component, that allows you to reason about the data during development. Read more. ## How TinyBase Is Built These guides discuss how TinyBase is structured and some of the interesting ways in which it is architected, tested, and built. Read more. * Developing TinyBase * Architecture * Testing * Documentation * How The Demos Work * Credits ## FAQ These are some of the frequently asked questions about TinyBase. Read more. ## Releases This is a reverse chronological list of the major TinyBase releases, with highlighted features. Read more. --- ## Page: https://tinybase.org/guides/the-basics/ These guides cover the very basics of TinyBase. We start with common ways to install the modules and then learn about how to interact with `Store` objects including creation, reading data, writing data, and listening for changes. See also the Hello World demos, and the Todo App demos. Let's get started! ## Getting Started This guide gets you up and running quickly with TinyBase. Read more. ## Creating A Store This guide shows you how to create a new `Store`. Read more. ## Writing To Stores This guide shows you how to write data to a `Store`. Read more. ## Reading From Stores This guide shows you how to read data from a `Store`. Read more. ## Listening To Stores This guide shows you how to listen to changes in the data in a `Store`. Read more. ## Transactions This guide shows you how to wrap multiple changes to the data in a `Store`. Read more. ## Importing TinyBase This guide provides an aside about importing TinyBase into your application. Read more. ## TinyBase And TypeScript This guide summarizes the two different levels of TypeScript coverage you can use with TinyBase. Read more. ## Architectural Options This guide discusses some of the ways in which you can use TinyBase, and how you can architect it into the bigger picture of how your app is built. Read more. --- ## Page: https://tinybase.org/guides/building-uis/ These guides cover how to use the `ui-react` module and use React hooks and components to easily build reactive user interfaces with TinyBase. See also the Countries demo, the Todo App demos, and the Drawing demo. ## Getting Started With ui-react To build React-based user interfaces with TinyBase, you will need to install the `ui-react` module in addition to the main module, and, of course, React itself. Read more. ## Using React Hooks There are reactive hooks in the `ui-react` module for accessing every part of a `Store`, as well as more advanced things like the `Metrics` and `Indexes` objects. Read more. ## Using React Components The reactive components in the `ui-react` module let you declaratively display parts of a `Store`. Read more. ## Using React DOM Components The reactive components in the `ui-react-dom` module let you declaratively display parts of a `Store` in a web browser, where the ReactDOM module is available. Read more. ## Using Context The `ui-react` module includes a context provider that lets you avoid passing global objects down through your component hierarchy. Read more. --- ## Page: https://tinybase.org/guides/schemas/ These guides discuss how to set up a `ValuesSchema` or `TablesSchema` on a `Store` so that certain structures of data are assured. See also the Countries demo, the Todo App demos, and the Drawing demo. ## Using Schemas Schemas are a simple declarative way to say what data you would like to store. Read more. ## Schema-Based Typing You can use type definitions that infer API types from the schemas you apply, providing a powerful way to improve your developer experience when you know the shape of the data being stored. Read more. ## Mutating Data With Listeners Although listeners are normally prevented from updating data, there are times when you may want to - such as when you are programmatically checking your data as it gets updated. Read more. --- ## Page: https://tinybase.org/guides/persistence/ These guides discuss how to load and save data to a `Store` from a persistence layer. See also the Countries demo, the Todo App demos, and the Drawing demo. ## An Intro To Persistence The persister module framework lets you save and load `Store` data to and from different locations, or underlying storage types. Read more. ## Database Persistence Since v4.0, there are various options for persisting `Store` data to and from SQLite databases, via a range of third-party modules. Read more. ## Third-Party CRDT Persistence Some persister modules let you save and load `Store` data to underlying storage types that can provide synchronization, local-first reconciliation, and CRDTs. Read more. ## Custom Persistence When you want to load and save `Store` data in unusual or custom ways, you can used the `createCustomPersister` function to do so in any way you wish. Read more. --- ## Page: https://tinybase.org/guides/synchronization/ These guides discuss how to merge and synchronize data in `MergeableStore` instances using synchronization techniques. The basic building block of TinyBase's synchronization system is the `MergeableStore` interface. This is a sub-type of the regular `Store` - so all your existing calls to the `Store` methods will be unchanged - but it records additional metadata as the data is changed so that potential conflicts can be reconciled. See the Using A MergeableStore guide for more details. On top of this, the synchronizer module framework uses this metadata to let you synchronize `MergeableStore` data between different devices, systems, or subsystems. Synchronization can take place over WebSockets, the browser's BroadcastChannel API, or other custom media. See the Using A Synchronizer guide for more details. It's possible - and in fact recommended! - to use both persistence and synchronization at the same time. You will often want to persist changes to your TinyBase data between browser reloads even when offline, for example, and then synchronize to other devices or a server once the device comes back online. See also the Todo App v6 (collaboration) demo for a simple example of adding synchronization between clients to an app. ## Using A MergeableStore The basic building block of TinyBase's synchronization system is the `MergeableStore` interface. Read more. ## Using A Synchronizer The synchronizer module framework lets you synchronize `MergeableStore` data between different devices, systems, or subsystems. Read more. --- ## Page: https://tinybase.org/guides/integrations/ There are plenty of other projects, products, and platforms that TinyBase can work with and alongside. These guides are far from exhaustive but provide a starting point for some of those integrations. There are also a number of starter projects that can be used to get these up and running. And of course the coverage will grow over time! ## Cloudflare Durable Objects Durable Objects are a new type of serverless compute platform from Cloudflare, and provide a way to run stateful applications in a serverless environment, without needing to manage infrastructure. Read more. --- ## Page: https://tinybase.org/guides/using-metrics/ These guides discuss how to define `Metrics` that aggregate values together. See also the Country demo and the Todo App demos. ## An Intro To Metrics This guide describes how the `metrics` module gives you the ability to create and track metrics based on the data in `Store` objects. Read more. ## Building A UI With Metrics This guide covers how the `ui-react` module supports the `Metrics` object. Read more. ## Advanced Metric Definition This guide describes how the `metrics` module let you create more complex types of metrics based on the data in `Store` objects. Read more. --- ## Page: https://tinybase.org/guides/using-indexes/ These guides discuss how to define `Indexes` that allow fast access to matching `Row` objects. See also the Rolling Dice demos, the Countries demo, and the Todo App demos. ## An Intro To Indexes This guide describes how the `indexes` module gives you the ability to create and track indexes based on the data in `Store` objects, and which allow you to look up and display filtered data quickly. Read more. ## Building A UI With Indexes This guide covers how the `ui-react` module supports the `Indexes` object. Read more. ## Advanced Index Definition This guide describes how the `indexes` module let you create more complex types of indexes based on the data in `Store` objects. Read more. --- ## Page: https://tinybase.org/guides/using-relationships/ These guides discuss how to define `Relationships` that connect Rows together between `Table` objects. See also the Drawing demo. ## An Intro To Relationships This guide describes how the `relationships` module gives you the ability to create and track relationships between `Row` objects based on the data in a `Store`. Read more. ## Building A UI With Relationships This guide covers how the `ui-react` module supports the `Relationships` object. Read more. ## Advanced Relationship Definitions This guide describes how the `relationships` module let you create more complex types of relationships based on the data in `Store` objects. Read more. --- ## Page: https://tinybase.org/guides/using-checkpoints/ These guides discuss how to use `Checkpoints` that allow you to build undo and redo functionality. See also the Todo App demos and the Drawing demo. ## An Intro To Checkpoints This guide describes how the `checkpoints` module gives you the ability to create and track changes to a `Store`'s data for the purposes of undo and redo functionality. Read more. ## Building A UI With Checkpoints This guide covers how the `ui-react` module supports the `Checkpoints` object. After all, if you have undo functionality in your app, you probably want an undo button! Read more. --- ## Page: https://tinybase.org/guides/using-queries/ These guides discuss how to define queries that let you select specific `Row` and `Cell` combinations from each `Table`, and benefit from powerful features like grouping and aggregation. See also the Car Analysis demos and the Movie Database demo. ## An Intro To Queries This guide describes how the `queries` module gives you the ability to create queries against `Tables` in the `Store` - such as selecting specific `Row` and `Cell` combinations from each `Table`, or performing powerful features like grouping and aggregation. Read more. ## TinyQL This guide describes how to build a query against `Store` data, using the API provided by the `setQueryDefinition` method in the `Queries` object. Read more. ## Building A UI With Queries This guide covers how the `ui-react` module supports the `Queries` object. Read more. --- ## Page: https://tinybase.org/guides/inspecting-data/ If you are using TinyBase with React, you can use its web-based inspector, the `Inspector` component, that allows you to reason about the data during development.  (NB: Previous to v5.0, this component was called `StoreInspector`.) ### Usage The component is available in the `ui-react-inspector` module. Simply add the component inside a `Provider` component (which is providing the `Store` context for the app that you want to be inspected). In total, the boilerplate will look something like this: const {Provider, useCreateStore} = TinyBaseUiReact; const {Inspector} = TinyBaseUiReactInspector; const App = () => ( <Provider store={useCreateStore(createStore)}> <Body /> </Provider> ); const Body = () => { return ( <> <h1>My app</h1> {/* ... */} <Inspector /> </> ); }; addEventListener('load', () => ReactDOM.createRoot(document.body).render(<App />), ); ### What Is In The Inspector? The inspector appears at first as a nub in the corner of the screen containing the TinyBase logo. Once clicked, it will open up to show a dark panel. You can reposition this to dock to any side of the window, or expand to the full screen. The inspector contains the following sections for whatever is available in the `Provider` component context: * Default `Store`: `Values` and a sortable view of each `Table` * Each named `Store`: `Values` and a sortable view of each `Table` * Default `Indexes`: each `Row` in each `Slice` of each `Index` * Each named `Indexes`: each `Row` in each `Slice` of each `Index` * Default `Relationships`: the pair of `Tables` in each `Relationship` * Each named `Relationships`: the pair of `Tables` in each `Relationship` * Default `Queries`: the pair of `Tables` in each Query * Each named `Queries`: the pair of `Tables` in each Query It is hoped that each section is quite self-explanatory. If not, please try it out in the <Inspector /> demo, or indeed in most of the TinyBase demos themselves! The Movie Database demo and Countries demo are quite good examples of the inspector in use. --- ## Page: https://tinybase.org/guides/how-tinybase-is-built/ These guides discuss how TinyBase is structured and some of the interesting ways in which it is architected, tested, and built. Don't forget to also take a look at the code on GitHub and knock yourself out! ## Developing TinyBase This guide is for people who would like to checkout the TinyBase code and build it from source. It's a quick overview of the common workflows. Read more. ## Architecture The architecture of TinyBase is pretty straightforward. This guide runs through the main file structure and principles. Read more. ## Testing Testing is a first-class citizen in TinyBase, as you may see from the high coverage it enjoys. Read more. ## Documentation Like testing, documentation is a first-class citizen of TinyBase, and most of it is structured as API documentation (from the `.d.ts` files) and from markdown pages. Read more. ## How The Demos Work The demos on the TinyBase site deserve a little explanation. Read more. ## Credits I'm James. Building TinyBase was an interesting exercise in API design, minification, and documentation. But now people seem to like using it! Read more. --- ## Page: https://tinybase.org/guides/faq/ These are some of the frequently asked questions about TinyBase. ### When Should I Use TinyBase? TinyBase is well suited for JavaScript applications where you need to manage structured data, such as those that have multiple 'records' to represent many of one type of object that might share fields. For example, in a Todo app, you can imagine using a `Row` in a `Table` for each todo. TinyBase makes it easy to set, get, respond to, and enumerate over this data and build user interfaces with it. Generally TinyBase will be appropriate for use in a client-side application such as a browser or rich web site where size and performance are a premium, and you want some of the boilerplate for managing tabular data structures taken care of. ### Why Should I Use TinyBase? You don't have to! There are many state management solutions for React and JavaScript applications. Many are more mature than TinyBase and have different (sometimes magical) approaches for dealing with reactivity. TinyBase models how _I_ think about building applications, and I could not find any existing solutions that provided the mix of structure, reactivity, small footprint, and functionality that I imagined. TinyBase was born! Maybe you share similar thoughts, and TinyBase will click with you. Maybe you don't and it won't. That's OK! ### When Should I Not Use TinyBase? While it may be appropriate to use TinyBase in a server environment, it does not replace a fully-fledged database system. But there are plenty of options for integrating with databases like SQLite and Postgres. ### Can I Contribute To TinyBase? Yes! You are very welcome to contribute to this project, though it is a spare-time endeavor so there is no guarantee around speed or certainty of contributions being accepted. Please follow the formatting mandated by the Prettier and ESLint config and just ensure that test coverage remains at 100%! ### Are There Good Examples Of TinyBase In Use? Please see the demos for ideas! ### What If I Have Other Questions? Please open a pull request or issue on GitHub and ask! Or ping the project on Bluesky, X, or Discord --- ## Page: https://tinybase.org/guides/releases/ This is a reverse chronological list of the major TinyBase releases, with highlighted features. * * * ## v6.0 This major release is about updating dependencies and infrastructure rather than adding new features. The most notable changes for users are: * The package distribution only includes modern ESM packages (both minified and non-minified). * React 19 is now compatible as an optional peer dependency. * The tools module and TinyBase CLI have been removed. If you have been using CJS or UMD packages, you will need to update your bundling strategy for TinyBase (in the same way that you will have had to have done for React 19, for example) but this change should be compatible with most packaging tools. If you had been using the library directly a browser, you should consider the esm.sh CDN, as we have for our demos. As a result of these changes, there have been some additional knock-on effects to the project and developer infrastructure as a whole. For example: * The test suite has been updated to use `react-testing-library` instead of `react-test-renderer`. * The React `jsx-runtime` is used for JSX transformations. * Demos (and CodePen examples) have been updated to use an `importmap` mapping the modules to the esm.sh CDN. * ESLint has finally been upgraded to v9. Note that TinyBase v6.0 adds no new functionality, so you can afford to stay on v5.4.x for a while if these changes are somehow incompatible for you. However, all future functionality changes and bug fixes _will_ take effect as v6.x releases (and probably won't be back-ported to v5.4.x), so you should endeavor to upgrade as soon as you can. Please let us know how these changes find you, and please file an issue on GitHub if you need help adapting to any of them. ## v5.4 ### Durable Objects synchronization This release contains a new WebSocket synchronization server that runs on Cloudflare as a Durable Object. It's in the new `synchronizer-ws-server-durable-object` module, and you use it by extending the `WsServerDurableObject` class. Use the `getWsServerDurableObjectFetch` function for conveniently binding your Cloudflare Worker to your Durable Object: import { WsServerDurableObject, getWsServerDurableObjectFetch, } from 'tinybase/synchronizers/synchronizer-ws-server-durable-object'; export class MyDurableObject extends WsServerDurableObject {} export default {fetch: getWsServerDurableObjectFetch('MyDurableObjects')}; For the above code to work, you'll need to have a Wrangler configuration that connects the `MyDurableObject` class to the `MyDurableObjects` namespace. In other words, you'll have something like this in your `wrangler.toml` file: [[durable_objects.bindings]] name = "MyDurableObjects" class_name = "MyDurableObject" With this you can now easily connect and synchronize clients that are using the `WsSynchronizer` synchronizer. ### Durable Objects Persistence But wait! There's more. Durable Objects also provide a storage mechanism, and sometimes you want TinyBase data to also be stored on the server (in case all the current clients disconnect and a new one joins, for example). So this release of TinyBase also includes a dedicated persister, the `DurableObjectStoragePersister`, that also synchronizes the data to the Durable Object storage layer. You create it with the `createDurableObjectStoragePersister` function, and hook it into the Durable Object by returning it from the `createPersister` method of your `WsServerDurableObject`: export class MyDurableObject extends WsServerDurableObject { createPersister() { return createDurableObjectStoragePersister( createMergeableStore(), this.ctx.storage, ); } } You can get started quickly with this architecture using the new Vite template that accompanies this release. ### Server Reference Implementation Unrelated to Durable Objects, this release also includes the new `synchronizer-ws-server-simple` module that contains a simple server implementation called `WsServerSimple`. Without the complications of listeners, persistence, or statistics, this is more suitable to be used as a reference implementation for other server environments. ### Architectural Guide To go with this release, we have added new documentation on ways in which you can use TinyBase in an app architecture. Check it out in the new Architectural Options guide. We've also started a new section of documentation for describing integrations, of which the Cloudflare Durable Objects guide, of course, is the first new entry! * * * ## v5.3 This release is focussed on a few API improvements and quality-of-life changes. These include: ### React SSR support Thanks to contributor Muhammad Muhajir for ensuring that TinyBase runs in server-side rendering environments! ### In the `persisters` module... All `Persister` objects now expose information about whether they are loading or saving. To access this `Status`, use: * The `getStatus` method, which will return 0 when it is idle, 1 when it is loading, and 2 when it is saving. * The `addStatusListener` method, which lets you add a `StatusListener` function and which is called whenever the status changes. These make it possible to track background load and save activities, so that, for example, you can show a status-bar spinner of asynchronous persistence activity. ### In the `synchronizers` module... Synchronizers are a sub-class of `Persister`, so all `Synchronizer` objects now also have: * The `getStatus` method, which will return 0 when it is idle, 1 when it is 'loading' (ie inbound syncing), and 2 when it is 'saving' (ie outbound syncing). * The `addStatusListener` method, which lets you add a `StatusListener` function and which is called whenever the status changes. ### In the `ui-react` module... There are corresponding hooks so that you can build these status changes into a React UI easily: * The `usePersisterStatus` hook, which will return the status for an explicitly provided, or context-derived `Persister`. * The `usePersisterStatusListener` hook, which lets you add your own `StatusListener` function to a `Persister`. * The `usePersister` hook, which lets you get direct access to a `Persister` from within your UI. And correspondingly for Synchronizers: * The `useSynchronizerStatus` hook, which will return the status for an explicitly provided, or context-derived `Synchronizer`. * The `useSynchronizerStatusListener` hook, which lets you add your own `StatusListener` function to a `Synchronizer`. * The `useSynchronizer` hook, which lets you get direct access to a `Synchronizer` from within your UI. In addition, this module also now includes hooks for injecting objects into the Provider context scope imperatively, much like the existing `useProvideStore` hook: * The `useProvideMetrics` hook, which lets you imperatively register `Metrics` objects. * The `useProvideIndexes` hook, which lets you register `Indexes` objects. * The `useProvideRelationships` hook, which lets you register `Relationships` objects. * The `useProvideQueries` hook, which lets you register `Queries` objects. * The `useProvideCheckpoints` hook, which lets you register `Checkpoints` objects. * The `useProvidePersister` hook, which lets you register `Persister` objects. * The `useProvideSynchronizer` hook, which lets you register `Synchronizer` objects. All of these new methods have extensive documentation, each with examples to show how to use them. Please provide feedback on this new release on GitHub! * * * ## v5.2 This release introduces new Persisters for... PostgreSQL! TinyBase now has two new `Persister` modules: * The `persister-postgres` module provides the `PostgresPersister`, which uses the excellent `postgres` module to bind to regular PostgreSQL databases, generally on a server. * The `persister-pglite` module provides the `PglitePersister`, which uses the new and exciting `pglite` module for running PostgreSQL... in a browser! Conceptually, things behave in the same way as they do for the various SQLite persisters. Simply use the `createPostgresPersister` function (or the similar `createPglitePersister` function) to persist your TinyBase data: import postgres from 'postgres'; import {createStore} from 'tinybase'; import {createPostgresPersister} from 'tinybase/persisters/persister-postgres'; // Create a TinyBase Store. const store = createStore().setTables({pets: {fido: {species: 'dog'}}}); // Create a postgres connection and Persister. const sql = postgres('postgres://localhost:5432/tinybase'); const pgPersister = await createPostgresPersister(store, sql, 'my_tinybase'); // Save Store to the database. await pgPersister.save(); console.log(await sql`SELECT * FROM my_tinybase;`); // -> [{_id: '_', store: '[{"pets":{"fido":{"species":"dog"}}},{}]'}] And, as per usual, you can update the database and have TinyBase automatically reflect those changes: // If separately the database gets updated... const json = '[{"pets":{"felix":{"species":"cat"}}},{}]'; await sql`UPDATE my_tinybase SET store = ${json} WHERE _id = '_';`; // ... then changes are loaded back. Reactive auto-load is also supported! await pgPersister.load(); console.log(store.getTables()); // -> {pets: {felix: {species: 'cat'}}} // As always, don't forget to tidy up. pgPersister.destroy(); await sql.end(); Note that these two `Persister` objects support both the `json` and `tabular` modes for saving TinyBase data into the database. See the `DatabasePersisterConfig` type for more details. (Note however that, like the SQLite Persisters, only the `json` mode is supported for `MergeableStore` instances, due to their additional CRDT metadata.) This release also exposes the new `createCustomSqlitePersister` function and `createCustomPostgreSqlPersister` function at the top level of the persister module. These can be used to build `Persister` objects against SQLite and PostgreSQL SDKs (or forks) that are not already included with TinyBase. ### Minor breaking change It's very unlikely to affect most apps, but also be aware that the `persisters` module and `synchronizers` module are no longer bundled in the 'master' tinybase module. If you are using them (most likely because you have built a custom `Persister` or `Synchronizer`), you will need to update your imports accordingly to the standalone `tinybase/persisters` and `tinybase/synchronizers` versions of them. Apologies. * * * ## v5.1 This release lets you persist data on a server using the `createWsServer` function. This makes it possible for all clients to disconnect from a path, but, when they reconnect, for the data to still be present for them to sync with. This is done by passing in a second argument to the `createWsServer` function that creates a `Persister` instance (for which also need to create or provide a `MergeableStore`) for a given path: import {createMergeableStore} from 'tinybase'; import {createFilePersister} from 'tinybase/persisters/persister-file'; import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server'; import {WebSocketServer} from 'ws'; const persistingServer = createWsServer( new WebSocketServer({port: 8051}), (pathId) => createFilePersister( createMergeableStore(), pathId.replace(/[^a-zA-Z0-9]/g, '-') + '.json', ), ); persistingServer.destroy(); This is a very crude (and not production-safe!) example, but demonstrates a server that will create a file, based on any path that clients connect to, and persist data to it. See the `createWsServer` function documentation for more details. This implementation is still experimental so please kick the tires! There is one small breaking change in this release: the functions for creating `Synchronizer` objects can now take optional onSend and onReceive callbacks that will fire whenever messages pass through the `Synchronizer`. See, for example, the `createWsSynchronizer` function. These are suitable for debugging synchronization issues in a development environment. * * * ## v5.0 We're excited to announce this major release for TinyBase! It includes important data synchronization functionality and a range of other improvements. ## In Summary * The new MergeableStore type wraps your data as a Conflict-Free Replicated Data Type (CRDT). * The new Synchronizer framework keeps multiple instances of data in sync across different media. * An improved module folder structure removes common packaging and bundling issues. * The TinyBase Inspector is now in its own standalone `ui-react-inspector` module. * TinyBase now supports only Expo SQLite v14 (SDK 51) and above. There are also some small breaking changes that may affect you (but which should easy to fix if they do). Let's look at the major functionality in more detail! ### The New `MergeableStore` Type A key part of TinyBase v5.0 is the new `mergeable-store` module, which contains a subtype of `Store` - called `MergeableStore` - that can be merged with another with deterministic results. The implementation uses an encoded hybrid logical clock (HLC) to timestamp the changes made so that they can be cleanly merged. The `getMergeableContent` method on a `MergeableStore` is used to get the state of a store that can be merged into another. The `applyMergeableChanges` method will let you apply that to (another) store. The `merge` method is a convenience function to bidirectionally merge two stores together: const localStore1 = createMergeableStore(); const localStore2 = createMergeableStore(); localStore1.setCell('pets', 'fido', 'species', 'dog'); localStore2.setCell('pets', 'felix', 'species', 'cat'); localStore1.merge(localStore2); console.log(localStore1.getContent()); // -> [{pets: {felix: {species: 'cat'}, fido: {species: 'dog'}}}, {}] console.log(localStore2.getContent()); // -> [{pets: {felix: {species: 'cat'}, fido: {species: 'dog'}}}, {}] Please read the new Using A MergeableStore guide for more details of how to use this important new API. A `MergeableStore` can be persisted locally, just like a regular `Store` into file, local and session storage, and simple SQLite environments such as Expo and SQLite3. These allow you to save the state of a `MergeableStore` locally before it has had the chance to be synchronized online, for example. Which leads us onto the next important feature in v5.0, allowing you to synchronize stores between systems... ### The New `Synchronizer` Framework The v5.0 release also introduces the new concept of synchronization. `Synchronizer` objects implement a negotiation protocol that allows multiple `MergeableStore` objects to be merged together. This can be across a network, using WebSockets, for example: import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client'; import {WebSocket} from 'ws'; // On a server machine: const server = createWsServer(new WebSocketServer({port: 8043})); // On the first client machine: const store1 = createMergeableStore(); const synchronizer1 = await createWsSynchronizer( store1, new WebSocket('ws://localhost:8043'), ); await synchronizer1.startSync(); store1.setCell('pets', 'fido', 'legs', 4); // On the second client machine: const store2 = createMergeableStore(); const synchronizer2 = await createWsSynchronizer( store2, new WebSocket('ws://localhost:8043'), ); await synchronizer2.startSync(); store2.setCell('pets', 'felix', 'price', 5); // ... console.log(store1.getTables()); // -> {pets: {felix: {price: 5}, fido: {legs: 4}}} console.log(store2.getTables()); // -> {pets: {felix: {price: 5}, fido: {legs: 4}}} synchronizer1.destroy(); synchronizer2.destroy(); server.destroy(); This release includes three types of `Synchronizer`: * The `WsSynchronizer` uses WebSockets to communicate between different systems as shown above. * The `BroadcastChannelSynchronizer` uses the browser's BroadcastChannel API to communicate between different tabs and workers. * The `LocalSynchronizer` demonstrates synchronization in memory on a single local system. Notice that the `WsSynchronizer` assumes that there exists a server that can forward requests to other `WsSynchronizer` systems. This can be created using the `createWsServer` function that takes a WebSocketServer as also shown above. Please read the new Using A Synchronizer guide for more details of how to synchronize your data. ### Improved Module Folder Structure We have previously found issues with legacy bundlers and other tools that didn't fully support the new `exports` field in the module's package. To mitigate that, the TinyBase distribution now has a top-level folder structure that fully echoes the import paths, including signifiers for JavaScript versions, schema support, minification and so on. Please read the comprehensive Importing TinyBase guide for more details of how to construct the correct import paths in v5.0. ### Breaking `Changes` in v5.0 #### Module File Structure If you previously had `/lib/` in your import paths, you should remove it. You also do not have to explicitly specify whether you need the `cjs` version of TinyBase - if you are using a `require` rather than an `import`, you will get it automatically. The non-minified version of the code is now default and you need to be explicit when you _want_ minified code. Previously you would add `/debug` to the import path to get non-minified code, but now you add `/min` to the import path to get _minified_ code. #### Expo SQLite `Persister` Previously the `persister-expo-sqlite` module supported expo-sqlite v13 and the persister-expo-sqlite-next module supported their modern 'next' package. In v5.0, the `persister-expo-sqlite` module only supports v14 and later, and the persister-expo-sqlite-next module has been removed. #### The TinyBase Inspector Previously, the React-based inspector (then known as `StoreInspector`) resided in the debug version of the `ui-react-dom` module. It now lives in its own `ui-react-inspector` module (so that it can be used against non-debug code) and has been renamed to Inspector. Please update your imports and rename the component when used, accordingly. See the API documentation for details, or the <Inspector /> demo, for example. #### API `Changes` The following changes have been made to the existing TinyBase API for consistency. These are less common parts of the API but should straightforward to correct if you are using them. In the type definitions: * The GetTransactionChanges and GetTransactionLog types have been removed. * The TransactionChanges type has been renamed as the `Changes` type. * The `Changes` type now uses `undefined` instead of `null` to indicate a `Cell` or `Value` that has been deleted or that was not present. * The `TransactionLog` type is now an array instead of a JavaScript object. In the `Store` interface: * There is a new `getTransactionChanges` method and a new getTransactionLog method. * The setTransactionChanges method is renamed as the `applyChanges` method. * A `DoRollback` function no longer gets passed arguments. You can use the `getTransactionChanges` method and `getTransactionLog` method directly instead. * Similarly, a `TransactionListener` function no longer gets passed arguments. In the `persisters` module: * The `createCustomPersister` function now takes a final optional boolean (`supportsMergeableStore`) to indicate that the `Persister` can support `MergeableStore` as well as `Store` objects. * A `Persister`'s `load` method and `startAutoLoad` method now take a `Content` object as one parameter, rather than `Tables` and `Values` as two. * If you create a custom `Persister`, the setPersisted method now receives changes made to a `Store` directly by reference, rather than via a callback. Similarly, the `PersisterListener` you register in your addPersisterListener implementation now takes `Content` and `Changes` objects directly rather than via a callback. * The broadcastTransactionChanges method in the `persister-partykit-server` module has been renamed to the broadcastChanges method. * * * ## v4.8 This release includes the new `persister-powersync` module, which provides a `Persister` for PowerSync's SQLite database. Much like the other SQLite persisters, use it by passing in a PowerSync instance to the `createPowerSyncPersister` function; something like: const powerSync = usePowerSync(); const persister = createPowerSyncPersister(store, powerSync, { mode: 'tabular', tables: { load: {items: {tableId: 'items', rowIdColumnName: 'value'}}, save: {items: {tableName: 'items', rowIdColumnName: 'value'}}, }, }); A huge thank you to Benedikt Mueller (@bndkt) for building out this functionality! And please provide feedback on how this new `Persister` works for you. * * * ## v4.7 This release includes the new `persister-libsql` module, which provides a `Persister` for Turso's LibSQL database. Use the `Persister` by passing in a reference to the LibSQL client to the createLibSQLPersister function; something like: const client = createClient({url: 'file:my.db'}); const persister = createLibSqlPersister(store, client, { mode: 'tabular', tables: { load: {items: {tableId: 'items', rowIdColumnName: 'value'}}, save: {items: {tableName: 'items', rowIdColumnName: 'value'}}, }, }); This is the first version of this functionality, so please provide feedback on how it works for you! * * * ## v4.6 This release includes the new `persister-electric-sql` module, which provides a `Persister` for ElectricSQL client databases. Use the `Persister` by passing in a reference to the Electric client to the `createElectricSqlPersister` function; something like: const electric = await electrify(connection, schema, config); const persister = createElectricSqlPersister(store, electric, { mode: 'tabular', tables: { load: {items: {tableId: 'items', rowIdColumnName: 'value'}}, save: {items: {tableName: 'items', rowIdColumnName: 'value'}}, }, }); This release is accompanied by a template project to get started quickly with this integration. Enjoy! * * * ## v4.5 This release includes the new persister-expo-sqlite-next module, which provides a `Persister` for the modern version of Expo's SQLite library, designated 'next' as of November 2023. This API should be used if you are installing the `expo-sqlite/next` module. Note that TinyBase support for the legacy version of Expo-SQLite (`expo-sqlite`) is still available in the `persister-expo-sqlite` module. NB as of TinyBase v5.0, this is now the default and legacy support has been removed. Thank you to Expo for providing this functionality! * * * ## v4.4 This relatively straightforward release adds a selection of new listeners to the `Store` object, and their respective hooks. These are for listening to changes in the 'existence' of entities rather than to their value. For example, the `addHasTableListener` method will let you listen for the presence (or not) of a specific table. The full set of new existence-listening methods and hooks to work with this is as follows: | Existence of: | Add Listener | Hook | Add Listener Hook | | --- | --- | --- | --- | | `Tables` | `addHasTablesListener` | `useHasTables` | `useHasTablesListener` | | A `Table` | `addHasTableListener` | `useHasTable` | `useHasTableListener` | | A `Table` `Cell` | `addHasTableCellListener` | `useHasTableCell` | `useHasTableCellListener` | | A `Row` | `addHasRowListener` | `useHasRow` | `useHasRowListener` | | A `Cell` | `addHasCellListener` | `useHasCell` | `useHasCellListener` | | `Values` | `addHasValuesListener` | `useHasValues` | `useHasValuesListener` | | A `Value` | `addHasValueListener` | `useHasValue` | `useHasValueListener` | These methods may become particularly important in future versions of TinyBase that support `null` as valid Cells and `Values`. * * * ## v4.3 We're excited to announce TinyBase 4.3, which provides an integration with PartyKit, a cloud-based collaboration provider. This allows you to enjoy the benefits of both a "local-first" architecture and a "sharing-first" platform. You can have structured data on the client with fast, reactive user experiences, but also benefit from cloud-based persistence and room-based collaboration.  This release includes two new modules: * The `persister-partykit-server` module provides a server class for coordinating clients and persisting `Store` data to the PartyKit cloud. * The `persister-partykit-client` module provides the API to create connections to the server and a binding to your `Store`. A TinyBase server implementation on PartyKit can be as simple as this: import {TinyBasePartyKitServer} from 'tinybase/persisters/persister-partykit-server'; export default class extends TinyBasePartyKitServer {} On the client, use the familiar `Persister` API, passing in a reference to a PartyKit socket object that's been configured to connect to your server deployment and named room: import {createPartyKitPersister} from 'tinybase/persisters/persister-partykit-client'; const persister = createPartyKitPersister( store, new PartySocket({ host: 'project-name.my-account.partykit.dev', room: 'my-partykit-room', }), ); await persister.startAutoSave(); await persister.startAutoLoad(); The `load` method and (gracefully failing) `save` method on this `Persister` use HTTPS to get or set full copies of the `Store` to the cloud. However, the auto-save and auto-load modes use a websocket to transmit subsequent incremental changes in either direction, making for performant sharing of state between clients. See and try out this new collaboration functionality in the Todo App v6 (collaboration) demo. This also emphasizes the few changes that need to be made to an existing app to make it instantly collaborative. Also try out the tinybase-ts-react-partykit template that gets you up and running with a PartyKit-enabled TinyBase app extremely quickly. PartyKit supports retries for clients that go offline, and so the disconnected user experience is solid, out of the box. Learn more about configuring this behavior here. Note, however, that this release is not yet a full CRDT implementation: there is no clock synchronization and it is more 'every write wins' than 'last write wins'. However, since the transmitted updates are at single cell (or value) granularity, conflicts are minimized. More resilient replication is planned as this integration matures. * * * ## v4.2 This release adds support for persisting TinyBase to a browser's IndexedDB storage. You'll need to import the new `persister-indexed-db` module, and call the `createIndexedDbPersister` function to create the IndexedDB `Persister`. The API is the same as for all the other `Persister` APIs: import {createIndexedDbPersister} from 'tinybase/persisters/persister-indexed-db'; store .setTable('pets', {fido: {species: 'dog'}}) .setTable('species', {dog: {price: 5}}) .setValues({open: true}); const indexedDbPersister = createIndexedDbPersister(store, 'petStore'); await indexedDbPersister.save(); // IndexedDB -> // database petStore: // objectStore t: // object 0: // k: "pets" // v: {fido: {species: dog}} // object 1: // k: "species" // v: {dog: {price: 5}} // objectStore v: // object 0: // k: "open" // v: true indexedDbPersister.destroy(); Note that it is not possible to reactively detect changes to a browser's IndexedDB storage. A polling technique is used to load underlying changes if you choose to 'autoLoad' your data into TinyBase. This release also upgrades Prettier to v3.0 which has a peer-dependency impact on the tools module. Please report any issues! * * * ## v4.1 This release introduces the new `ui-react-dom` module. This provides pre-built components for tabular display of your data in a web application.  ### New DOM Components The following is the list of all the components released in v4.1: | Component | Purpose | | | --- | --- | --- | | `ValuesInHtmlTable` | Renders `Values`. | demo | | `TableInHtmlTable` | Renders a `Table`. | demo | | `SortedTableInHtmlTable` | Renders a sorted `Table`, with optional interactivity. | demo | | `SliceInHtmlTable` | Renders a `Slice` from an `Index`. | demo | | `RelationshipInHtmlTable` | Renders the local and remote `Tables` of a relationship | demo | | `ResultTableInHtmlTable` | Renders a `ResultTable`. | demo | | `ResultSortedTableInHtmlTable` | Renders a sorted `ResultTable`, with optional interactivity. | demo | | `EditableCellView` | Renders a `Cell` and lets you change its type and value. | demo | | `EditableValueView` | Renders a `Value` and lets you change its type and value. | demo | These pre-built components are showcased in the UI Components demos. Using them should be very familiar if you have used the more abstract `ui-react` module: import React from 'react'; import {createRoot} from 'react-dom/client'; import {SortedTableInHtmlTable} from 'tinybase/ui-react-dom'; const App = ({store}) => ( <SortedTableInHtmlTable tableId="pets" cellId="species" store={store} /> ); store.setTables({ pets: { fido: {species: 'dog'}, felix: {species: 'cat'}, }, }); const app = document.createElement('div'); const root = createRoot(app); root.render(<App store={store} />); console.log(app.innerHTML); // -> ` <table> <thead> <tr><th>Id</th><th class="sorted ascending">โ species</th></tr> </thead> <tbody> <tr><th>felix</th><td>cat</td></tr> <tr><th>fido</th><td>dog</td></tr> </tbody> </table> `; root.unmount(); The `EditableCellView` component and `EditableValueView` component are interactive input controls for updating `Cell` and `Value` content respectively. You can generally use them across your table views by adding the `editable` prop to your table component. ### The new Inspector  The new `Inspector` component allows you to view, understand, and edit the content of a `Store` in a debug web environment. Try it out in most of the demos on the site, including the Movie Database demo, pictured. This requires a debug build of the new `ui-react-dom` module, which is now also included in the UMD distribution. Also in this release, the `getResultTableCellIds` method and `addResultTableCellIdsListener` method have been added to the `Queries` object. The equivalent `useResultTableCellIds` hook and `useResultTableCellIdsListener` hook have also been added to `ui-react` module. A number of other minor React hooks have been added to support the components above. Demos have been updated to demonstrate the `ui-react-dom` module and the `Inspector` component where appropriate. (NB: Previous to v5.0, this component was called `StoreInspector`.) * * * ## v4.0 This major release 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 | See the Database Persistence guide for details on how to work with SQLite databases, and the Synchronizing Data guide for more complex synchronization with the CRDT frameworks. Take a look at the vite-tinybase-ts-react-crsqlite template, for example, which demonstrates Vulcan's cr-sqlite to provide persistence and synchronization via this technique. ### 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: import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm'; const sqlite3 = await sqlite3InitModule(); const db = new sqlite3.oo1.DB(':memory:', 'c'); store.setTables({pets: {fido: {species: 'dog'}}}); const sqlitePersister = createSqliteWasmPersister(store, sqlite3, db, { mode: 'tabular', tables: {load: {pets: 'pets'}, save: {pets: 'pets'}}, }); await sqlitePersister.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 sqlitePersister.load(); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}} sqlitePersister.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: import {createYjsPersister} from 'tinybase/persisters/persister-yjs'; import {Doc} from 'yjs'; store.setTables({pets: {fido: {species: 'dog'}}}); const doc = new 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: import {Repo} from '@automerge/automerge-repo'; import {BroadcastChannelNetworkAdapter} from '@automerge/automerge-repo-network-broadcastchannel'; import {createAutomergePersister} from 'tinybase/persisters/persister-automerge'; const docHandler = new Repo({ network: [new BroadcastChannelNetworkAdapter()], }).create(); const automergePersister = createAutomergePersister(store, docHandler); await automergePersister.save(); // Store will be saved to the document. console.log(await 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. Although 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 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`: 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 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 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); store.delTables(); This release also fixes a bug where using the explicit `startTransaction` method _inside_ another listener could create infinite recursion. * * * ## v3.1 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. * * * ## v3.0 This major new release adds key/value store functionality to TinyBase. Alongside existing tabular data, it allows you to get, set, and listen to, individual `Value` items, each with a unique `Id`. store.setValues({employees: 3, open: true}); console.log(store.getValues()); // -> {employees: 3, open: true} listenerId = store.addValueListener( null, (store, valueId, newValue, oldValue) => { console.log(`Value '${valueId}' changed from ${oldValue} to ${newValue}`); }, ); store.setValue('employees', 4); // -> "Value 'employees' changed from 3 to 4" store.delListener(listenerId).delValues(); Guides and documentation have been fully updated, and certain demos - such as the Todo App v2 (indexes) demo, and the Countries demo - have been updated to use this new functionality. If you use the optional `ui-react` module with TinyBase, v3.0 now uses and expects React v18. In terms of core API changes in v3.0, there are some minor breaking changes (see below), but the majority of the alterations are additions. The `Store` object gains the following: * The `setValues` method, `setPartialValues` method, and `setValue` method, to set keyed value data into the `Store`. * The `getValues` method, `getValueIds` method, and `getValue` method, to get keyed value data out of the `Store`. * The `delValues` method and `delValue` method for removing keyed value data. * The `addValuesListener` method, `addValueIdsListener` method, addValueListener method, and `addInvalidValueListener` method, for listening to changes to keyed value data. * The `hasValues` method, `hasValue` method, and `forEachValue` method, for existence and enumeration purposes. * The `getTablesJson` method, `getValuesJson` method, `setTablesJson` method, and `setValuesJson` method, for reading and writing tabular and keyed value data to and from a JSON string. Also see below. * The `getTablesSchemaJson` method, `getValuesSchemaJson` method, setTablesSchema method, `setValuesSchema` method, `delTablesSchema` method, and delValuesSchema method, for reading and writing tabular and keyed value schemas for the `Store`. Also see below. The following types have been added to the `store` module: * `Values`, `Value`, and `ValueOrUndefined`, representing keyed value data in a `Store`. * `ValueListener` and `InvalidValueListener`, to describe functions used to listen to (valid or invalid) changes to a `Value`. * `ValuesSchema` and `ValueSchema`, to describe the keyed `Values` that can be set in a `Store` and their types. * `ValueCallback`, `MapValue`, `ChangedValues`, and `InvalidValues`, which also correspond to their '`Cell`' equivalents. Additionally: * The persisters' `load` method and `startAutoLoad` method take an optional `initialValues` parameter for setting `Values` when a persisted `Store` is bootstrapped. * The `Checkpoints` module will undo and redo changes to keyed values in the same way they do for tabular data. * The tools module provides a getStoreValuesSchema method for inferring value-based schemas. The getStoreApi method and getPrettyStoreApi method now also provides an ORM-like code-generated API for schematized key values. All attempts have been made to provide backwards compatibility and/or easy upgrade paths. In previous versions, `getJson` method would get a JSON serialization of the `Store`'s tabular data. That functionality is now provided by the `getTablesJson` method, and the `getJson` method instead now returns a two-part array containing the tabular data and the keyed value data. Similarly, the `getSchemaJson` method used to return the tabular schema, now provided by the `getTablesSchemaJson` method. The `getSchemaJson` method instead now returns a two-part array of tabular schema and the keyed value schema. The `setJson` method used to take a serialization of just the tabular data object. That's now provided by the `setTablesJson` method, and the `setJson` method instead expects a two-part array containing the tabular data and the keyed value data (as emitted by the `getJson` method). However, for backwards compatibility, if the `setJson` method is passed an object, it _will_ set the tabular data, as it did prior to v3.0. Along similar lines, the `setSchema` method's previous behavior is now provided by the `setTablesSchema` method. The `setSchema` method now takes two arguments, the second of which is optional, also aiding backward compatibility. The `delSchema` method removes both types of schema. * * * ## v2.2 Note: The tools module has been removed in TinyBase v6.0. This release includes a new tools module. These tools are not intended for production use, but are instead to be used as part of your engineering workflow to perform tasks like generating APIs from schemas, or schemas from data. For example: import {createTools} from 'tinybase/tools'; store.setTable('pets', { fido: {species: 'dog'}, felix: {species: 'cat'}, cujo: {species: 'dog'}, }); const tools = createTools(store); const [dTs, ts] = tools.getStoreApi('shop'); This will generate two files: // -- shop.d.ts -- /* Represents the 'pets' Table. */ export type PetsTable = {[rowId: Id]: PetsRow}; /* Represents a Row when getting the content of the 'pets' Table. */ export type PetsRow = {species: string}; //... // -- shop.ts -- export const createShop: typeof createShopDecl = () => { //... }; This release includes a new `tinybase` CLI tool which allows you to generate Typescript definition and implementation files direct from a schema file: npx tinybase getStoreApi schema.json shop api Definition: [...]/api/shop.d.ts Implementation: [...]/api/shop.ts Finally, the tools module also provides ways to track the overall size and structure of a `Store` for use while debugging. * * * ## v2.1 This release allows you to create indexes where a single `Row` `Id` can exist in multiple slices. You can utilize this to build simple keyword searches, for example. Simply provide a custom getSliceIdOrIds function in the `setIndexDefinition` method that returns an array of `Slice` `Ids`, rather than a single `Id`: import {createIndexes} from 'tinybase'; store.setTable('pets', { fido: {species: 'dog'}, felix: {species: 'cat'}, rex: {species: 'dog'}, }); const indexes = createIndexes(store); indexes.setIndexDefinition('containsLetter', 'pets', (_, rowId) => rowId.split(''), ); console.log(indexes.getSliceIds('containsLetter')); // -> ['f', 'i', 'd', 'o', 'e', 'l', 'x', 'r'] console.log(indexes.getSliceRowIds('containsLetter', 'i')); // -> ['fido', 'felix'] console.log(indexes.getSliceRowIds('containsLetter', 'x')); // -> ['felix', 'rex'] This functionality is showcased in the Word Frequencies demo if you would like to see it in action. * * * ## v2.0 **Announcing the next major version of TinyBase 2.0!** This is an exciting release that evolves TinyBase towards becoming a reactive, relational data store, complete with querying, sorting, and pagination. Here are a few of the highlights... ### Query Engine The flagship feature of this release is the new `queries` module. This allows you to build expressive queries against your data with a SQL-adjacent API that we've cheekily called TinyQL. The query engine lets you select, join, filter, group, sort and paginate data. And of course, it's all reactive! The best way to see the power of this new engine is with the two new demos we've included this release:  The Car Analysis demo showcases the analytical query capabilities of TinyBase v2.0, grouping and sorting dimensional data for lightweight analytical usage, graphing, and tabular display. _Try this demo here._  The Movie Database demo showcases the relational query capabilities of TinyBase v2.0, joining together information about movies, directors, and actors from across multiple source tables. _Try this demo here._ ### Sorting and Pagination To complement the query engine, you can now sort and paginate `Row` `Ids`. This makes it very easy to build grid-like user interfaces (also shown in the demos above). To achieve this, the `Store` now includes the `getSortedRowIds` method (and the `addSortedRowIdsListener` method for reactivity), and the `Queries` object includes the equivalent `getResultSortedRowIds` method and `addResultSortedRowIdsListener` method. These are also exposed in the optional `ui-react` module via the `useSortedRowIds` hook, the `useResultSortedRowIds` hook, the `SortedTableView` component and the `ResultSortedTableView` component, and so on. ### `Queries` in the `ui-react` module The v2.0 query functionality is fully supported by the `ui-react` module (to match support for `Store`, `Metrics`, `Indexes`, and `Relationship` objects). The `useCreateQueries` hook memoizes the creation of app- or component-wide Query objects; and the `useResultTable` hook, `useResultRow` hook, `useResultCell` hook (and so on) let you bind you component to the results of a query. This is, of course, supplemented with higher-level components: the `ResultTableView` component, the `ResultRowView` component, the `ResultCellView` component, and so on. See the Building A UI With Queries guide for more details. ### It's a big release! Thank you for all your support as we brought this important new release to life, and we hope you enjoy using it as much as we did building it. Please provide feedback via Github, Bluesky, and X! * * * ## v1.3 Adds support for explicit transaction start and finish methods, as well as listeners for transactions finishing. The `startTransaction` method and `finishTransaction` method allow you to explicitly enclose a transaction that will make multiple mutations to the `Store`, buffering all calls to the relevant listeners until it completes when you call the `finishTransaction` method. Unlike the `transaction` method, this approach is useful when you have a more 'open-ended' transaction, such as one containing mutations triggered from other events that are asynchronous or not occurring inline to your code. You must remember to also call the `finishTransaction` method explicitly when the transaction is started with the `startTransaction` method, of course. store.setTables({pets: {fido: {species: 'dog'}}}); store.addRowListener('pets', 'fido', () => console.log('Fido changed')); store.startTransaction(); store.setCell('pets', 'fido', 'color', 'brown'); store.setCell('pets', 'fido', 'sold', true); store.finishTransaction(); // -> 'Fido changed' In addition, see the `addWillFinishTransactionListener` method and the `addDidFinishTransactionListener` method for details around listening to transactions completing. Together, this release allows stores to couple their transaction life-cycles together, which we need for the query engine. Note: this API was updated to be more comprehensive in v4.0. * * * ## v1.2 This adds a way to revert transactions if they have not met certain conditions. When using the `transaction` method, you can provide an optional `doRollback` callback which should return true if you want to revert the whole transaction at its conclusion. The callback is provided with two objects, `changedCells` and `invalidCells`, which list all the net changes and invalid attempts at changes that were made during the transaction. You will most likely use the contents of those objects to decide whether the transaction should be rolled back. Note: this API was updated to be more comprehensive in v4.0. * * * ## v1.1 This release allows you to listen to invalid data being added to a `Store`, allowing you to gracefully handle errors, rather than them failing silently. There is a new listener type `InvalidCellListener` and a `addInvalidCellListener` method in the `Store` interface. These allow you to keep track of failed attempts to update the `Store` with invalid `Cell` data. These listeners can also be mutators, allowing you to address any failed writes programmatically. For more information, please see the `addInvalidCellListener` method documentation. In particular, this explains how this listener behaves for a `Store` with a `TablesSchema`. --- ## Page: https://tinybase.org/guides/the-basics/getting-started/ This guide gets you up and running quickly with TinyBase. It is not intended to be a detailed introduction to installing JavaScript build- and run-time environments! It assumes that you have (or know how to have) a browser or Node-based development environment. Note that TinyBase requires a reasonably modern environment, as it makes extensive use of contemporary JavaScript features. A regularly-updated browser and Node 16 (or above) are recommended. If you find you need older compatibility, there are additional transpilations in the `es6` folder of the distribution. Let's go! ### TinyBase from a template Vite is a build tool that makes it easy to get started with modern web projects based on application templates. To use the TinyBase template, firstly make a copy of it: npx tiged tinyplex/vite-tinybase my-tinybase-app Then go into the directory, install the dependencies, and run the application: cd my-tinybase-app npm install npm run dev The final step will display a local URL, which should serve up a basic TinyBase application for you:  In fact, there are eleven templates for TinyBase, depending on whether you want to use TypeScript or React, and the integrations you want to target. Instructions are available in the README of each: | Template | Language | React | Plus | | --- | --- | --- | --- | | vite-tinybase | JavaScript | No | | | vite-tinybase-ts | TypeScript | No | | | vite-tinybase-react | JavaScript | Yes | | | vite-tinybase-ts-react | TypeScript | Yes | | | vite-tinybase-ts-react-sync | TypeScript | Yes | Synchronization | | vite-tinybase-ts-react-sync-durable-object | TypeScript | Yes | Sync & Durable Objects | | vite-tinybase-ts-react-pglite | TypeScript | Yes | PGlite | | vite-tinybase-ts-react-crsqlite | TypeScript | Yes | CR-SQLite | | tinybase-ts-react-partykit | TypeScript | Yes | PartyKit | | tinybase-ts-react-electricsql | TypeScript | Yes | ElectricSQL | | expo/examples/with-tinybase | JavaScript | Yes | React Native & Expo | ### TinyBase in a browser Another simple way to get started with TinyBase is to include it from a CDN in a web page. Create a file called `index.html`, for example: <html> <head> <title>My First TinyBase App</title> <script type="importmap"> {"imports": {"tinybase": "https://esm.sh/tinybase@6.0.0"}} </script> <script type="module"> import {createStore} from 'tinybase'; addEventListener('load', () => { const store = createStore(); store.setValue('v1', 'Hello'); store.setCell('t1', 'r1', 'c1', 'World'); document.body.innerHTML = store.getValue('v1') + ' ' + store.getCell('t1', 'r1', 'c1'); }); </script> </head> <body /> </html> Open this file in your browser and you should see the words 'Hello World' on the screen, each having been written to, and read from, a `Store`. Note that the TinyBase module is pulled in from esm.sh, and the `importmap` allows you to use a regular import statement in the main script section. ### TinyBase in a Node application TinyBase is packaged on NPM, so you can easily install it as a dependency for your application. mkdir MyFirstTinyBaseApp cd MyFirstTinyBaseApp npm init -y npm install tinybase Create a file in this directory called `index.mjs`: import {createStore} from 'tinybase'; const store = createStore(); store.setValue('v1', 'Hello'); store.setCell('t1', 'r1', 'c1', 'World'); console.log(store.getValue('v1') + ' ' + store.getCell('t1', 'r1', 'c1')); Run this module script with: node index.mjs Again, you will see the words 'Hello World' on the screen, having each been written to, and read from, a `Store`. If that all worked, you are set up and ready to learn more about TinyBase! From here on, we will mostly show Node-based code snippets, but most should be easily translatable to run in a browser too. Before we move on, you should be aware that the overall package includes a number of different versions of TinyBase, transpiled for different targets and formats. You may want to take a look at the Importing TinyBase guide if the code above isn't working in your environment - React Native in particular. Let's move onto the Creating A Store guide. --- ## Page: https://tinybase.org/guides/the-basics/creating-a-store/ This guide shows you how to create a new `Store`. Creating a `Store` requires just a simple call to the `createStore` function from the `store` module. import {createStore} from 'tinybase'; const store = createStore(); Easy enough! The returned `Store` starts off empty of course: console.log(store.getValues()); // -> {} console.log(store.getTables()); // -> {} To fix that, let's move onto the Writing To Stores guide. --- ## Page: https://tinybase.org/guides/the-basics/writing-to-stores/ This guide shows you how to write data to a `Store`. A `Store` has two types of data in it: keyed values ('`Values`'), and tabular data ('`Tables`'). `Values` are just `Id`/`Value` pairs. `Tables` on the other hand, have a simple hierarchical structure: * The `Store`'s `Tables` object contains a number of `Table` objects. * Each `Table` contains a number of `Row` objects. * Each `Row` contains a number of `Cell` objects. Once you have created a `Store`, you can write data to it with one of its setter methods, according to the level of the hierarchy that you want to set. For example, you can set the data for the keyed value structure of `Store` with the `setValues` method: import {createStore} from 'tinybase'; const store = createStore(); store.setValues({employees: 3, open: true}); Similarly, you can set the data for the tabular structure of `Store` with the `setTables` method: store.setTables({pets: {fido: {species: 'dog'}}}); Hopefully self-evidently, this sets the `Store` to have two `Values` (`employees` and `open`, which are `3` and `true` respectively). It also has one `Table` object (called `pets`), containing one `Row` object (called `fido`), containing one `Cell` object (called `species` and with the string value `dog`): console.log(store.getValues()); // -> {employees: 3, open: true} console.log(store.getTables()); // -> {pets: {fido: {species: 'dog'}}} You can also alter `Store` data at different granularities with the `setValue` method, the `setTable` method, the `setRow` method, and the `setCell` method: store.setValue('employees', 4); console.log(store.getValues()); // -> {employees: 4, open: true} store.setTable('species', {dog: {price: 5}}); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog'}}, species: {dog: {price: 5}}} store.setRow('species', 'cat', {price: 4}); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog'}}, species: {dog: {price: 5}, cat: {price: 4}}} store.setCell('pets', 'fido', 'color', 'brown'); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', color: 'brown'}}, species: {dog: {price: 5}, cat: {price: 4}}} The data in a `Value` or a `Cell` can be a string, a number, or a boolean type. It's worth mentioning here that there are two extra methods to manipulate `Row` objects. The `addRow` method is like the `setRow` method but automatically assigns it a new unique `Id`. And the `setPartialRow` method lets you update multiple `Cell` values in a `Row` without affecting the others. (setPartialValues does the same for `Values`.) ### Deleting Data There are dedicated deletion methods (again, for each level of granularity), such as the `delValue` method, the `delTable` method, the `delRow` method, and the `delCell` method. For example: store.delValue('employees'); console.log(store.getValues()); // -> {open: true} store.delTable('species'); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', color: 'brown'}}} Deletions are also implied when you set an object that omits something that existed before: console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', color: 'brown'}}} store.setRow('pets', 'fido', {species: 'dog'}); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog'}}} // The `color` Cell has been deleted. `Table` and `Row` objects cannot be empty - if they are, they are removed - which leads to a cascading effect when you remove the final child of a parent object: store.delCell('pets', 'fido', 'species'); console.log(store.getTables()); // -> {} // The `fido` Row and `pets` Table have been recursively deleted. ### Summary That's a quick overview on how to write data to a `Store`. But of course you want to get it out again too! In the examples above, we've used the `getValues` method and the `getTables` method to get a view into the data in the `Store`. Unsurprisingly, you can also use more granular methods to get data out - for which we proceed to the Reading From Stores guide. --- ## Page: https://tinybase.org/guides/the-basics/reading-from-stores/ This guide shows you how to read data from a `Store`. While we're here, notice how the the `createStore` function and setter methods return the `Store` again, so we can easily instantiate it by chaining methods together: import {createStore} from 'tinybase'; const store = createStore() .setValues({employees: 3, open: true}) .setTables({ pets: {fido: {species: 'dog'}}, species: {dog: {price: 5}}, }); To get the data out again, according to the level of the hierarchy that you want to get data for, you can use the `getValues` method, the `getValue` method, the `getTables` method, the `getTable` method, the `getRow` method, or the `getCell` method. By now, this should be starting to look intuitive. (I hope so! If not, let me know!) console.log(store.getValues()); // -> {employees: 3, open: true} console.log(store.getValue('employees')); // -> 3 console.log(store.getTables()); // -> {pets: {fido: {species: 'dog'}}, species: {dog: {price: 5}}} console.log(store.getTable('pets')); // -> {fido: {species: 'dog'}} console.log(store.getRow('pets', 'fido')); // -> {species: 'dog'} console.log(store.getCell('pets', 'fido', 'species')); // -> 'dog' It is worth noting that the return types of these methods are by value, not by reference. So if you manipulate the returned object, the `Store` is not updated: const fido = store.getRow('pets', 'fido'); fido.color = 'brown'; console.log(fido); // -> {species: 'dog', color: 'brown'} console.log(store.getRow('pets', 'fido')); // -> {species: 'dog'} ### Handling Non-Existent Data The `hasValue` method, the `hasTable` method, the `hasRow` method, and the `hasCell` method can be used to see whether a given object exists, without having to read it: console.log(store.hasValue('website')); // -> false console.log(store.hasTable('customers')); // -> false console.log(store.hasRow('pets', 'fido')); // -> true When you try to access something that doesn't exist, you'll receive an `undefined` value for a `Value` or `Cell`, or an empty object: console.log(store.getValue('website')); // -> undefined console.log(store.getTable('customers')); // -> {} console.log(store.getRow('pets', 'felix')); // -> {} console.log(store.getCell('pets', 'fido', 'color')); // -> undefined ### Enumerating `Ids` A `Store` contains `Value` and `Table` objects, keyed by `Id`. A `Table` contains `Row` objects, keyed by `Id`. And a `Row` contains `Cell` objects, keyed by `Id`. You can enumerate the `Id` keys for each with the `getValueIds` method, the `getTableIds` method, the `getRowIds` method, or the `getCellIds` method - each of which return arrays: console.log(store.getValueIds()); // -> ['employees', 'open'] console.log(store.getTableIds()); // -> ['pets', 'species'] console.log(store.getRowIds('pets')); // -> ['fido'] console.log(store.getCellIds('pets', 'fido')); // -> ['species'] There is also the `getSortedRowIds` method that lets you get the `Ids` sorted by a specific `Cell` `Id`, and the `getTableCellIds` method that lets you get all the `Ids` used across a whole `Table`. Again, the return types of these methods are by value, not by reference. So if you manipulate the returned array, the `Store` is not updated: const tableIds = store.getTableIds(); tableIds.pop(); console.log(tableIds); // -> ['pets'] console.log(store.getTableIds()); // -> ['pets', 'species'] Finally, the `forEachValue` method, the `forEachTable` method, the `forEachRow` method, and the `forEachCell` method each provide a convenient way to iterate over these objects and their children in turn: store.forEachTable((tableId, forEachRow) => { console.log(tableId); forEachRow((rowId) => console.log(`- ${rowId}`)); }); // -> 'pets' // -> '- fido' // -> 'species' // -> '- dog' ### Summary So far, this should seem relatively straightforward. For more information on all of these methods, you'll find a lot more in the `Store` documentation. The reactive TinyBase magic starts to happen when we register listeners on the `Store` so we don't have to keep explicitly fetching data. For that, we proceed to the Listening To Stores guide. --- ## Page: https://tinybase.org/guides/the-basics/listening-to-stores/ This guide shows you how to listen to changes in the data in a `Store`. By now, you'll have noticed that there are always consistent methods for each level of the `Store` hierarchy, and the way you register listeners is no exception: * Listen to `Values` with the `addValuesListener` method. * Listen to `Value` `Ids` with the `addValueIdsListener` method. * Listen to a `Value` with the `addValueListener` method. And for tabular data: * Listen to `Tables` with the `addTablesListener` method. * Listen to `Table` `Ids` with the `addTableIdsListener` method. * Listen to a `Table` with the `addTableListener` method. * Listen to Cells `Ids` across a `Table` with the `addTableCellIdsListener` method. * Listen to `Row` `Ids` with the `addRowIdsListener` method. * Listen to sorted `Row` `Ids` with the `addSortedRowIdsListener` method. * Listen to a `Row` with the `addRowListener` method. * Listen to `Cell` `Ids` with the `addCellIdsListener` method. * Listen to a `Cell` with the `addCellListener` method. You can also listen to attempts to write invalid data to a `Value` with the `addInvalidValueListener` method, and to a `Cell` with the `addInvalidCellListener` method. Let's start with the simplest type of listener, addTablesListener, which listens to changes to any tabular data in the `Store`. Firstly, let's set up some simple data: import {createStore} from 'tinybase'; const store = createStore().setTables({ pets: {fido: {species: 'dog'}}, species: {dog: {price: 5}}, }); We can then use the `addTablesListener` method to register a function on the `Store` that will be called whenever the data in the `Store` changes: const listenerId = store.addTablesListener(() => console.log('Tables changed!'), ); Let's test it out by updating a `Cell` in the `Store`: store.setCell('species', 'dog', 'price', 6); // -> 'Tables changed!' The listener will be called, regardless of which type of setter method was used to make the change. But a change needs to have been made! If a setter method was used to no effect, the listener is not called: store.setCell('pets', 'fido', 'species', 'dog'); // Since the data didn't actually change, the listener was not called. It is important to note that by default, you can't mutate the `Store` with code inside a listener, and attempting to do so will fail silently. We cover how to mutate the `Store` from with in a listener (in order to adhere to a `TablesSchema`, for example) in the Mutating Data With Listeners guide. ### Cleaning Up Listeners You will have noticed that the `addTablesListener` method didn't return a reference to the `Store` object (so you can't chain other methods after it), but an `Id` representing the registration of that listener. You can use that `Id` to remove the listener at a later stage with the `delListener` method: store.delListener(listenerId); store.setCell('species', 'dog', 'price', 7); // Listener has been unregistered and so is not called. It's good habit to remove the listeners you are no longer using. Note that listener `Ids` are commonly re-used, so you have removed a listener with a given `Id`, don't try to use that `Id` again. ### Listener Parameters In the example above, we registered a listener that didn't take any parameters. However, all `Store` listeners are called with at least a reference to the `Store`, and often a convenient `getCellChange` function that lets you inspect changes that might have happened: const listenerId2 = store.addTablesListener((store, getCellChange) => console.log(getCellChange('species', 'dog', 'price')), ); store.setCell('species', 'dog', 'price', 8); // -> [true, 7, 8] store.delListener(listenerId2); See the `addTablesListener` method documentation for more information on these parameters. When you listen to changes down inside a `Store` (with more granular listeners), you will also be passed `Id` parameters reflecting what changed. For example, here we register a listener on the `fido` `Row` in the `pets` `Table`: const listenerId3 = store.addRowListener( 'pets', 'fido', (store, tableId, rowId) => console.log(`${rowId} row in ${tableId} table changed`), ); store.setCell('pets', 'fido', 'color', 'brown'); // -> 'fido row in pets table changed' store.delListener(listenerId3); When you register a `CellListener` listener with the `addCellListener` method, that also receives parameters containing the old and new `Cell` values. ### Wildcard Listeners The fact that the listeners are passed parameters for what changed becomes very useful when you register wildcard listeners. These listen to changes at a particular part of the `Store` hierarchy but not necessarily to a specific object. So for example, you can listen to changes to any `Row` in a given `Table`. To wildcard what you want to listen to, simply use `null` in place of an `Id` argument when you add a listener: const listenerId4 = store.addRowListener(null, null, (store, tableId, rowId) => console.log(`${rowId} row in ${tableId} table changed`), ); store.setCell('pets', 'fido', 'color', 'walnut'); // -> 'fido row in pets table changed' store.setCell('species', 'dog', 'price', '9'); // -> 'dog row in species table changed' store.delListener(listenerId4); You can intermingle wildcards and actual `Id` values for any of the parameters. So, for example, you could listen to the `Cell` values with a given `Id` in any `Row` in a given `Table`, and so on. Note that you can't use the wildcard technique with the `addSortedRowIdsListener` method. You must explicitly specify just one `Table`, for performance reasons. ### Summary We've now seen how to create a `Store`, set data in it, read it back out, and set up listeners to detect whenever it changes. Finally we'll cover how to wrap multiple changes together, in the Transactions guide. --- ## Page: https://tinybase.org/guides/the-basics/transactions/ * TinyBase * Guides * The Basics * Transactions This guide shows you how to wrap multiple changes to the data in a `Store`. A transaction is a sequence of changes made to a `Store`. No listeners will be fired until the full transaction is complete. This is a useful way to debounce listener side-effects and ensure that you are only responding to net changes. `Changes` are made silently during the transaction, and listeners relevant to the changes you have made will instead only be called when the whole transaction is complete. A transaction can also be rolled back and the original state of the `Store` will be restored. ### Creating Transactions The `transaction` method takes a function that makes multiple mutations to the store, buffering all calls to the relevant listeners until it completes. import {createStore} from 'tinybase'; const store = createStore().setTables({pets: {fido: {species: 'dog'}}}); const listenerId = store.addRowListener('pets', 'fido', () => console.log('Fido changed'), ); // Multiple changes, not in a transaction store.setCell('pets', 'fido', 'color', 'brown'); store.setCell('pets', 'fido', 'sold', false); // -> 'Fido changed' // -> 'Fido changed' // Multiple changes in a transaction store.transaction(() => { store.setCell('pets', 'fido', 'color', 'walnut'); store.setCell('pets', 'fido', 'sold', true); }); // -> 'Fido changed' store.delListener(listenerId); If multiple changes are made to a piece of `Store` data throughout the transaction, a relevant listener will only be called with the final value (assuming it is different to the value at the start of the transaction), regardless of the changes that happened in between. For example, if a `Cell` had a value `'a'` and then, within a transaction, it was changed to `'b'` and then `'c'`, any `CellListener` registered for that cell would be called once as if there had been a single change from `'a'` to `'c'`: const listenerId2 = store.addCellListener( 'pets', 'fido', 'color', (store, tableId, rowId, cellId, newCell) => console.log(`Fido color changed to ${newCell}`), ); store.transaction(() => { store.setCell('pets', 'fido', 'color', 'black'); store.setCell('pets', 'fido', 'color', 'brown'); }); // -> 'Fido color changed to brown' store.delListener(listenerId2); Note that transactions can be nested. Relevant listeners will be called only when the outermost one completes. ### Rolling Back Transactions The `transaction` method takes a second optional parameter, `doRollback`. This is a callback that you can use to rollback the transaction if it did not complete to your satisfaction. This example makes multiple changes to the `Store`, including some attempts to update a `Cell` with invalid values. The `doRollback` callback fetches information about the changes and invalid attempts, and then judges that the transaction should be rolled back to its original state. store.transaction( () => { store.setCell('pets', 'fido', 'color', 'black'); store.setCell('pets', 'fido', 'eyes', ['left', 'right']); store.setCell('pets', 'fido', 'buyer', {name: 'Bob'}); }, () => { const [, , changedCells, invalidCells] = store.getTransactionLog(); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', color: 'black', sold: true}}} console.log(changedCells); // -> {pets: {fido: {color: ['brown', 'black']}}} console.log(invalidCells); // -> {pets: {fido: {eyes: [['left', 'right']], buyer: [{name: 'Bob'}]}}} return invalidCells['pets'] != null; }, ); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', color: 'brown', sold: true}}} ### Listening to transactions You can register listeners to the start and finish of a transaction. There are three points in its lifecycle: | Event | Add a listener with | When | Can mutate data | | --- | --- | --- | --- | | Start | `addStartTransactionListener` | Before changes | Yes | | WillFinish | `addWillFinishTransactionListener` | After changes and other mutator listeners | Yes | | DidFinish | `addDidFinishTransactionListener` | After non-mutator listeners | No | For example: store.delTables(); 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); ### Summary We've covered all of the essential basics of working with a TinyBase `Store`, but that's still just the start! Before we move on, we have a quick aside about how to use various flavors of TinyBase in your app, in the Importing TinyBase guide. --- ## Page: https://tinybase.org/guides/the-basics/importing-tinybase/ This guide provides an aside about importing TinyBase into your application. ### The Simplest Imports The simplest import of TinyBase is: import {createMetrics, createStore} from 'tinybase'; This will get you an ESNext, ESM, non-minified import of the main `tinybase` module, (which contains most of the core functionality), and should be enough to get started. You may also want to import specific persister, synchronizer, or UI modules: import {createSessionPersister} from 'tinybase/persisters/persister-browser'; import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client'; import {useCell} from 'tinybase/ui-react'; import {TableInHtmlTable} from 'tinybase/ui-react-dom'; // ... etc All the example code throughout these guides and the API documentation are shown with the correct imports so that you can be clear about which functions and types come from which modules. ### Using TinyBase Submodules The `tinybase` module is the master package of most of the core functionality. It includes the following submodules: * The `store` module * The `metrics` module * The `indexes` module * The `relationships` module * The `queries` module * The `checkpoints` module * The `mergeable-store` module * The `common` module Since many of the submodules above share compiled-in dependencies, the master package is smaller to include than including all of the submodules separately. However, for a very minimal set of submodules, you may save size by including them piecemeal. If you only wanted a `Store` and a `Metrics` object, for example, you could import them alone like this: import {createMetrics} from 'tinybase/metrics'; import {createStore} from 'tinybase/store'; // ... With a good minifier in your application bundler, however, you may find that this level of granularity is unnecessary, and that you can just stick with the overall `tinybase` module for most things. The submodules for various `Persister` and `Synchronizer` types are _not_ included in the main tinybase module, but should be imported separately from inside the `persisters` and `synchronizers` folders. See the Persistence and Synchronization guides, respectively, for more details. ### Targets And Formats Prior to TinyBase v6.0, the NPM package included a number of different versions of each module, transpiled for different targets and formats. From v6.0 onwards, only ESNext, ESM modules are included in the main package. However, both non-minified and minified versions are available: the default is non-minified code, but minified versions are available in the top-level `min` folder: import {createStore} from 'tinybase'; // non-minified // or import {createStore} from 'tinybase/min'; // minified ### Indicating Schema-based Typing As we will see in more details in the following TinyBase And TypeScript guide, it is possible to use schema-aware type definitions by appending `with-schemas` to the very end of the path like this: import {createStore} from 'tinybase/with-schemas'; // NB the 'with-schemas' ### Putting It All Together As long as you put the optional parts of the path in the right order, you can access all the valid combinations of minification, sub-module and schema support. The syntax for the import (split onto different lines for clarity) is: tinybase [ /min ] [ /store | /metrics | /queries | ... ] [ /with-schemas ] For example, this is a non-exhaustive list of options that are all valid: | Import | Minified | Sub-module | With schemas | | --- | --- | --- | --- | | `import {...} from 'tinybase';` | no | | no | | `import {...} from 'tinybase/with-schemas';` | no | | yes | | `import {...} from 'tinybase/min';` | yes | | no | | `import {...} from 'tinybase/store/with-schemas'` | no | `store` | no | | `import {...} from 'tinybase/min/store/with-schemas'` | yes | `store` | yes | | ... | | | | If all else fails, take a look into the package folder and see what's what! ### React Native If you are using React Native - for example with Expo - be aware that the Metro bundler does not currently support module resolution very well. You may have to add in the exact file path to be explicit about your imports: import {createStore} from 'tinybase/index.js'; import {useCell} from 'tinybase/ui-react/index.js'; This situation is evolving however, so you may find these extra file names unnecessary as bundler support improves. Check out the Expo TinyBase example for a simple working template to get started with TinyBase and React Native. ### ESlint Resolver Issues There is a known issue with the `no-unresolved` ESlint rule whereby it does not understand the `exports` section of the TinyBase `package.json`. You may wish to disable that rule if you are getting false positives using TinyBase submodules. ### Enough! OK, we're done with the `import` shenanigans. Let's briefly look at how TinyBase benefits from using TypeScript to improve your developer experience in the TinyBase and TypeScript guide. --- ## Page: https://tinybase.org/guides/the-basics/tinybase-and-typescript/ This guide summarizes the two different levels of TypeScript coverage you can use with TinyBase. ### 1\. Basic Type Support Out of the box, TinyBase has complete type coverage for all of its modules. So for example, setting and getting tabular and key-value data will obey the system's constraints. A `Cell` or a `Value` can only be a number, string, or boolean, for example: import {createStore} from 'tinybase'; const store = createStore(); store.setValues({employees: 3}); // OK store.setValues({employees: true}); // OK store.setValues({employees: ['Alice', 'Bob']}); // TypeScript error This basic typing of the API is comprehensively described throughout in the API documentation. ### 2\. Schema-based Typing The next step up is when you provide a schema for your TinyBase data. This more tightly constrains the types of `Table`, `Cell`, and `Value` that your `Store` can contain. Since v3.1, TinyBase can provide typing that adapts according to the schema when you import the `with-schemas` version of the library. For example: import {createStore} from 'tinybase/with-schemas'; // NB the 'with-schemas' const store = createStore().setValuesSchema({ employees: {type: 'number'}, open: {type: 'boolean', default: false}, }); store.setValues({employees: 3}); // OK store.setValues({employees: true}); // TypeScript error store.setValues({employees: 3, website: 'pets.com'}); // TypeScript error (The separate import is provided because the schema-based autocomplete and errors can be fairly verbose and confusing when you only need the basic type support.) Read more about this technique in the Schema-Based Typing guide. ### Summary TinyBase provides different levels of typed support for your data, depending on how prescriptive you want it to be and your personal preferences. Next we will run through some of the many ways you can build your app around TinyBase in the Architectural Options guide. --- ## Page: https://tinybase.org/guides/the-basics/architectural-options/ This guide discusses some of the ways in which you can use TinyBase, and how you can architect it into the bigger picture of how your app is built. Before we go any further, remember that TinyBase is an in-memory data store that runs within a JavaScript environment like a browser or a worker. Whilst it can theoretically stand alone in a simple app, you'll probably want to preserve, share, or sync the data between reloads and sessions. Here are the options we'll discuss in this guide: * Standalone TinyBase * Read-Only Cloud Data * Browser Storage * Client Database Storage * Client-Only Synchronization * Client-Server Synchronization * Third-Party Synchronization As you can see lot of what we'll be discussing is how to integrate TinyBase with different persistence and synchronization techniques - whether on the client or the server, or both. Let's go! ### 0\. Standalone TinyBase In this option, a TinyBase `Store` is instantiated when the app runs. During its use, data is added or updated, and rendered accordingly. When the app is reloaded or closed, the data is lost. * **Pros**: This is very simple to set up, and good for prototyping small apps. * **Cons**: It's a transient experience and your users' data won't show up again if they refresh their browser, revisit the app later. The Todo App v1 (the basics) demo is a good example of how to get started with an app like this. ### 1\. Read-Only Cloud Data As one way to enhance the standalone app option, you can use the TinyBase persistence framework to load data from a server when the app starts, and then store it in a `Store`. This might be appropriate for an app that uses read-only structured data which is small enough to fit into memory (and fast enough to load at start up). * **Pros**: This is also relatively simple to set up, and good for data-centric or reference apps. * **Cons**: The data is not interactive (or at least, changes made locally will not be saved). At some point, the size of the data needed might start to challenge the browser's memory - or the time you are prepared to let the startup spinner run for! - after which local persistence and pagination might be preferable. The Movie Database demo, the Word Frequencies demo, the Car Analysis demo, and the City Database demo are all good examples of this sort of 'read-only' app, each exercising different aspects of the TinyBase framework. The Countries demo also loads one of its stores from a server. See the `RemotePersister` interface for more details on how to pull data down from a server. Note that it _is_ possible to configure that `Persister` to 'save' data back to the server, but for anything other than the simplest use-cases, you may want to consider using a `Synchronizer` instead, so that multiple clients can edit data without conflict. We'll discuss that option later in this guide. ### 2\. Browser Storage Another way to upgrade the standalone experience is to have TinyBase persist its data to the browser's storage. This way, the data or state can be preserved when the app is reloaded, or even when it is returned to in a later session. This is a basic 'local-only' approach. * **Pros**: This approach provides persistence of data and state between reloads and sessions. * **Cons**: Data is only stored in one particular browser on one particular device. The data may also get evicted (and its size limited) by the browser, depending on the storage used. The Todo App v1 (the basics) and the Todo App v3 (persistence) demo are good examples of how to get started with an app like this. Also see the `SessionPersister` and `LocalPersister` documentation for more details. ### 3\. Client Database Storage As well as its native storage techniques, there are now many options for running richer client-side databases, such as SQLite or PGLite, in the browser. These solutions typically rely on WASM packages to provide the database functionality and then store the underlying data in IndexedDB or OPFS. Similar database run times might also be provided natively in some client environments (like React Native or Node- or Bun-based solutions). TinyBase can persist its own data to a relational database like this, either serialized as JSON or in a more structured relational form, where TinyBase tables map directly to database tables. * **Pros**: This approach provides more structured persistence of data with less likelihood of eviction. Relational data can also be queried or updated with SQL outside of TinyBase (though it will nevertheless react to those changes). * **Cons**: A WASM payload is required to provide the database functionality in the browser, increasing asset size, and some of these client solutions are still young and experimental. See the `SqliteWasmPersister` and `PglitePersister` documentation for two of the browser-based database solutions. `ExpoSqlitePersister` is appropriate for Expo-based React Native projects. ### 4\. Client-Only Synchronization Regardless of the client storage solution you choose, you may want to synchronize data between clients, either because you're supporting single users with multiple devices, or multiple users sharing common data. This relies on you instantiating your data in a TinyBase `MergeableStore`, which captures metadata for deterministic synchronization. Each client then uses a `Synchronizer` (such as the WebSocket-based `WsSynchronizer`) to negotiate changes with others. WebSockets require a lightweight server that can forward and broadcast messages between clients. * **Pros**: This approach lets users share data between devices or with each other. Combined with client storage, this can also support offline usage with eventual reconciliation. * **Cons**: There is technically no 'source of truth': each client negotiates to merge changes with each other. If all devices evict their client storage simultaneously, the data is lost. See the `MergeableStore` documentation and the Synchronization guide to understand how this works. The Todo App v6 (collaboration) demo shows client-to-client synchronization for a simple to-do list application. The server is created, in a simple Node- or Bun-style environment with the `createWsServer` function. ### 5\. Client-Server Synchronization From here it is only a simple step to add server storage into the mix, removing the risk of all client devices clearing their data simultaneously and it being lost. Here, the synchronizer server (which is coordinating messages between clients) _also_ acts as a 'client' with an instance of TinyBase itself. This is most usefully then persisted to a server storage solution, such as SQLite, PostgreSQL, the file system, or a Cloudflare Durable Object. * **Pros**: The server can now be considered a more permanent 'source of truth' than clients. Authentication and data integrity can now be more easily enforced. * **Cons**: The only minor downside of this approach is the need for the server to have a copy of the TinyBase store in memory, so the default solutions page it in and out from the persisted storage when clients connect or disconnect. See the `createWsServer` function for details of how to create a persister for the synchronization server, such as `Sqlite3Persister` or `PostgresPersister`. A reliable all-in-one solution is to run both synchronization and storage on Cloudflare. Check out the Cloudflare Durable Objects guide and the dedicated Vite starter template to see how to set this up. ### 6\. Third-Party Synchronization For completeness, it's worth mentioning that TinyBase can also integrate with other database and synchronization platforms. In these cases, you simply persist data locally and the third-party service takes care of the synchronization to a server or cloud service. (It is also possible to persist your data via two other open-source CRDT solutions, namely Yjs and Automerge, using the `YjsPersister` and `AutomergePersister` interfaces respectively.) * **Pros**: You can add TinyBase into applications that are already using a third-party synchronization platform. Conversely you can then abstract away your choice of synchronization platform behind a consistent TinyBase API, preventing vendor lock-in. * **Cons**: This approach adds additional moving parts, other libraries, and possible fees for commercial services, based on usage. For more details on these interfaces, see the `ElectricSqlPersister`, `PowerSyncPersister`, and `LibSqlPersister` (Turso) interfaces. The APIs, consistent with the other SQLite- and PostgreSQL-based persisters, are described in the Database Persistence guide. ### Mix It Up! It should go without saying that very few of these options are mutually exclusive! You can mix and match them as you see fit, depending on the way you want your persistence and synchronization to work. Not only that, you can of course have multiple Stores in your app, each with its own persistence and synchronization strategy. For example, a complex app might have multiple TinyBase stores use in lots of different ways: * Transient state that is stored just in memory and not preserved between sessions. * Views, routes and settings that are stored in the browser's local storage. * Reference data that is read in from a server at startup, perhaps then stored in a client database for faster future loads. * User documents that are synchronized between clients and a server, with the server persisting them as an 'source of truth'. TinyHub uses several of these techniques throughout its client app. Its different stores are each initialized with different persister strategies. ### Summary TinyBase provides many different architectural choices, depending on the type of app you are building, and where you want the data to reside when not in use. Next we will show how you can quickly build user interfaces on top of a `Store`, and for that, it's time to proceed to the Building UIs guide. --- ## Page: https://tinybase.org/guides/building-uis/getting-started-with-ui-react/ * TinyBase * Guides * Building UIs * Getting Started With ui-react To build React-based user interfaces with TinyBase, you will need to install the `ui-react` module in addition to the main module, and, of course, React itself. For example, in an HTML file, you can get started with boilerplate that might look like this: <html> <head> <title>My First TinyBase App</title> <script type="importmap"> { "imports": { "tinybase": "https://esm.sh/tinybase@6.0.0", "tinybase/ui-react": "https://esm.sh/tinybase@6.0.0/ui-react", "react": "https://esm.sh/react@^19.0.0", "react/jsx-runtime": "https://esm.sh/react@^19.0.0/jsx-runtime", "react-dom/client": "https://esm.sh/react-dom@^19.0.0/client" } } </script> <script type="module" src="https://esm.sh/tsx"></script> <script type="text/jsx"> import {createStore} from "tinybase"; import {CellView} from "tinybase/ui-react"; import {createRoot} from "react-dom/client"; import React from "react"; const store = createStore(); store.setCell('t1', 'r1', 'c1', 'Hello World'); createRoot(document.body).render( <CellView store={store} tableId="t1" rowId="r1" cellId="c1" />, ); </script> </head> <body /> </html> Open this file in your browser and you should see the words 'Hello World' on the screen, having been written to, and read from, a `Store`, and then rendered by the `CellView` component from the `ui-react` module. Note that the standalone `https://esm.sh/tsx` script and `text/jsx` type on the script here are merely to support JSX in the browser and for the purposes of illustrating how to get started quickly. In a production environment you should pre-compile and your JSX and modules to create a bundled browser app. If you're bundling the whole app, you can of course import the `ui-react` module something like this. Boilerplate aside, let's move on to understand how to use hooks in the `ui-react` module, with the Using React Hooks guide. --- ## Page: https://tinybase.org/guides/building-uis/using-react-hooks/ There are reactive hooks in the `ui-react` module for accessing every part of a `Store`, as well as more advanced things like the `Metrics` and `Indexes` objects. By reactive hooks, we mean that the hook not only fetches part of the `Store`, but that it also registers a listener that will then cause a component to re-render if the underlying value changes. Therefore, it's easy to describe a user interface in terms of raw data in a `Store`, and know that it will stay updated when the data changes. To start with a simple example, we use the `useCell` hook in a component called `App` to get the value of a `Cell` and render it in a `<span>` element. When the `Cell` is updated, so is the HTML. import React from 'react'; import {createRoot} from 'react-dom/client'; import {createStore} from 'tinybase'; import {useCell} from 'tinybase/ui-react'; const store = createStore().setCell('pets', 'fido', 'color', 'brown'); const App = () => <span>{useCell('pets', 'fido', 'color', store)}</span>; const app = document.createElement('div'); const root = createRoot(app); root.render(<App />); console.log(app.innerHTML); // -> '<span>brown</span>' store.setCell('pets', 'fido', 'color', 'walnut'); console.log(app.innerHTML); // -> '<span>walnut</span>' There are hooks that correspond to each of the `Store` getter methods: * The `useValues` hook is the reactive equivalent of the `getValues` method. * The `useValueIds` hook is the reactive equivalent of the `getValueIds` method. * The `useValue` hook is the reactive equivalent of the `getValue` method. And for tabular data: * The `useTables` hook is the reactive equivalent of the `getTables` method. * The `useTableIds` hook is the reactive equivalent of the `getTableIds` method. * The `useTable` hook is the reactive equivalent of the `getTable` method. * The `useTableCellIds` hook is the reactive equivalent of the `getTableCellIds` method. * The `useRowIds` hook is the reactive equivalent of the `getRowIds` method. * The `useSortedRowIds` hook is the reactive equivalent of the `getSortedRowIds` method. * The `useRow` hook is the reactive equivalent of the `getRow` method. * The `useCellIds` hook is the reactive equivalent of the `getCellIds` method. * The `useCell` hook is the reactive equivalent of the `getCell` method. They have the same return types. For example, the `useTable` hook returns an object: import {useTable} from 'tinybase/ui-react'; const App2 = () => <span>{JSON.stringify(useTable('pets', store))}</span>; root.render(<App2 />); console.log(app.innerHTML); // -> '<span>{"fido":{"color":"walnut"}}</span>' store.setCell('pets', 'fido', 'species', 'dog'); console.log(app.innerHTML); // -> '<span>{"fido":{"color":"walnut","species":"dog"}}</span>' When the component is unmounted, the listener will be automatically removed. This means you can use these hooks without having to worry too much about the lifecycle of how your component interacts with the `Store`. ### Using Hooks To Set Data In an interactive application, you don't just want to read data. You also want to be able to set it in response to user's actions. For this purpose, there is a group of hooks that return callbacks for setting data based on events. Let's start with a simple example, the `useSetCellCallback` hook. The `Cell` to be updated needs to be identified by the `Table`, `Row`, and `Cell` `Id` parameters. The fourth parameter to the hook is a parameterized callback (that will be memoized based on the dependencies in the fifth parameter). The responsibility of that function is to return the value that will be used to update the `Cell`. It's probably easier to understand with an example: import {useSetCellCallback} from 'tinybase/ui-react'; const App3 = () => { const handleClick = useSetCellCallback( 'pets', 'fido', 'sold', (event) => event.bubbles, [], store, ); return ( <span> Sold: {useCell('pets', 'fido', 'sold', store) ? 'yes' : 'no'} <br /> <button onClick={handleClick}>Sell</button> </span> ); }; root.render(<App3 />); console.log(app.innerHTML); // -> '<span>Sold: no<br><button>Sell</button></span>' const button = app.querySelector('button'); // User clicks the <button> element: // -> button MouseEvent('click', {bubbles: true}) console.log(store.getTables()); // -> {pets: {fido: {color: 'walnut', species: 'dog', sold: true}}} console.log(app.innerHTML); // -> '<span>Sold: yes<br><button>Sell</button></span>' In the real-world, a more valid case for using the event parameter might be to handle the content of a text input to write into the `Store`. See the Todo demo for a working example of doing that with the `useAddRowCallback` hook to add new todos. ### Other Hook Types The hooks to read and write `Store` data (described above) will be the ones you most commonly use. For completeness, there are three other broad groups of hooks. Firstly, there are those that create callbacks to delete data (such as the `useDelRowCallback` hook), which should be self-explanatory. Then there are hooks that are used to create objects (including `Store` objects, but also `Metrics`, and `Indexes` objects, and so on). These are essentially convenient aliases for memoization so that object creation can be performed inside a component without fear of creating a new instance per render: import {useCreateStore} from 'tinybase/ui-react'; const App4 = () => { const store = useCreateStore(() => { console.log('Store created'); return createStore().setTables({pets: {fido: {species: 'dog'}}}); }); return <span>{store.getCell('pets', 'fido', 'species')}</span>; }; root.render(<App4 />); // -> 'Store created' root.render(<App4 />); // No second Store creation There is also a final group of hooks that add listeners (such as the `useCellListener` hook). Since the regular hooks (like the `useCell` hook) already register listeners to track changes, you won't often need to use these unless you need to establish a listener in a component that has some other side-effect, such as mutating data to enforce a schema, for example. ### Summary The hooks available in the `ui-react` module make it easy to connect your user interface to TinyBase `Store` data. It also contains some convenient components that you can use to build your user interface more declaratively. For that, let's proceed to the Using React Components guide. --- ## Page: https://tinybase.org/guides/building-uis/using-react-components/ The reactive components in the `ui-react` module let you declaratively display parts of a `Store`. These are all essentially convenience wrappers around the hooks we described in the Using React Hooks guide, but make it easy to build hierarchical component trees from the `Store` data. For example, the `ValuesView` component wraps around the `useValueIds` hook to render child `ValueView` components. Similarly, the `TablesView` component wraps around the `useTableIds` hook to render child `TableView` components, which in turn can render child `RowView` components and `CellView` components. In this simple example, the `CellView` component is used to render the color `Cell` in a `<span>`: import React from 'react'; import {createRoot} from 'react-dom/client'; import {createStore} from 'tinybase'; import {CellView} from 'tinybase/ui-react'; const store = createStore().setCell('pets', 'fido', 'color', 'brown'); const App = () => ( <span> <CellView tableId="pets" rowId="fido" cellId="color" store={store} /> </span> ); const app = document.createElement('div'); const root = createRoot(app); root.render(<App />); console.log(app.innerHTML); // -> '<span>brown</span>' store.setCell('pets', 'fido', 'color', 'walnut'); console.log(app.innerHTML); // -> '<span>walnut</span>' These components have very plain default renderings, and don't even generate HTML or use ReactDOM. This means that the `ui-react` module works just as well with React Native or other React-based rendering systems. It does mean though, that if you use the default `RowView` component, you will simply render a concatenation of the values of its Cells: import {RowView} from 'tinybase/ui-react'; store.setCell('pets', 'fido', 'weight', 42); const App2 = () => ( <span> <RowView tableId="pets" rowId="fido" store={store} /> </span> ); root.render(<App2 />); console.log(app.innerHTML); // -> '<span>walnut42</span>' This is not a particularly nice rendering! Even for the purposes of debugging data, you may want to separate the values, and this can be cheaply done with the `separator` prop: const App3 = () => ( <span> <RowView tableId="pets" rowId="fido" store={store} separator="," /> </span> ); root.render(<App3 />); console.log(app.innerHTML); // -> '<span>walnut,42</span>' Going further, the `debugIds` prop helps you see the structure of the objects with their `Ids`. const App4 = () => ( <span> <RowView tableId="pets" rowId="fido" store={store} debugIds={true} /> </span> ); root.render(<App4 />); console.log(app.innerHTML); // -> '<span>fido:{color:{walnut}weight:{42}}</span>' These are slightly more readable, but are still not really appropriate to actually build a user interface! For that we need to understand how to customize components. ### Customizing Components More likely than JSON-like strings, you will want to customize or compose the rendering of parts of the `Store` for your UI. The way this works is that each of the react-ui module components has a prop that takes an alternative rendering for its children. For example, the `TableView` component takes a `rowComponent` prop that lets you indicate how each `Row` should be rendered, and the `RowView` component takes a `cellComponent` prop that lets you indicate how each `Cell` should be rendered. The component passed in to such props itself needs to be capable of taking the same props that the default component would have. To render the contents of a `Table` into an HTML table, therefore, you might set the components up like this: import {TableView} from 'tinybase/ui-react'; const MyTableView = (props) => ( <table> <tbody> <TableView {...props} rowComponent={MyRowView} /> </tbody> </table> ); const MyRowView = (props) => ( <tr> <th>{props.rowId}</th> <RowView {...props} cellComponent={MyCellView} /> </tr> ); const MyCellView = (props) => ( <td> <CellView {...props} /> </td> ); const App5 = () => <MyTableView store={store} tableId="pets" />; root.render(<App5 />); console.log(app.innerHTML); // -> '<table><tbody><tr><th>fido</th><td>walnut</td><td>42</td></tr></tbody></table>' That is now starting to resemble a useful UI for tabular data! A final touch here is that each view can also let you create custom props for each of its children. For example the `getRowComponentProps` prop of the `TableView` component should be a function that returns additional props that will be passed to each child. See the API documentation for more examples. ### Summary The components available in the `ui-react` module make it easy to enumerate over objects to build your user interface with customized, composed components. This will work wherever the React module does, including React Native. When you are building an app in a web browser, however, where the ReactDOM module is available, TinyBase includes pre-made HTML components. We will look at these in the next Using React DOM Components guide. --- ## Page: https://tinybase.org/guides/building-uis/using-react-dom-components/ The reactive components in the `ui-react-dom` module let you declaratively display parts of a `Store` in a web browser, where the ReactDOM module is available. These are generally implementations of the components we discussed in the previous guide, but are specifically designed to render HTML content in a browser. Styling and class names are very basic, since you are expected to style them with CSS to fit your app's overall styling. The easiest way to understand these components is to see them all in action in the UI Components demos. There are table-based components for rendering `Tables`, sorted `Tables`, `Values`, and so on: | Component | Purpose | | | --- | --- | --- | | `ValuesInHtmlTable` | Renders `Values`. | demo | | `TableInHtmlTable` | Renders a `Table`. | demo | | `SortedTableInHtmlTable` | Renders a sorted `Table`, with optional interactivity. | demo | | `SliceInHtmlTable` | Renders a `Slice` from an `Index`. | demo | | `RelationshipInHtmlTable` | Renders the local and remote `Tables` of a relationship | demo | | `ResultTableInHtmlTable` | Renders a `ResultTable`. | demo | | `ResultSortedTableInHtmlTable` | Renders a sorted `ResultTable`, with optional interactivity. | demo | There are also editable components for individual Cells and `Values`: | Component | Purpose | | | --- | --- | --- | | `EditableCellView` | Renders a `Cell` and lets you change its type and value. | demo | | `EditableValueView` | Renders a `Value` and lets you change its type and value. | demo | We finish off this section with a best practice to avoid passing the global `Store` down into components. Please proceed to to the Using Context guide! --- ## Page: https://tinybase.org/guides/building-uis/using-context/ The `ui-react` module includes a context provider that lets you avoid passing global objects down through your component hierarchy. One thing you may have noticed (especially with the hooks) is how we've had to reference the global `Store` object within components (or potentially drill it through the hierarchy with props). It's very likely that your whole app (or parts of it) will use the same `Store` throughout, though. To help with this, the `Provider` component lets you specify a `Store` that all the hooks and components will bind to automatically. Simply provide the `Store` in the `store` prop, and it will be used by default. Notice how the `store` variable is not referenced in the child `Pane` component here, for example: import React from 'react'; import {createRoot} from 'react-dom/client'; import {createStore} from 'tinybase'; import {CellView, Provider, useCell, useCreateStore} from 'tinybase/ui-react'; const App = () => { const store = useCreateStore(() => createStore().setTables({pets: {fido: {species: 'dog', color: 'brown'}}}), ); return ( <Provider store={store}> <Pane /> </Provider> ); }; const Pane = () => ( <span> <CellView tableId="pets" rowId="fido" cellId="species" />, {useCell('pets', 'fido', 'color')} </span> ); const app = document.createElement('div'); const root = createRoot(app); root.render(<App />); console.log(app.innerHTML); // -> '<span>dog,brown</span>' Obviously this requires your components to be used in a context where you know the right sort of `Store` will be available. ### Context With Multiple Stores In cases where you want to have multiple `Store` objects available to an application, the `Provider` component takes a `storesById` prop that is an object keyed by `Id`. Your hooks and components use the `Id` to indicate which they want to use: const App2 = () => { const petStore = useCreateStore(() => createStore().setTables({pets: {fido: {species: 'dog'}}}), ); const planetStore = useCreateStore(() => createStore().setTables({planets: {mars: {moons: 2}}}), ); return ( <Provider storesById={{pet: petStore, planet: planetStore}}> <Pane2 /> </Provider> ); }; const Pane2 = () => ( <span> <CellView tableId="pets" rowId="fido" cellId="species" store="pet" />, {useCell('planets', 'mars', 'moons', 'planet')} </span> ); root.render(<App2 />); console.log(app.innerHTML); // -> '<span>dog,2</span>' ### Nesting Context `Provider` components can be nested and the contexts are merged. This last example is a little verbose, but shows how two `Store` objects each keyed with a different `Id` are both visible, despite having been set in two different `Provider` components: const App3 = () => { const petStore = useCreateStore(() => createStore().setTables({pets: {fido: {species: 'dog'}}}), ); return ( <Provider storesById={{pet: petStore}}> <OuterPane /> </Provider> ); }; const OuterPane = () => { const planetStore = useCreateStore(() => createStore().setTables({planets: {mars: {moons: 2}}}), ); return ( <Provider store={planetStore}> <InnerPane /> </Provider> ); }; const InnerPane = () => ( <span> <CellView tableId="pets" rowId="fido" cellId="species" store="pet" />, {useCell('planets', 'mars', 'moons')} </span> ); root.render(<App3 />); console.log(app.innerHTML); // -> '<span>dog,2</span>' ### Summary We have covered the main parts of the `ui-react` module, including its hooks and components, and the way it supports context to make `Store` objects available. Next we talk about how a `Store` can have a `TablesSchema` and can be persisted. Let's move onto the Schemas guide to find out more. --- ## Page: https://tinybase.org/guides/schemas/using-schemas/ Schemas are a simple declarative way to say what data you would like to store. A `ValuesSchema` simply describes specific `Value` types and default. A `TablesSchema` describes specific `Cell` types and defaults in specific `Tables`. Each is a JavaScript object, and to apply them, you use the `setValuesSchema` method and `setTablesSchema` method respectively. ### Adding A `ValuesSchema` Typically you will want to set a `ValuesSchema` prior to loading and setting data in your `Store`: import {createStore} from 'tinybase'; const store = createStore().setValuesSchema({ employees: {type: 'number'}, open: {type: 'boolean', default: false}, }); store.setValues({employees: 3, website: 'pets.com'}); console.log(store.getValues()); // -> {employees: 3, open: false} In the above example, we indicated that the `Store` contains an `employees` `Value` (which needs to be a number) and an `open` `Value` (which needs to be a boolean). As you can see, when a `Values` object is used that doesn't quite match those constraints, the data is corrected. The `website` `Value` is ignored, and the missing `open` `Value` gets defaulted to `false`. ### Adding A `TablesSchema` Tabular schemas are similar. Set a `TablesSchema` prior to loading data into your `Tables`: store.setTablesSchema({ pets: { species: {type: 'string'}, sold: {type: 'boolean', default: false}, }, }); store.setRow('pets', 'fido', {species: 'dog', color: 'brown', sold: 'maybe'}); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', sold: false}}} In the above example, we indicated that the `Store` contains a single `pets` `Table`, each `Row` of which has a `species` `Cell` (which needs to be a string) and a `sold` `Cell` (which needs to be a boolean). Again, when a `Row` is added that doesn't quite match those constraints, the data is corrected. The `color` `Cell` is ignored, and the `sold` string is corrected to the default `false` value. In general, if a default value is provided (and its type is correct), you can be certain that that `Cell` will always be present in a `Row`. If the default value is _not_ provided (or its type is incorrect), the `Cell` may be missing from the `Row`. But when it is present you can be guaranteed it is of the correct type. ### Altering A Schema You can also set or change the `ValuesSchema` or `TablesSchema` after data has been added to the `Store`. Note that this may result in a change to data in the `Store`, as defaults are applied or as invalid `Value`, `Table`, `Row`, or `Cell` objects are removed. These changes will fire any listeners to that data, as expected. In this example, the `TablesSchema` gains a new required field that is added to the current `Row` to make it compliant: store.setTablesSchema({ pets: { species: {type: 'string'}, legs: {type: 'number', default: 4}, sold: {type: 'boolean', default: false}, }, }); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', sold: false, legs: 4}}} The `TablesSchema` does not attempt to cast data. If a field needs to be of a particular type, it really needs to be of that type: store.setCell('pets', 'fido', 'legs', '3'); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', sold: false, legs: 4}}} store.setCell('pets', 'fido', 'legs', 3); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', sold: false, legs: 3}}} ### Be Aware Of Potential Data Loss In order to guarantee that a schema is met, `Value` or `Cell` data may be removed. In the case of a `Cell` being removed, this might result in the removal of a whole `Row`. In this case, for example, the `TablesSchema` changes quite dramatically and none of the Cells of the existing data match it, so the `Row` is deleted: store.setTablesSchema({ pets: { color: {type: 'string'}, weight: {type: 'number'}, }, }); console.log(store.getTables()); // -> {} When no longer needed, you can also completely removes existing schemas with the `delValuesSchema` method or the `delTablesSchema` method. ### Summary Adding a schema gives you a simple declarative way to describe your data structure. You can also benefit from a better developer experience based on these schemas, and for that we turn to the Schema-Based Typing guide. --- ## Page: https://tinybase.org/guides/schemas/schema-based-typing/ * TinyBase * Guides * Schemas * Schema-Based Typing You can use type definitions that infer API types from the schemas you apply, providing a powerful way to improve your developer experience when you know the shape of the data being stored. The schema-based definitions can be accessed by adding the `with-schemas` suffix to your imports. For example: import {createStore} from 'tinybase/with-schemas'; // NB the 'with-schemas' const store = createStore().setValuesSchema({ employees: {type: 'number'}, open: {type: 'boolean', default: false}, }); store.setValues({employees: 3}); // OK store.setValues({employees: true}); // TypeScript error store.setValues({employees: 3, website: 'pets.com'}); // TypeScript error In this example, the store is known to have the `ValuesSchema` provided, and all relevant methods will have type constraints accordingly, even for listeners: store.addValueListener(null, (store, valueId, newValue, oldValue) => { valueId == 'employees'; // OK valueId == 'open'; // OK valueId == 'website'; // TypeScript error if (valueId == 'employees') { newValue as number; // OK oldValue as number; // OK newValue as boolean; // TypeScript error oldValue as boolean; // TypeScript error } if (valueId == 'open') { newValue as boolean; // OK oldValue as boolean; // OK } }); ### Getting the Typed `Store` Only the `setSchema` method, `setTablesSchema` method, and `setValuesSchema` method return a typed `Store` object. So, to benefit from the typing, ensure you assign your `Store` variable to what those methods return, rather than just the `createStore` function. For example, the following will work at runtime, but you will _not_ benefit from the developer experience of typing on the `store` variable as we did in the example above. import {createStore} from 'tinybase/with-schemas'; const store = createStore(); // This is not a schema-typed Store store.setValuesSchema({ employees: {type: 'number'}, open: {type: 'boolean', default: false}, }); // Instead you should use the return type from this method One further thing to be aware of is that for the typing to work effectively, the schema must be passed in directly, or, if it is a variable, as a constant: const valuesSchema = { employees: {type: 'number'}, open: {type: 'boolean', default: false}, } as const; // NB the `as const` modifier store.setValuesSchema(valuesSchema); It's worth noting that typing will adapt according to schemas being added, removed, or changed: const tablesSchema = { pets: {species: {type: 'string'}}, } as const; const valuesSchema = { employees: {type: 'number'}, open: {type: 'boolean', default: false}, } as const; const store = createStore(); const storeWithBothSchemas = store.setSchema(tablesSchema, valuesSchema); const storeWithJustValuesSchema = storeWithBothSchemas.delTablesSchema(); const storeWithValuesAndNewTablesSchema = storeWithBothSchemas.setTablesSchema({ pets: { species: {type: 'string'}, sold: {type: 'boolean', default: false}, }, }); ### Typing The ui-react Module Schema-based typing for the `ui-react` module is handled a little differently, due to the fact that all of the hooks and components are top level functions in the module. It would be frustrating to apply a schema to type each and every one in turn. Instead, you can use the `WithSchemas` type (which takes the `typeof` the schemas), and the following pattern after your import. This applies the schema types to the whole module en masse, and then you can select the hooks and components you want to use: import React from 'react'; import * as UiReact from 'tinybase/ui-react/with-schemas'; import {createStore} from 'tinybase/with-schemas'; const tablesSchema = { pets: {species: {type: 'string'}}, } as const; const valuesSchema = { employees: {type: 'number'}, open: {type: 'boolean', default: false}, } as const; // Cast the whole module to be schema-based with WithSchemas: const UiReactWithSchemas = UiReact as UiReact.WithSchemas< [typeof tablesSchema, typeof valuesSchema] >; // Deconstruct to access the hooks and components you need: const {TableView, useTable, ValueView} = UiReactWithSchemas; const store = createStore().setSchema(tablesSchema, valuesSchema); const App = () => ( <div> <TableView store={store} tableId="species" /> {/* OK */} <TableView store={store} tableId="customers" /> {/* TypeScript error */} {/* ... */} </div> ); Note that in React Native, the resolution of modules and types isn't yet quite compatible with Node and TypeScript. You may need to try something like the following to explicitly load code and types from different folders: // code import React from 'react'; import * as UiReact from 'tinybase/ui-react'; import type {WithSchemas} from 'tinybase/ui-react/with-schemas'; // types import {TablesSchema, ValuesSchema, createStore} from 'tinybase/with-schemas'; const tablesSchema = { pets: {species: {type: 'string'}}, } as const; const valuesSchema = { employees: {type: 'number'}, open: {type: 'boolean', default: false}, } as const; const UiReactWithSchemas = UiReact as unknown as WithSchemas< [typeof tablesSchema, typeof valuesSchema] >; //... ### Multiple Stores In the case that you have multiple `Store` objects with different schemas, you will need to use `WithSchemas` several times, and deconstruct each, something like this: const UiReactWithPetShopSchemas = UiReact as UiReact.WithSchemas< [typeof petShopTablesSchema, typeof petShopValuesSchema] >; const { TableView: PetShopTableView, useTable: usePetShopTable, ValueView: usePetShopValueView, } = UiReactWithPetShopSchemas; const UiReactWithSettingsSchemas = UiReact as UiReact.WithSchemas< [typeof settingsTablesSchema, typeof settingsValuesSchema] >; const { TableView: SettingsTableView, useTable: useSettingsTable, ValueView: useSettingsValueView, } = UiReactWithSettingsSchemas; const petShopStore = createStore().setSchema( petShopTablesSchema, petShopValuesSchema, ); const settingsStore = createStore().setSchema( settingsTablesSchema, settingsValuesSchema, ); const App = () => ( <div> <PetShopTableView store={petShopStore} tableId="species" /> <SettingsTableView store={settingsStore} tableId="viewSettings" /> {/* ... */} </div> ); ### Summary Schema-based typing provides a powerful developer-time experience for checking your code and autocompletion in your IDE. Remember to use the `with-schema` suffix on the import path and use the patterns described above. We move on to discussing more complex programmatic enforcement of your data, and for that we turn to the Mutating Data With Listeners guide. --- ## Page: https://tinybase.org/guides/schemas/mutating-data-with-listeners/ * TinyBase * Guides * Schemas * Mutating Data With Listeners Although listeners are normally prevented from updating data, there are times when you may want to - such as when you are programmatically checking your data as it gets updated. ### Configuring Listeners By default, listeners cannot update data. For instance, you might imagine that this code will replace 'walnut' with 'brown' when the `color` `Cell` is updated. But in fact the correction will fail silently: import {createStore} from 'tinybase'; const store = createStore(); store.setRow('pets', 'fido', {species: 'dog', color: 'black'}); const colorListenerId = store.addCellListener( 'pets', null, 'color', (store, tableId, rowId, cellId, newCell) => { if (newCell == 'walnut') { store.setCell(tableId, rowId, cellId, 'brown'); } }, ); store.setCell('pets', 'fido', 'color', 'walnut'); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', color: 'walnut'}}} store.delListener(colorListenerId); ### Mutator Listeners To indicate that a listener is a 'mutator' (meaning that you are willing to allow it to change data), simply set the `mutator` flag to true on the method that adds the listener to the `Store`. In this example, the `Cell` value must be one of the known species, or else it is set to 'unknown': const SPECIES = ['unknown', 'dog', 'cat', 'worm']; store.addCellListener( 'pets', null, 'species', (store, tableId, rowId, cellId, newCell) => { if (!SPECIES.includes(newCell)) { store.setCell(tableId, rowId, cellId, SPECIES[0]); } }, true, // This listener is permitted to mutate the Store. ); store.setCell('pets', 'fido', 'species', 'worm'); console.log(store.getTables()); // -> {pets: {fido: {species: 'worm', color: 'walnut'}}} store.setCell('pets', 'fido', 'species', 'wolf'); console.log(store.getTables()); // -> {pets: {fido: {species: 'unknown', color: 'walnut'}}} Note that all the listeners that are marked as mutators will run _before_ all of those that are not. This means you can be sure that when your read-only listeners fire, the data within the `Store` has already been been fully manipulated to your liking. ### Summary We have now effectively implemented a programmatic schema, one which is capable of ensuring values are valid, and defaulting them to something else if not. This same technique can also constrain numeric `Cell` values to valid ranges, for example - and even potentially have `Cell` values which are constrained by other `Cell` values (though note that this needs to be done carefully to avoid expensive or impossible constraint solutions). One common circumstance for creating a `TablesSchema` for a `Store` is when you are loading data from a source and you want to ensure the data is sculpted as your application require. But how do you save and load `Store` data? For that we proceed to the Persistence guides. --- ## Page: https://tinybase.org/guides/persistence/an-intro-to-persistence/ The persister module framework lets you save and load `Store` data to and from different locations, or underlying storage types. Remember that TinyBase Stores are in-memory data structures, so you will generally want to use a `Persister` to store that data longer-term. For example, they are useful for preserving `Store` data between browser sessions or reloads, saving or loading browser state to or from a server, saving `Store` data to disk in a environment with filesystem access, or, in v4.0 and above, to SQLite, PostgreSQL, or CRDT frameworks like Yjs and Automerge. ### Types of Persisters Many entry points are provided (in separately installed modules), each of which returns different types of `Persister` that can load and save a `Store`. Between them, these allow you to store your TinyBase data locally, remotely, to databases, and across synchronization boundaries with CRDT frameworks. #### Basic Persisters These are reasonably simple Persisters that generally load and save a JSON-serialized version of your `Store`. They are good for smaller data sets and where you need to have something saved in a basic browser or server environment. | `Persister` | Storage | | --- | --- | | `SessionPersister` | Browser session storage | | `LocalPersister` | Browser local storage | | `FilePersister` | Local file | | `IndexedDbPersister` | Browser IndexedDB | | `RemotePersister` | Remote server | #### Database Persisters These are Persisters that can load and save either a JSON-serialized, or tabular version of your `Store` into a database. They are good for larger data sets, often on a server - but can also work in a browser environment when a SQLite instance is available. | `Persister` | Storage | | --- | --- | | `Sqlite3Persister` | SQLite in Node, via sqlite3 | | `SqliteWasmPersister` | SQLite in a browser, via sqlite-wasm | | `ExpoSqlitePersister` | SQLite in React Native, via expo-sqlite | | `CrSqliteWasmPersister` | SQLite CRDTs, via cr-sqlite-wasm | | `ElectricSqlPersister` | Electric SQL, via electric | | `LibSqlPersister` | LibSQL for Turso, via libsql-client | | `PowerSyncPersister` | PowerSync, via powersync-sdk | | `PostgresPersister` | PostgreSQL, via postgres | | `PglitePersister` | PostgreSQL, via PGlite | See the Database Persistence guide for details on how to work with databases. #### Third-Party CRDT & Socket Persisters These Persisters can bind your `Store` into third-party CRDT frameworks, or synchronize over sockets to PartyKit. | `Persister` | Storage | | --- | --- | | `YjsPersister` | Yjs CRDTs, via yjs | | `AutomergePersister` | Automerge CRDTs, via automerge-repo | | `PartyKitPersister` | PartyKit, via the `persister-partykit-server` module | See the Third-Party CRDT Persistence guide for more complex synchronization with the CRDT frameworks. There is also a way to develop custom Persisters of your own, which we describe in the Custom Persistence guide. ### `Persister` Operations A `Persister` lets you explicitly save or load data, with the `save` method and the `load` method respectively. These methods are both asynchronous (since the underlying data storage may also be) and return promises. As a result you should use the `await` keyword to call them in a way that guarantees subsequent execution order. In this example, a `Persister` saves data to, and loads it from, the browser's session storage: import {createStore} from 'tinybase'; import {createSessionPersister} from 'tinybase/persisters/persister-browser'; const store = createStore() .setValues({employees: 3}) .setTables({pets: {fido: {species: 'dog'}}}); const persister = createSessionPersister(store, 'petStore'); await persister.save(); console.log(sessionStorage.getItem('petStore')); // -> '[{"pets":{"fido":{"species":"dog"}}},{"employees":3}]' sessionStorage.setItem( 'petStore', '[{"pets":{"toto":{"species":"dog"}}},{"employees":4}]', ); await persister.load(); console.log(store.getTables()); // -> {pets: {toto: {species: 'dog'}}} console.log(store.getValues()); // -> {employees: 4} sessionStorage.clear(); ### Automatic Loading and Saving When you don't want to deal with explicit persistence operations, a `Persister` object also provides automatic saving and loading. Automatic saving listens for changes to the `Store` and persists the data immediately. Automatic loading listens (or polls) for changes to the persisted data and reflects those changes in the `Store`. You can start automatic saving or loading with the `startAutoSave` method and `startAutoLoad` method. Both are asynchronous since they will do an immediate save and load before starting to listen for subsequent changes. You can stop the behavior with the `stopAutoSave` method and `stopAutoLoad` method (which are synchronous). In this example, both automatic loading and saving are configured: await persister.startAutoLoad([{pets: {fido: {species: 'dog'}}}, {}]); await persister.startAutoSave(); store.delValues().setTables({pets: {felix: {species: 'cat'}}}); // ... console.log(sessionStorage.getItem('petStore')); // -> '[{"pets":{"felix":{"species":"cat"}}},{}]' sessionStorage.setItem('petStore', '[{"pets":{"toto":{"species":"dog"}}},{}]'); // -> StorageEvent('storage', {storageArea: sessionStorage, key: 'petStore'}) // ... console.log(store.getTables()); // -> {pets: {toto: {species: "dog"}}} persister.destroy(); sessionStorage.clear(); Note that the `startAutoLoad` method also takes a default set of `Tables` so that the `Store` can be instantiated with good data if the persistence layer is empty (such as when this is the first time the app has been executed). ### A Caveat You may often want to have both automatic saving and loading of a `Store` so that changes are constantly synchronized (allowing basic state preservation between browser tabs, for example). The framework has some basic provisions to prevent race conditions - for example it will not attempt to save data if it is currently loading it and vice-versa - and will sequentially `schedule` methods that could cause race conditions. That said, be aware that you should always comprehensively test your persistence strategy to understand the opportunity for data loss (in the case of trying to save data to a server under poor network conditions, for example). To help debug such issues, since v4.0.4, the create methods for all `Persister` objects take an optional `onIgnoredError` argument. This is a handler for the errors that the `Persister` would otherwise ignore when trying to save or load data (such as when handling corrupted stored data). It's recommended you use this for debugging persistence issues, but only in a development environment. Database-based `Persister` objects also take an optional `onSqlCommand` argument for logging commands and queries made to the underlying database. ### Summary Use the `persisters` module to load and save data from and to a variety of common persistence layers. When these don't suffice, you can also develop custom Persisters of your own. Next we move on to look at how to fully synchronize TinyBase Stores with databases, particularly SQLite, in the Database Persistence guide. --- ## Page: https://tinybase.org/guides/persistence/database-persistence/ Since v4.0, there are various options for persisting `Store` data to and from SQLite databases, via a range of third-party modules. There are currently seven SQLite-based persistence options, and two for PostgreSQL: | `Persister` | Storage | | --- | --- | | `Sqlite3Persister` | SQLite in Node, via sqlite3 | | `SqliteWasmPersister` | SQLite in a browser, via sqlite-wasm | | `ExpoSqlitePersister` | SQLite in React Native, via expo-sqlite | | `CrSqliteWasmPersister` | SQLite CRDTs, via cr-sqlite-wasm | | `ElectricSqlPersister` | Electric SQL, via electric | | `LibSqlPersister` | LibSQL for Turso, via libsql-client | | `PowerSyncPersister` | PowerSync, via powersync-sdk | | `PostgresPersister` | PostgreSQL, via postgres | | `PglitePersister` | PostgreSQL, via PGlite | (Take a look at the vite-tinybase-ts-react-crsqlite template, for example, which demonstrates Vulcan's cr-sqlite to provide persistence and synchronization via the third of these.) Each creation function takes a database reference, and a `DatabasePersisterConfig` object to describe its configuration. There are two modes for persisting a `Store` with a database: * A JSON serialization of the whole `Store`, which is stored in a single row of a table (normally called `tinybase`) within the database. This is configured by providing a `DpcJson` object. * A tabular mapping of `Table` `Ids` to database table names (and vice-versa). `Values` are stored in a separate special table (normally called `tinybase_values`). This is configured by providing a `DpcTabular` object. Note that changes made to the database (outside of a `Persister`) are picked up immediately if they are made via the same connection or library that it is using. If the database is being changed by another client, the `Persister` needs to poll for changes. Hence both configuration types also contain an `autoLoadIntervalSeconds` property which indicates how often it should do that. This defaults to 1 second. ### Using JSON Serialization To get started, we'll use JSON serialization to save a `Store` to SQLite in the browser, via the sqlite-wasm module. Firstly, use the module to initiate a database. Here it will be created in memory, but typically you would use the origin private file system (OPFS) as a storage back-end. import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; import {createStore} from 'tinybase'; import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm'; const sqlite3 = await sqlite3InitModule(); let db = new sqlite3.oo1.DB(':memory:', 'c'); Next create a simple `Store` with a small amount of data: const store = createStore().setTables({pets: {fido: {species: 'dog'}}}); The `Persister` itself is created with the createSqliteWasmPersister method. This requires a reference to the the `sqlite3` module itself and the database. We're not providing any configuration so it will use JSON serialization into the default table (namely one called `tinybase`). const jsonPersister = createSqliteWasmPersister(store, sqlite3, db); Now we can use the `Persister` to save data to the `Store`. Of course you can also use the `startAutoSave` method to make it automatic. await jsonPersister.save(); And we can check the database to ensure the data has been stored: console.log(db.exec('SELECT * FROM tinybase;', {rowMode: 'object'})); // -> [{_id: '_', store: '[{"pets":{"fido":{"species":"dog"}}},{}]'}] If the data in the database is changed... db.exec( 'UPDATE tinybase SET store = ' + `'[{"pets":{"felix":{"species":"cat"}}},{}]' WHERE _id = '_';`, ); ...it can be picked up by loading it explicitly or with auto-loading: await jsonPersister.load(); console.log(store.getTables()); // -> {pets: {felix: {species: 'cat'}}} jsonPersister.destroy(); Please see the `DpcJson` documentation for more detail on configuring this type of persistence. ### Using Tabular Mapping More flexibly, you can map distinct `Store` `Tables` to database tables and back again. This is likely a more suitable approach if you are binding TinyBase to existing data. To use this technique, you must provide a `DatabasePersisterConfig` object when you create the `Persister`, and specify how you would like `Store` `Tables` (and `Values`) to correspond to tables in the database. It is important to note that both the tabular mapping in ('save') and out ('load') of an underlying database are disabled by default. This is to ensure that if you pass in an existing populated database you don't run the immediate risk of corrupting or losing all your data. This configuration therefore takes a `tables` property object (with child `load` and `save` property objects) and a `values` property object. One of these at least is required for the `Persister` to do anything! Let's demonstrate. We start by creating a new database and resetting the data in the `Store` to put into it: db = new sqlite3.oo1.DB(':memory:', 'c'); store.setTables({ pets: {felix: {species: 'cat'}, fido: {species: 'dog'}}, species: {dog: {price: 5}, cat: {price: 4}}, }); The persister itself has a more complex configuration as described above: const tabularPersister = createSqliteWasmPersister(store, sqlite3, db, { mode: 'tabular', tables: { save: {pets: 'pets', species: 'animal_species'}, load: {pets: 'pets', animal_species: 'species'}, }, }); Notice how there is a symmetric mapping of `Store` `Table` to database table and vice-versa. It is deliberate that this must be spelled out like this, so that your intent to connect to (or especially mutate) existing data is very explicit. Again, we can save the `Store`... await tabularPersister.save(); ...and see the resulting data in the SQLite database: console.log(db.exec('SELECT * FROM pets;', {rowMode: 'object'})); // -> [{_id: 'felix', species: 'cat'}, {_id: 'fido', species: 'dog'}] console.log(db.exec('SELECT * FROM animal_species;', {rowMode: 'object'})); // -> [{_id: 'dog', price: 5}, {_id: 'cat', price: 4}] And, as expected, making a change to the database and re-loading brings the changes back into the `Store`: db.exec(`INSERT INTO pets (_id, species) VALUES ('cujo', 'wolf')`); await tabularPersister.load(); console.log(store.getTable('pets')); // -> {felix: {species: 'cat'}, fido: {species: 'dog'}, cujo: {species: 'wolf'}} tabularPersister.destroy(); `Store` `Values` are saved into a separate table, normally called `tinybase_values`. See the `DpcTabularValues` documentation for examples of how to use that. ### Working With An Existing Database In theory, it's possible to bind TinyBase to a SQLite database that already exists. You will obviously want to list the tables of interest in the `load` section of the configuration. Do be aware that TinyBase is an in-memory data structure, and so you will not want to do this if your database tables are particularly large and complex. Also be very careful when setting the `save` configuration, since it will mean that TinyBase writes its version of the data back to the database (optionally removing empty columns). If there is data that does not survive the round trip (because of schema constraints or data typing), it will be lost. The `Persister` maps a column in the database table to provide and store the `Store` `Table`'s `Row` `Ids`. By default, this is a database column called `_id`, but you can set it to be something else, per table. It is required that this column is a primary or unique key in the database so that the `Persister` knows how to update existing records. So for example, imagine your existing database table looks like this, with the first column of each table being a primary key: > SELECT * FROM the_pets_table; +--------+---------+-------+ | pet_id | species | color | +--------+---------+-------+ | fido | dog | brown | | felix | cat | black | +--------+---------+-------+ > SELECT * FROM the_species_table; +------------+-------+ | species_id | price | +------------+-------+ | dog | 5 | | cat | 4 | +------------+-------+ For this, you may consider the following configuration for your `Persister`: const databasePersisterConfig: DatabasePersisterConfig = { mode: 'tabular', tables: { load: { the_pets_table: {tableId: 'pets', rowIdColumnName: 'pet_id'}, the_species_table: {tableId: 'species', rowIdColumnName: 'species_id'}, }, save: { pets: {tableId: 'the_pets_table', rowIdColumnName: 'pet_id'}, species: {tableId: 'the_species_table', rowIdColumnName: 'species_id'}, }, }, }; This will load into a `Store` (and save back again) with `Tables` that look like this: { "pets": { "fido": {"species": "dog", "color": "brown"}, "felix": {"species": "cat", "color": "black"} }, "species": { "dog": {"price": 5}, "cat": {"price": 4} } } ### Summary With care, you can load and save `Store` data from and to a SQLite database in a variety of ways and via different modules. This is new in v4.0, so feedback on the functionality is welcomed! Next we move on to look at how to fully synchronize TinyBase Stores using more complex CRDT frameworks, such as Yjs and Automerge, in the Third-Party CRDT Persistence guide. --- ## Page: https://tinybase.org/guides/persistence/third-party-crdt-persistence/ Some persister modules let you save and load `Store` data to underlying storage types that can provide synchronization, local-first reconciliation, and CRDTs. | `Persister` | Storage | | --- | --- | | `YjsPersister` | Yjs CRDTs, via yjs | | `AutomergePersister` | Automerge CRDTs, via automerge-repo | The APIs are exactly the same as for other persisters, but there is some additional infrastructure behind the scenes to ensure that the updates are as incremental and atomic as possible, improving the reconciliation capabilities. For TinyBase v5's first-party CRDT and synchronization techniques, see instead the Synchronization guides. ### A Synchronization Walkthrough For fully-fledged synchronization with a third-party framework, you will want to use both auto-load and auto-save between the `Store` and the underlying synchronization framework. In this case, we have two Yjs documents that are each bound to their respective `Store` objects by a `Persister` with auto-load and auto-save: import {createStore} from 'tinybase'; import {createYjsPersister} from 'tinybase/persisters/persister-yjs'; import {Doc} from 'yjs'; const doc1 = new Doc(); const store1 = createStore(); const persister1 = createYjsPersister(store1, doc1); await persister1.startAutoLoad(); await persister1.startAutoSave(); const doc2 = new Doc(); const store2 = createStore(); const persister2 = createYjsPersister(store2, doc2); await persister2.startAutoLoad(); await persister2.startAutoSave(); Typically, real-world synchronization for Yjs happens between two systems via a Yjs connection provider with state machine vectors and so on. For the purposes of illustration here, we synthesize that with a simple `syncDocs` function that copies full state between the documents: import {applyUpdate, encodeStateAsUpdate} from 'yjs'; const syncDocs = async () => { // ... applyUpdate(doc1, encodeStateAsUpdate(doc2)); applyUpdate(doc2, encodeStateAsUpdate(doc1)); }; It is good practice to establish a synchronization baseline between the Stores: await syncDocs(); Now imagine that we make a change to one of the Stores, and synchronize again: store1.setTables({pets: {fido: {species: 'dog'}}}); await syncDocs(); If all goes well, we should see those changes in the other `Store`! console.log(store2.getTables()); // -> {pets: {fido: {species: 'dog'}}} And of course we can make changes propagate back again: store2.setValue('open', true); await syncDocs(); console.log(store1.getValues()); // -> {open: true} ### Conflicts TinyBase deliberately avoids having an opinion on simultaneous or conflicting updates, deferring that to the underlying CRDT framework. However, most changes are dealt with as atomically as possible - in other words at a `Cell` or `Value` level - in order to limit data loss by default. Here we update two adjacent Cells in the same `Row`: store1.setCell('pets', 'fido', 'color', 'brown'); store2.setCell('pets', 'fido', 'legs', 4); // ... await syncDocs(); console.log(store1.getTables()); // -> {pets: {fido: {species: 'dog', color: 'brown', 'legs': 4}}} console.log(store2.getTables()); // -> {pets: {fido: {species: 'dog', color: 'brown', 'legs': 4}}} If there are two conflicting changes to the same `Cell`, Yjs typically runs a tie-break based on the client ID of the document, where the higher one wins. // Here we force the clientID so that the reconciliation is deterministic. doc1.clientID = 1; doc2.clientID = 2; store1.setCell('pets', 'fido', 'price', 4); store2.setCell('pets', 'fido', 'price', 5); // ... await syncDocs(); console.log(store1.getCell('pets', 'fido', 'price')); // -> 5 console.log(store2.getCell('pets', 'fido', 'price')); // -> 5 However, in production use, you are highly discouraged from setting the clientID yourself, and hence the reconciliation will not be deterministic without manual resolution. --- ## Page: https://tinybase.org/guides/persistence/custom-persistence/ * TinyBase * Guides * Persistence * Custom Persistence When you want to load and save `Store` data in unusual or custom ways, you can used the `createCustomPersister` function to do so in any way you wish. As well as providing a reference to the `Store` to persist, you must provide functions that handle how to fetch, write, and listen to, the persistence layer. ### Functions To Implement To build a custom `Persister`, you should provide four functions: * `getPersisted`, an asynchronous function which will fetch content from the persistence layer (or `null` or `undefined` if not present). * `setPersisted`, an asynchronous function which will send content to the persistence layer. * `addPersisterListener`, a function that will register a `listener` listener on underlying changes to the persistence layer. You can return a listening handle that will be provided again when `delPersisterListener` is called. * `delPersisterListener`, a function that will unregister the listener from the underlying changes to the persistence layer. It receives whatever was returned from your `addPersisterListener` implementation. @returns A reference to the new `Persister` object. Note that the first two functions are asynchronous and _must_ return promises. The latter two are synchronous and should return `void` (i.e. should not return a value at all). This API changed in v4.0. Any custom persisters created on previous versions should be upgraded. 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`. This example creates a custom `Persister` object that persists the `Store` to a local string called `storeJson` and which would automatically load by polling for changes every second: import {createStore} from 'tinybase'; import {createCustomPersister} from 'tinybase/persisters'; const store = createStore().setTables({pets: {fido: {species: 'dog'}}}); let storeJson; let interval; const persister = createCustomPersister( store, async () => { try { return JSON.parse(storeJson); } catch {} }, async (getContent) => (storeJson = JSON.stringify(getContent())), (listener) => (interval = setInterval(listener, 1000)), () => clearInterval(interval), ); await persister.save(); console.log(storeJson); // -> '[{"pets":{"fido":{"species":"dog"}}},{}]' storeJson = '[{"pets":{"fido":{"species":"dog","color":"brown"}}},{}]'; await persister.load(); console.log(store.getTables()); // -> {pets: {fido: {species: 'dog', color: 'brown'}}} persister.destroy(); Note that the other creation functions (such as the `createSessionPersister` function and `createFilePersister` function, for example) all use this function under the covers. See those implementations for ideas on how to implement your own `Persister` types. --- ## Page: https://tinybase.org/guides/synchronization/using-a-mergeablestore/ * TinyBase * Guides * Synchronization * Using A MergeableStore The basic building block of TinyBase's synchronization system is the `MergeableStore` interface. ### The Anatomy Of A `MergeableStore` The `MergeableStore` interface is a sub-type of the regular `Store` - and it shares its underlying implementation. This means that if you want to add synchronization to your app, all of your existing calls to the `Store` methods will be unchanged - you just need to use the `createMergeableStore` function to instantiate it, instead of the classic `createStore` function. import {createMergeableStore} from 'tinybase'; const store1 = createMergeableStore('store1'); store1.setCell('pets', 'fido', 'species', 'dog'); console.log(store1.getContent()); // -> [{pets: {fido: {species: 'dog'}}}, {}] The difference, though, is that a `MergeableStore` records additional metadata as the data is changed so that potential conflicts between it and another instance can be reconciled. This metadata is intended to be opaque, but you can see it if you call the `getMergeableContent` method: console.log(store1.getMergeableContent()); // -> [ [ { pets: [ { fido: [ {species: ['dog', 'Nn1JUF-----FnHIC', 290599168]}, '', 2682656941, ], }, '', 2102515304, ], }, '', 3506229770, ], [{}, '', 0], ]; Without going into the detail of this, the main point to understand is that each update gets a timestamp, based on a hybrid logical clock (HLC), and a hash. As a result, TinyBase is able to understand which parts of the data have changed, and which changes are the most recent. The resulting 'last write wins' (LWW) approach allows the `MergeableStore` to act as a Conflict-Free Replicated Data Type (CRDT). (Notice we provided an explicit `uniqueId` when we initialized the `MergeableStore`: this is not normally required, but here it just ensures the hashes in the example are deterministic). We can of course, create a second `MergeableStore` with different data: const store2 = createMergeableStore(); store2.setCell('pets', 'felix', 'species', 'cat'); And now merge them together with the convenient `merge` method: store1.merge(store2); console.log(store1.getContent()); // -> [{pets: {felix: {species: 'cat'}, fido: {species: 'dog'}}}, {}] console.log(store2.getContent()); // -> [{pets: {felix: {species: 'cat'}, fido: {species: 'dog'}}}, {}] Magic! This all said, it's very unlikely you will need to use the numerous extra methods available on a `MergeableStore` (compared to a `Store`) since most of them exist to support synchronization behind the scenes. In general, you'll just use a `MergeableStore` in the same was as you would have used a `Store`, and instead rely on the more approachable `Synchronizer` API for synchronization. We'll discuss this next in the Using A Synchronizer guide. ## Persisting A `MergeableStore` Once important thing that you need to be aware of is that a `MergeableStore` cannot currently be persisted by every type of `Persister` available to a regular `Store`. This is partly because some are already designed to work with alternative third-party CRDT systems (like the `YjsPersister` and `AutomergePersister`), and partly because this extra metadata cannot be easily stored in a plain SQLite database. The following `Persister` types _can_ be used to persist a `MergeableStore`: | `Persister` | Storage | | --- | --- | | `SessionPersister` | Browser session storage | | `LocalPersister` | Browser local storage | | `FilePersister` | Local file (where possible) | The following database-oriented `Persister` types can be used to persist a `MergeableStore`, but _only_ in the 'JSON-serialization' mode: | `Persister` | Storage | | --- | --- | | `Sqlite3Persister` | SQLite in Node, via sqlite3 | | `SqliteWasmPersister` | SQLite in a browser, via sqlite-wasm | | `ExpoSqlitePersister` | SQLite in React Native, via expo-sqlite | | `PostgresPersister` | PostgreSQL, via postgres | | `PglitePersister` | PostgreSQL, via PGlite | The following database-oriented `Persister` types _cannot_ currently be used to persist a `MergeableStore`: | `Persister` | Storage | | --- | --- | | `IndexedDbPersister` | Browser IndexedDB | | `RemotePersister` | Remote server | | `CrSqliteWasmPersister` | SQLite CRDTs, via cr-sqlite-wasm | | `ElectricSqlPersister` | Electric SQL, via electric-sql | | `LibSqlPersister` | LibSQL for Turso, via libsql-client | | `PowerSyncPersister` | PowerSync, via powersync-sdk | | `YjsPersister` | Yjs CRDTs, via yjs | | `AutomergePersister` | Automerge CRDTs, via automerge-repo | | `PartyKitPersister` | PartyKit, via the `persister-partykit-server` module | Next, let's see how to synchronize `MergeableStore` objects together with the `synchronizers` module. Please continue on to the Using A Synchronizer guide. --- ## Page: https://tinybase.org/guides/synchronization/using-a-synchronizer/ The synchronizer module framework lets you synchronize `MergeableStore` data between different devices, systems, or subsystems. It contains the `Synchronizer` interface, describing objects which can be used to synchronize a `MergeableStore`. Under the covers, a `Synchronizer` is actually a very specialized type of `Persister` that _only_ supports `MergeableStore` objects, and which has a `startSync` method and a `stopSync` method. ### Types Of `Synchronizer` In TinyBase v5.0, there are three types of `Synchronizer`: * The `WsSynchronizer` uses WebSockets to communicate between different systems. * The `BroadcastChannelSynchronizer` uses the browser's BroadcastChannel API to communicate between different tabs and workers. * The `LocalSynchronizer` demonstrates synchronization in memory on a single local system. Of course it is also possible to create custom `Synchronizer` objects if you have a transmission medium that allows the synchronization messages to be sent reliably between clients. ### Synchronizing With WebSockets A common pattern for synchronizing over the web is to use WebSockets. This allows multiple clients to pass lightweight messages to each other, facilitating efficient synchronization. One thing to understand is that this set up will typically require a server. This can be a relatively 'thin server' - it does not need to store data of its own - but is needed to keep a list of clients that are being synchronized together, and route and broadcast messages between the clients. TinyBase includes some implementations of WebSocket servers: * `WsServer`, created with the `createWsServer` function in the `synchronizer-ws-server` module. This includes the option to persist data in the server. * `WsServerSimple`, created with the `createWsServerSimple` function in the `synchronizer-ws-server-simple` module. This does not have the complications of listeners, persistence, or statistics, and is suitable to be used as a reference implementation * `WsServerDurableObject`, implemented as Cloudflare Durable Object, created by extending the `WsServerDurableObject` class, and routed with the convenient `getWsServerDurableObjectFetch` function. Here we'll use the regular `WsServer`. You simply need to create it, instantiated with a configured WebSocketServer object from the `ws` package: // On a server machine: import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server'; import {WebSocketServer} from 'ws'; const server = createWsServer(new WebSocketServer({port: 8048})); This sets up a `WsServer` object, listening on port 8048. Each client then needs to create a `WsSynchronizer` object, instantiated with the `MergeableStore` being synchronized, and a WebSocket configured to connect to the aforementioned server: // On the first client machine: import {createMergeableStore} from 'tinybase'; import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client'; import {WebSocket} from 'ws'; const clientStore1 = createMergeableStore(); const clientSynchronizer1 = await createWsSynchronizer( clientStore1, new WebSocket('ws://localhost:8048'), ); This `WsSynchronizer` can then be started, and data manipulated as normal: await clientSynchronizer1.startSync(); clientStore1.setCell('pets', 'fido', 'species', 'dog'); // ... Meanwhile, on another client, an empty `MergeableStore` and another `WsSynchronizer` can be created and started, connecting to the same server. // On the second client machine: const clientStore2 = createMergeableStore(); const clientSynchronizer2 = await createWsSynchronizer( clientStore2, new WebSocket('ws://localhost:8048'), ); await clientSynchronizer2.startSync(); Once the synchronization is started, the server will broker the messages being passed back and forward between the two clients, and the data will be synchronized. The empty second `MergeableStore` will be populated with the data from the first: // ... console.log(clientStore2.getTables()); // -> {pets: {fido: {species: 'dog'}}} And of course the synchronization is bi-directional: clientStore2.setCell('pets', 'felix', 'species', 'cat'); console.log(clientStore2.getTables()); // -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}} // ... console.log(clientStore1.getTables()); // -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}} When done, it's important to destroy a `WsSynchronizer` to close and tidy up the client WebSockets: clientSynchronizer1.destroy(); clientSynchronizer2.destroy(); And, if shut down, the `WsServer` should also be explicitly destroyed to close its listeners: server.destroy(); #### Persisting Data On The Server New in TinyBase v5.1, the `createWsServer` function lets you specify a way to persist data to the server. This makes it possible for all clients to disconnect from a path, but, when they reconnect, for the data to still be present for them to sync with. This is done by passing in a second argument to the function that creates a `Persister` instance (for which also need to create or provide a `MergeableStore`) for a given path: import {createFilePersister} from 'tinybase/persisters/persister-file'; const persistingServer = createWsServer( new WebSocketServer({port: 8050}), (pathId) => createFilePersister( createMergeableStore(), pathId.replace(/[^a-zA-Z0-9]/g, '-') + '.json', ), ); persistingServer.destroy(); This is a very crude example, but demonstrates a server that will create a file, based on any path that clients connect to, and persist data to it. In production, you will certainly want to sanitize the file name! And more likely you will want to explore using a database-oriented `Persister` instead of simply using raw files. See the `createWsServer` function documentation for more details. Also note that there is a `synchronizer-ws-server-simple` module that contains a simple server implementation called `WsServerSimple`. Without the complications of listeners, persistence, or statistics, this is more suitable to be used as a reference implementation for other server environments. ### Synchronizing Over The Browser BroadcastChannel There may be situations where you need to synchronize data between different parts of a browser. For example, you might have a transient in-memory `MergeableStore` driving your UI, but then another instance in a Service Worker that can be persisted to (say) IndexedDB or another medium. To facilitate keeping these in sync, the `BroadcastChannelSynchronizer` lets you synchronize over the browser's BroadcastChannel API, common to each browser sub-system. You simply need to provide a distinguishing channel name that can be used to identify what the two parts should be using to send and receive messages. For example, in the UI part of your app: import {createBroadcastChannelSynchronizer} from 'tinybase/synchronizers/synchronizer-broadcast-channel'; const frontStore = createMergeableStore(); const frontSynchronizer = createBroadcastChannelSynchronizer( frontStore, 'syncChannel', ); await frontSynchronizer.startSync(); And then in the service worker: const backStore = createMergeableStore(); const backSynchronizer = createBroadcastChannelSynchronizer( backStore, 'syncChannel', ); await backSynchronizer.startSync(); Since they both share the `syncChannel` channel name, the data of the two is now synchronized: frontStore.setCell('pets', 'fido', 'species', 'dog'); // ... console.log(backStore.getTables()); // -> {pets: {fido: {species: 'dog'}}} And so on! When finished, these synchronizers should also be explicitly destroyed to ensure the channel listeners are cleaned up: frontSynchronizer.destroy(); backSynchronizer.destroy(); ### Wrapping Up The `Synchronizer` interface provides an easy way to keep multiple TinyBase MergeableStores in sync. The WebSocket and BroadcastChannel options above allow for numerous interesting and powerful app architectures - and they are not sufficient, consider exploring the `createCustomSynchronizer` function to develop your own! --- ## Page: https://tinybase.org/guides/integrations/cloudflare-durable-objects/ Durable Objects are a new type of serverless compute platform from Cloudflare, and provide a way to run stateful applications in a serverless environment, without needing to manage infrastructure. ### What Are Durable Objects? Each Durable Object can function as a WebSocket server, so it can co-ordinate messages between multiple clients. But importantly, it also has private, transactional and strongly consistent storage attached. Combined with a TinyBase `Store` on the client, and using the built-in synchronization and persistence functionality, this gives you an full-stack way to build complex, real-time, multi-device (and even collaborative) apps. As this guide hopefully shows, this can be done with minimal effort! ### Getting Started with Vite The quickest way to get started with TinyBase and Cloudflare Durable Objects is to use our Vite template. This includes both a client implementation (that by default connects to a demo server we provide) and a server implementation that you can instead use for your own Cloudflare installation. #### Install The Client 1. Make a copy of the template into a new directory: npx tiged tinyplex/vite-tinybase-ts-react-sync-durable-object my-tinybase-app 2. Go into the client directory: cd my-tinybase-app/client 3. Install the dependencies: npm install 4. Run the application: npm run dev 5. Go the URL shown and enjoy!  #### Run Your Own Server This template uses a lightweight socket server on `vite.tinybase.cloud` to synchronize data between clients. This is fine for a demo but not intended as a production server for your apps! If you wish to run your own instance, see the `server` directory and start from there. The `vite.tinybase.cloud` server is hosted on Cloudflare (of course), so you should adapt the `wrangler.toml` configuration in the server directory. Update it to match your account, domains, and required configuration. In the `index.ts` file, you can configure whether data will be stored in the Durable Object or just synchronized between clients. You will also have to have your client communicate with your new server by configuring the `SERVER` constant at the top of the client's `App.tsx` file. ### How It All Works #### Client: Persistence and Synchronization On the client, we create a simple TinyBase `MergeableStore` to encapsulate the data we need for the app. The following are simplified extracts of the code in the `App.tsx` file of the Vite template: export const App = () => { const store = useCreateMergeableStore(createMergeableStore); // ... (Because the template is using React, we use the `useCreateMergeableStore` hook so it's not re-created on every render.) We also create a local `Persister` so that if the client goes offline, and the browser window is refreshed, changes will have been cached locally in session storage, with a key 'foo': // ... useCreatePersister( store, (store) => createLocalPersister(store, 'foo'), [], async (persister) => { await persister.startAutoLoad(/* any initial contents */); await persister.startAutoSave(); }, ); // ... More interestingly, we also set up a `Synchronizer` that connects to the Cloudflare installation, on a path that also happens to be called 'foo': // ... useCreateSynchronizer(store, async (store) => { const synchronizer = await createWsSynchronizer( store, new WebSocket('wss://example.com/foo'), ); await synchronizer.startSync(); return synchronizer; }); Those simple lines are enough to have the `Store` attempt to synchronize itself with the common Durable Object called `/foo` on the server. #### Server: Worker and Durable Object On the server, Cloudflare needs us to configure a worker and Durable Object. It will help if you are familiar with these concepts already - and if not, start with the documentation here. Following the instructions above, you'll have a `wrangler.toml` file containing the configuration for your worker and the Durable Object. In there you'll need to bind a namespace of Durable Objects to a class, something like: [[durable_objects.bindings]] name = "TinyBaseDurableObjects" class_name = "TinyBaseDurableObject" [[migrations]] tag = "v1" new_classes = ["TinyBaseDurableObject"] (Again, take a look at the contents of the Vite template for the full configuration file.) In the main worker file, probably called `index.js` or `index.ts`, you'll need to configure the worker as the default export from the file. TinyBase provides a convenience `getWsServerDurableObjectFetch` function that will create a `fetch` method that routes WebSocket requests based on the path of the URL: export default { fetch: getWsServerDurableObjectFetch('TinyBaseDurableObjects'), }; In here, the argument is the namespace containing your bound Durable Objects. Now we need to create the Durable Object itself. This can be as simple as simply extending TinyBase's `WsServerDurableObject` class: export class TinyBaseDurableObject extends WsServerDurableObject { // ... } This sets up synchronization between any clients that connect to this common `/foo` path so that their `Store` data stays in sync. But if all your clients disconnect and flush their locally-stored data, it is technically possible to lose it all! So it's also a good idea to have the Durable Object store a synchronized copy too. We do this by overriding the `createPersister` method: // ... createPersister() { return createDurableObjectStoragePersister( createMergeableStore(), this.ctx.storage, ); } // ... In this method, all we need to do is create a `MergeableStore` (that will reside in the Durable Object memory whenever it is running and not hibernated), and indicate how it will be persisted. Almost always you will want to return a `DurableObjectStoragePersister` object, which is dedicated to storing a TinyBase `Store` in Durable Object storage - as indicated by the `this.ctx.storage` argument. With this in place you now have the full set up! The clients are storing a local copy of the TinyBase data so they can go offline and reload without loss of data; and the server is also storing a copy of the data in Durable Object storage. When online, the clients will connect to the worker, which routes the request to the Durable Object indicated by the URL path, and synchronization between them all keeps them each up-to-date! ### Final Notes Durable Objects have limitations on the data that can be stored in each key of their key-value structure. The `DurableObjectStoragePersister` uses one key per TinyBase `Value`, one key per `Cell`, one key per `Row`, and one key per `Table`. Mostly this is CRDT metadata, but the main caution is to ensure that each individual TinyBase `Cell` and `Value` data does not exceed the (128 KiB) limit. The `WsServerDurableObject` is an overridden implementation of the DurableObject class, so you can have access to its members as well as the TinyBase-specific methods. If you are using the storage for other data, you may want to configure a `prefix` parameter to ensure you don't accidentally collide with TinyBase data. Also, always remember to call the `super` implementations of the methods that TinyBase uses (the constructor, `fetch`, `webSocketMessage`, and `webSocketClose`) if you further override them. Finally, the `WsServerDurableObject` uses hibernation, which is a Cloudflare feature to minimize the amount of memory used by your Durable Object. After a small amount of time with no WebSocket activity, it will be 'hibernated' even though the WebSockets stay connected. This means that the in-memory TinyBase `Store` will be removed (which is a good thing for your Cloudflare usage!) and then re-created with the `Persister` when new activity arrives. This lifecycle should be transparent, but should be understood if you want to debug certain Durable Object behaviors. --- ## Page: https://tinybase.org/guides/using-metrics/an-intro-to-metrics/ This guide describes how the `metrics` module gives you the ability to create and track metrics based on the data in `Store` objects. The main entry point to using the `metrics` module is the `createMetrics` function, which returns a new `Metrics` object. That object in turn has methods that let you create new `Metric` definitions, access the values of those metrics directly, and register listeners for when they change. ### The Basics Here's a simple example to show a `Metrics` object in action. The `species` `Table` has three `Row` objects, each with a numeric `price` `Cell`. We create a `Metric` definition called `highestPrice` which is the maximum value of that `Cell` across the whole `Table`: import {createMetrics, createStore} from 'tinybase'; const store = createStore().setTable('species', { dog: {price: 5}, cat: {price: 4}, worm: {price: 1}, }); const metrics = createMetrics(store); metrics.setMetricDefinition( 'highestPrice', // metricId 'species', // tableId to aggregate 'max', // aggregation 'price', // cellId to aggregate ); console.log(metrics.getMetric('highestPrice')); // -> 5 The out-of-the-box aggregations you can use in the third parameter are `sum`, `avg`, `min`, and `max`, each of which should be self-explanatory. ### `Metric` Reactivity Things get interesting when the underlying data changes. The `Metrics` object takes care of tracking changes that will affect the `Metric`. A similar paradigm to that used on the `Store` is used to let you add a listener to the `Metrics` object. The listener fires when there's a new highest price: const listenerId = metrics.addMetricListener('highestPrice', () => { console.log(metrics.getMetric('highestPrice')); }); store.setCell('species', 'horse', 'price', 20); // -> 20 You can set multiple `Metric` definitions on each `Metrics` object. However, a given `Store` can only have one `Metrics` object associated with it. If you call this function twice on the same `Store`, your second call will return a reference to the `Metrics` object created by the first. Let's find out how to include metrics in a user interface in the Building A UI With Metrics guide. --- ## Page: https://tinybase.org/guides/using-metrics/building-a-ui-with-metrics/ * TinyBase * Guides * Using Metrics * Building A UI With Metrics This guide covers how the `ui-react` module supports the `Metrics` object. As with the React-based bindings to a `Store` object, the `ui-react` module provides both hooks and components to connect your metrics to your interface. ### `Metrics` Hooks The `useMetric` hook is very simple. It gets the current value of a `Metric`, and registers a listener so that any changes to that result will cause a re-render: import React from 'react'; import {createRoot} from 'react-dom/client'; import {createMetrics, createStore} from 'tinybase'; import {useMetric} from 'tinybase/ui-react'; const store = createStore().setTable('species', { dog: {price: 5}, cat: {price: 4}, worm: {price: 1}, }); const metrics = createMetrics(store); metrics.setMetricDefinition('highestPrice', 'species', 'max', 'price'); const App = () => <span>{useMetric('highestPrice', metrics)}</span>; const app = document.createElement('div'); const root = createRoot(app); root.render(<App />); console.log(app.innerHTML); // -> '<span>5</span>' store.setCell('species', 'horse', 'price', 20); console.log(app.innerHTML); // -> '<span>20</span>' The `useCreateMetrics` hook is used to create a `Metrics` object within a React application with convenient memoization: import {useCreateMetrics, useCreateStore} from 'tinybase/ui-react'; const App2 = () => { const store = useCreateStore(() => createStore().setTable('species', { dog: {price: 5}, cat: {price: 4}, worm: {price: 1}, }), ); const metrics = useCreateMetrics(store, (store) => createMetrics(store).setMetricDefinition( 'highestPrice', 'species', 'max', 'price', ), ); return <span>{metrics?.getMetric('highestPrice')}</span>; }; root.render(<App2 />); console.log(app.innerHTML); // -> '<span>5</span>' ### `Metrics` View The `MetricView` component renders the current value of a `Metric`, and registers a listener so that any changes to that result will cause a re-render. import {MetricView} from 'tinybase/ui-react'; const App3 = () => ( <div> <MetricView metricId="highestPrice" metrics={metrics} /> </div> ); root.render(<App3 />); console.log(app.innerHTML); // -> '<div>20</div>' ### `Metrics` Context In the same way that a `Store` can be passed into a `Provider` component context and used throughout the app, a `Metrics` object can also be provided to be used by default: import {Provider} from 'tinybase/ui-react'; const App4 = () => { const store = useCreateStore(() => createStore().setTable('species', { dog: {price: 5}, cat: {price: 4}, worm: {price: 1}, }), ); const metrics = useCreateMetrics(store, (store) => createMetrics(store).setMetricDefinition( 'highestPrice', 'species', 'max', 'price', ), ); return ( <Provider metrics={metrics}> <Pane /> </Provider> ); }; const Pane = () => ( <span> <MetricView metricId="highestPrice" />,{useMetric('highestPrice')} </span> ); root.render(<App4 />); console.log(app.innerHTML); // -> '<span>5,5</span>' The `metricsById` prop can be used in the same way that the `storesById` prop is, to let you reference multiple `Metrics` objects by `Id`. ### Summary The support for `Metrics` objects in the `ui-react` module is very similar to that for the `Store` object, making it easy to attach `Metric` results to your user interface. We finish off this section about the `metrics` module with the Advanced Metric Definition guide. --- ## Page: https://tinybase.org/guides/using-metrics/advanced-metric-definition/ * TinyBase * Guides * Using Metrics * Advanced Metric Definition This guide describes how the `metrics` module let you create more complex types of metrics based on the data in `Store` objects. ### Advanced Aggregations The four standard aggregation types you can use when defining a `Metric` are `sum`, `avg`, `min`, and `max`, but often you will want to create more interesting aggregations of the data in your `Table`. Instead of a string as the third parameter of the `setMetricDefinition` method, you can provide an `Aggregate` parameter. This is simply a function that takes an array of numbers and returns the aggregation. So for example, you could create a `Metric` which is the hypotenuse of the numeric `distance` `Cell` values in every `Row`: import {createMetrics, createStore} from 'tinybase'; const store = createStore().setTable('dimensions', { x: {distance: 1}, y: {distance: 2}, z: {distance: 2}, }); const metrics = createMetrics(store); metrics.setMetricDefinition( 'hypotenuse', // metricId 'dimensions', // tableId to aggregate (distances) => Math.hypot(...distances), // custom aggregation 'distance', // cellId to aggregate ); console.log(metrics.getMetric('hypotenuse')); // -> 3 Such a `Metric` will have a performance complexity linear with the size of the `Table`, any time any value changes. But some aggregations have shortcuts: if a contributing value is updated, you can sometimes calculate the new value for the metric without scanning every value again. For example, if your aggregation is a sum, and an additional `Row` is added, its value can simply be added to the previous total. There are three types of shortcuts you can add if the aggregation can benefit from them, and they can be provided as the final three parameters of the `setMetricDefinition` method. These describe how to change the overall `Metric` when a number is added, removed, or replaced. Our hypotenuse example can benefit. If a new value is added to the list of numbers to be aggregated, square the current result, add the square of the new number, and square root the total. This is then constant cost, regardless of the number of `Row` objects being aggregated: const sqr = (num) => num * num; const sqrt = Math.sqrt; metrics.setMetricDefinition( 'fasterHypotenuse', // metricId 'dimensions', // tableId to aggregate (distances) => Math.hypot(...distances), // custom aggregation 'distance', // cellId to aggregate (metric, add) => sqr(sqr(metric) + sqr(add)), // add (metric, rem) => sqr(sqr(metric) - sqr(rem)), // remove (metric, add, rem) => sqr(sqr(metric) + sqr(add) - sqr(rem)), // replace ); store.setCell('dimensions', 'x', 'distance', 3); store.setCell('dimensions', 'z', 'distance', 6); console.log(metrics.getMetric('hypotenuse')); // -> 7 ### Getting Custom `Values` From Rows By default, our `Metric` definitions have named a `Cell` in the `Row` which contains a numeric value - like the `distance` `Cell` in the example above. Sometimes you may wish to derive a number for each `Row` that is not in a single `Cell`, and in this case you can replace the fourth parameter with a function which can process the `Row` in any way you wish. In this example, we average the density of a set of cuboids, which means that each `Row`'s contributory number needs to be the mass divided by the volume: store.setTable('cuboids', { 0: {mass: 10, volume: 5}, 1: {mass: 12, volume: 3}, 2: {mass: 24, volume: 4}, }); metrics.setMetricDefinition( 'averageDensity', // metricId 'cuboids', // tableId to aggregate 'avg', // aggregation (getCell) => getCell('mass') / getCell('volume'), // => number to aggregate ); console.log(metrics.getMetric('averageDensity')); // -> 4 Of course it is possible to combine both advanced aggregations and getting custom values from each `Row`: metrics.setMetricDefinition( 'countOfDenseCuboids', 'cuboids', (densities) => densities.filter((density) => density > 5).length, (getCell) => getCell('mass') / getCell('volume'), ); console.log(metrics.getMetric('countOfDenseCuboids')); // -> 1 And with that, we have covered most of the basics of using the `metrics` module. Let's move on to a very similar module for indexing data, as covered in the Using Indexes guide. --- ## Page: https://tinybase.org/guides/using-indexes/an-intro-to-indexes/ * TinyBase * Guides * Using Indexes * An Intro To Indexes This guide describes how the `indexes` module gives you the ability to create and track indexes based on the data in `Store` objects, and which allow you to look up and display filtered data quickly. The main entry point to using the `indexes` module is the `createIndexes` function, which returns a new `Indexes` object. That object in turn has methods that let you create new `Index` definitions, access the content of those `Indexes` directly, and register listeners for when they change. ### The Basics An `Index` comprises a map of `Slice` objects, keyed by `Id`. The `Ids` in a `Slice` represent `Row` objects from a `Table` that all have a derived string value in common, as described by the `setIndexDefinition` method. Those values are used as the key for each `Slice` in the overall `Index` object. This might be simpler to understand with an example: if a `Table` contains pets, each with a species, then an `Index` could be configured to contain the `Ids` of each `Row`, grouped into a `Slice` for each distinct species. In other words, this `Table`: { // Store pets: { fido: {species: 'dog'}, felix: {species: 'cat'}, cujo: {species: 'dog'}, }, } would conceptually become this `Index`: { // Indexes bySpecies: { dog: ['fido', 'cujo'], cat: ['felix'], }, } This is for illustrative purposes: note that this resulting `Index` structure is never an object literal like this: you would instead use the `getSliceIds` method and the `getSliceRowIds` method to iterate through the it. Here's a simple example to show such an `Index` in action. The `pets` `Table` has three `Row` objects, each with a string `species` `Cell`. We create an `Index` definition called `bySpecies` which groups them: import {createIndexes, createStore} from 'tinybase'; const store = createStore().setTable('pets', { fido: {species: 'dog'}, felix: {species: 'cat'}, cujo: {species: 'dog'}, }); const indexes = createIndexes(store); indexes.setIndexDefinition( 'bySpecies', // indexId 'pets', // tableId to index 'species', // cellId to index on ); console.log(indexes.getSliceIds('bySpecies')); // -> ['dog', 'cat'] console.log(indexes.getSliceRowIds('bySpecies', 'dog')); // -> ['fido', 'cujo'] ### `Index` Reactivity As with the `Metrics` object, magic happens when the underlying data changes. The `Indexes` object efficiently takes care of tracking changes that will affect the `Index` or the `Slice` arrays within it. A similar paradigm to that used on the `Store` is used to let you add a listener to the `Indexes` object. The listener fires when there's a change to the `Slice` `Ids` or a `Slice`'s content: indexes.addSliceIdsListener('bySpecies', () => { console.log(indexes.getSliceIds('bySpecies')); }); store.setRow('pets', 'lowly', {species: 'worm'}); // -> ['dog', 'cat', 'worm'] indexes.addSliceRowIdsListener('bySpecies', 'worm', () => { console.log(indexes.getSliceRowIds('bySpecies', 'worm')); }); store.setRow('pets', 'smaug', {species: 'worm'}); // -> ['lowly', 'smaug'] You can set multiple `Index` definitions on each `Indexes` object. However, a given `Store` can only have one `Indexes` object associated with it. So, as with the `Metrics` object, if you call this function twice on the same `Store`, your second call will return a reference to the `Indexes` object created by the first. Let's next find out how to include `Indexes` in a user interface in the Building A UI With Indexes guide. --- ## Page: https://tinybase.org/guides/using-indexes/building-a-ui-with-indexes/ This guide covers how the `ui-react` module supports the `Indexes` object. As with the React-based bindings to a `Store` object, the `ui-react` module provides both hooks and components to connect your indexes to your interface. ### `Indexes` Hooks The `useSliceIds` hook is as simple as it sounds. It gets the current set of `Slice` `Ids` in an `Index`, and registers a listener so that any changes to that result will cause a re-render: import React from 'react'; import {createRoot} from 'react-dom/client'; import {createIndexes, createStore} from 'tinybase'; import {useSliceIds} from 'tinybase/ui-react'; const store = createStore().setTable('pets', { fido: {species: 'dog'}, felix: {species: 'cat'}, cujo: {species: 'dog'}, }); const indexes = createIndexes(store); indexes.setIndexDefinition( 'bySpecies', // indexId 'pets', // tableId to index 'species', // cellId to index on ); const App = () => ( <span>{JSON.stringify(useSliceIds('bySpecies', indexes))}</span> ); const app = document.createElement('div'); const root = createRoot(app); root.render(<App />); console.log(app.innerHTML); // -> '<span>["dog","cat"]</span>' store.setRow('pets', 'lowly', {species: 'worm'}); console.log(app.innerHTML); // -> '<span>["dog","cat","worm"]</span>' The `useCreateIndexes` hook is used to create an `Indexes` object within a React application with convenient memoization: import {useCreateIndexes, useCreateStore} from 'tinybase/ui-react'; const App2 = () => { const store = useCreateStore(() => createStore().setTable('pets', { fido: {species: 'dog'}, felix: {species: 'cat'}, cujo: {species: 'dog'}, }), ); const indexes = useCreateIndexes(store, (store) => createIndexes(store).setIndexDefinition('bySpecies', 'pets', 'species'), ); return <span>{JSON.stringify(useSliceIds('bySpecies', indexes))}</span>; }; root.render(<App2 />); console.log(app.innerHTML); // -> '<span>["dog","cat"]</span>' ### `Index` And `Slice` Views The `IndexView` component renders the structure of an `Index`, and registers a listener so that any changes to that result will cause a re-render. The `SliceView` component renders just a single `Slice` by iterating over each `Row` in that `Slice`. As with all ui-react view components, these use their corresponding hooks under the covers, which means that any changes to the `Index` or the `Row` objects referenced by it will cause a re-render. import {SliceView} from 'tinybase/ui-react'; const App3 = () => ( <div> <SliceView indexId="bySpecies" sliceId="dog" indexes={indexes} debugIds={true} /> </div> ); root.render(<App3 />); console.log(app.innerHTML); // -> '<div>dog:{fido:{species:{dog}}cujo:{species:{dog}}}</div>' A SliceView can be given a custom RowView-compatible component to render its children, much like a `TableView` component can. And an IndexView can be in turn given a custom SliceView-compatible component: import {IndexView} from 'tinybase/ui-react'; const MyRowView = (props) => <>{props.rowId};</>; const MySliceView = (props) => ( <div> {props.sliceId}:<SliceView {...props} rowComponent={MyRowView} /> </div> ); const App4 = () => ( <IndexView indexId="bySpecies" indexes={indexes} sliceComponent={MySliceView} /> ); root.render(<App4 />); console.log(app.innerHTML); // -> '<div>dog:fido;cujo;</div><div>cat:felix;</div><div>worm:lowly;</div>' ### `Indexes` Context In the same way that a `Store` can be passed into a `Provider` component context and used throughout the app, an `Indexes` object can also be provided to be used by default: import {Provider, useSliceRowIds} from 'tinybase/ui-react'; const App5 = () => { const store = useCreateStore(() => createStore().setTable('pets', { fido: {species: 'dog'}, felix: {species: 'cat'}, cujo: {species: 'dog'}, }), ); const indexes = useCreateIndexes(store, (store) => createIndexes(store).setIndexDefinition('bySpecies', 'pets', 'species'), ); return ( <Provider indexes={indexes}> <Pane /> </Provider> ); }; const Pane = () => ( <span> <SliceView indexId="bySpecies" sliceId="dog" debugIds={true} />/ {useSliceRowIds('bySpecies', 'cat')} </span> ); root.render(<App5 />); console.log(app.innerHTML); // -> '<span>dog:{fido:{species:{dog}}cujo:{species:{dog}}}/felix</span>' The `indexesById` prop can be used in the same way that the `storesById` prop is, to let you reference multiple `Indexes` objects by `Id`. ### Summary The support for `Indexes` objects in the `ui-react` module is very similar to that for the `Store` object and `Metrics` object, making it easy to attach `Index` and `Slice` contents to your user interface. We finish off this section about the `indexes` module with the Advanced Index Definition guide. --- ## Page: https://tinybase.org/guides/using-indexes/advanced-index-definition/ * TinyBase * Guides * Using Indexes * Advanced Index Definition This guide describes how the `indexes` module let you create more complex types of indexes based on the data in `Store` objects. ### Custom Sorting As well as indicating what the `Slice` `Ids` in the `Index` should be, based on a `Cell` value in each `Row`, you can also indicate how the `Row` `Ids` will be sorted inside each `Slice`. For example, here the members of each species are sorted by weight, by specifying that CellId in the fourth parameter: import {createIndexes, createStore} from 'tinybase'; const store = createStore().setTable('pets', { fido: {species: 'dog', weight: 42}, felix: {species: 'cat', weight: 13}, cujo: {species: 'dog', weight: 37}, }); const indexes = createIndexes(store); indexes.setIndexDefinition( 'bySpecies', // indexId 'pets', // tableId to index 'species', // cellId to index 'weight', // cellId to sort by ); console.log(indexes.getSliceRowIds('bySpecies', 'dog')); // -> ['cujo', 'fido'] With a further fifth and sixth parameter, you can also indicate how the `Slice` `Ids` (and the Rows within them) should be sorted. These two parameters take a 'sorter' function (much like JavaScript's own array `sort` method) which compares pairs of values. For example, to order the species Slices alphabetically, and the animals in each species in _reverse_ weight order: indexes.setIndexDefinition( 'bySpecies', // indexId 'pets', // tableId to index 'species', // cellId to index 'weight', // cellId to sort by (id1, id2) => (id1 < id2 ? -1 : 1), // Slices in alphabetical order (id1, id2) => (id1 > id2 ? -1 : 1), // Rows in reverse numerical order ); console.log(indexes.getSliceIds('bySpecies')); // -> ['cat', 'dog'] console.log(indexes.getSliceRowIds('bySpecies', 'dog')); // -> ['fido', 'cujo'] Note that you can use the `defaultSorter` function from the `common` module (which is literally equivalent to `(id1, id2) => (id1 < id2 ? -1 : 1)`) if all you want to do is sort something alphanumerically. Sorting is used in the Countries demo to sort both the `Slice` `Ids` (the first letters of the alphabet) and the `Row` `Ids` (the country names) within them. ### Getting Custom `Values` From Rows By default, our `Index` definitions have named a `Cell` in the `Row` which contains the string to use as the `Slice` `Id` - like the `species` `Cell` in the example above. Sometimes you may wish to derive a `Slice` `Id` for each `Row` that is not in a single `Cell`, and in this case you can replace the third parameter with a function which can process the `Row` in any way you wish. For example, we could group our pets into 'heavy' and 'light' Slices, based on the range that the `weight` `Cell` lies in.: indexes.setIndexDefinition( 'byWeightRange', // indexId 'pets', // tableId to index (getCell) => (getCell('weight') > 40 ? 'heavy' : 'light'), // => sliceId ); console.log(indexes.getSliceIds('byWeightRange')); // -> ['heavy', 'light'] console.log(indexes.getSliceRowIds('byWeightRange', 'light')); // -> ['felix', 'cujo'] You can also provide a function for the key to sort entries by. This sorts animal `Row` `Ids`, heaviest first, in each `Slice`: indexes.setIndexDefinition( 'byWeightRange', // indexId 'pets', // tableId to index (getCell) => (getCell('weight') > 40 ? 'heavy' : 'light'), // => sliceId (getCell) => -getCell('weight'), // => sort key ); console.log(indexes.getSliceRowIds('byWeightRange', 'light')); // -> ['cujo', 'felix'] And with that, we have covered most of the basics of using the `indexes` module. Let's move on to a very similar module for creating relationships between data in the Using Relationships guide. --- ## Page: https://tinybase.org/guides/using-relationships/an-intro-to-relationships/ * TinyBase * Guides * Using Relationships * An Intro To Relationships This guide describes how the `relationships` module gives you the ability to create and track relationships between `Row` objects based on the data in a `Store`. The main entry point to using the `relationships` module is the `createRelationships` function, which returns a new `Relationships` object. That object in turn has methods that let you create new `Relationship` definitions, access them directly, and register listeners for when they change. ### The Basics Here's a simple example to show a `Relationships` object in action. The `pets` `Table` has three `Row` objects, each with a string `species` `Cell`, which act as a key into the `Ids` of the `species` `Table`. We create a `Relationship` definition called `petSpecies` which connects the two: import {createRelationships, createStore} from 'tinybase'; const store = createStore() .setTable('pets', { fido: {species: 'dog'}, felix: {species: 'cat'}, cujo: {species: 'dog'}, }) .setTable('species', { dog: {price: 5}, cat: {price: 4}, }); const relationships = createRelationships(store); relationships.setRelationshipDefinition( 'petSpecies', // relationshipId 'pets', // localTableId to link from 'species', // remoteTableId to link to 'species', // cellId containing remote key ); console.log(relationships.getRemoteRowId('petSpecies', 'fido')); // -> 'dog' console.log(relationships.getLocalRowIds('petSpecies', 'dog')); // -> ['fido', 'cujo'] The `getRemoteRowId` method allows you to traverse in the many-to-one direction: in other words for every `Row` in the local `Table` you can find out the one remote `Row` referenced. The `getLocalRowIds` method is the reverse: for a remote `Row`, it will return an array of the `Row` `Ids` in the local `Table` that reference it. There is a special case when the local `Table` is the same as the remote `Table`. This creates a 'linked list' of `Row` `Ids`. Specify from which you would like to start the list, and the `getLinkedRowIds` method will return the list: store.setTable('pets', { fido: {species: 'dog', next: 'felix'}, felix: {species: 'cat', next: 'cujo'}, cujo: {species: 'dog'}, }); relationships.setRelationshipDefinition('petSequence', 'pets', 'pets', 'next'); console.log(relationships.getLinkedRowIds('petSequence', 'fido')); // -> ['fido', 'felix', 'cujo'] console.log(relationships.getLinkedRowIds('petSequence', 'felix')); // -> ['felix', 'cujo'] ### `Relationship` Reactivity As with `Metrics` and `Indexes`, `Relationships` objects take care of tracking changes that will affect the `Relationships`. The familiar paradigm is used to let you add a listener to the `Relationships` object. The listener fires when there's a change to a `Relationship`: const listenerId = relationships.addRemoteRowIdListener( 'petSpecies', 'cujo', () => { console.log(relationships.getRemoteRowId('petSpecies', 'cujo')); }, ); store.setCell('pets', 'cujo', 'species', 'wolf'); // -> 'wolf' As expected, reactivity will also work for local and linked `Row` relationships (with the `addLocalRowIdsListener` method and `addLinkedRowIdsListener` method respectively). You can set multiple `Relationship` definitions on each `Relationships` object. However, a given `Store` can only have one `Relationships` object associated with it. If you call this function twice on the same `Store`, your second call will return a reference to the `Relationships` object created by the first. Let's find out how to include relationships in a user interface in the Building A UI With Relationships guide. --- ## Page: https://tinybase.org/guides/using-relationships/building-a-ui-with-relationships/ * TinyBase * Guides * Using Relationships * Building A UI With Relationships This guide covers how the `ui-react` module supports the `Relationships` object. As with the React-based bindings to a `Store` object, the `ui-react` module provides both hooks and components to connect your relationships to your interface. ### `Relationships` Hooks As you may have guessed by now, there are three hooks you'll commonly use here: * The `useRemoteRowId` hook gets the remote `Row` `Id` for a given local `Row` in a `Relationship`. * The `useLocalRowIds` hook gets the local `Row` `Ids` for a given remote `Row` in a `Relationship`. * The `useLinkedRowIds` hook gets the linked `Row` `Ids` for a given `Row` in a linked list `Relationship`. Each hook registers a listener so that any relevant changes will cause a re-render. As an example: import React from 'react'; import {createRoot} from 'react-dom/client'; import {createRelationships, createStore} from 'tinybase'; import {useRemoteRowId} from 'tinybase/ui-react'; const store = createStore() .setTable('pets', {fido: {species: 'dog'}, cujo: {species: 'dog'}}) .setTable('species', {wolf: {price: 10}, dog: {price: 5}}); const relationships = createRelationships(store).setRelationshipDefinition( 'petSpecies', 'pets', 'species', 'species', ); const App = () => ( <span>{useRemoteRowId('petSpecies', 'cujo', relationships)}</span> ); const app = document.createElement('div'); const root = createRoot(app); root.render(<App />); console.log(app.innerHTML); // -> '<span>dog</span>' store.setCell('pets', 'cujo', 'species', 'wolf'); console.log(app.innerHTML); // -> '<span>wolf</span>' The `useCreateRelationships` hook is used to create a `Relationships` object within a React application with convenient memoization: import {useCreateRelationships, useCreateStore} from 'tinybase/ui-react'; const App2 = () => { const store = useCreateStore(() => createStore() .setTable('pets', { fido: {species: 'dog'}, felix: {species: 'cat'}, cujo: {species: 'dog'}, }) .setTable('species', {dog: {price: 5}, cat: {price: 4}}), ); const relationships = useCreateRelationships(store, (store) => createRelationships(store).setRelationshipDefinition( 'petSpecies', 'pets', 'species', 'species', ), ); return <span>{relationships?.getRemoteRowId('petSpecies', 'fido')}</span>; }; root.render(<App2 />); console.log(app.innerHTML); // -> '<span>dog</span>' ### `Relationships` Views The three components you'll use for rendering the contents of `Relationships` are the `RemoteRowView` component, `LocalRowsView` component, and `LinkedRowsView` component, each of which matches the three types of getters as expected. Also as expected (hopefully by now!), each registers a listener so that any changes to that result will cause a re-render. These components can be given a custom RowView-compatible component to render their `Row` children: import {CellView, RemoteRowView} from 'tinybase/ui-react'; const MyRowView = (props) => ( <> {props.rowId}: <CellView {...props} cellId="price" /> </> ); const App3 = () => ( <div> <RemoteRowView relationshipId="petSpecies" localRowId="cujo" rowComponent={MyRowView} relationships={relationships} /> </div> ); root.render(<App3 />); console.log(app.innerHTML); // -> '<div>wolf: 10</div>' ### `Relationships` Context In the same way that other objects can be passed into a `Provider` component context and used throughout the app, a `Relationships` object can also be provided to be used by default: import {Provider} from 'tinybase/ui-react'; const App4 = () => { const store = useCreateStore(() => createStore() .setTable('pets', {fido: {species: 'dog'}, cujo: {species: 'dog'}}) .setTable('species', {wolf: {price: 10}, dog: {price: 5}}), ); const relationships = useCreateRelationships(store, (store) => createRelationships(store).setRelationshipDefinition( 'petSpecies', 'pets', 'species', 'species', ), ); return ( <Provider relationships={relationships}> <Pane /> </Provider> ); }; const Pane = () => ( <span> <RemoteRowView relationshipId="petSpecies" localRowId="cujo" debugIds={true} /> /{useRemoteRowId('petSpecies', 'cujo')} </span> ); root.render(<App4 />); console.log(app.innerHTML); // -> '<span>cujo:{dog:{price:{5}}}/dog</span>' The `relationshipsById` prop can be used in the same way that the `storesById` prop is, to let you reference multiple `Relationships` objects by `Id`. ### Summary The support for `Relationships` objects in the `ui-react` module is very similar to that for the `Store` object, making it easy to attach relationships to your user interface. We finish off this section about the `relationships` module with the Advanced `Relationship` Definition guide. --- ## Page: https://tinybase.org/guides/using-relationships/advanced-relationship-definitions/ * TinyBase * Guides * Using Relationships * Advanced Relationship Definitions This guide describes how the `relationships` module let you create more complex types of relationships based on the data in `Store` objects. By default, our `Relationship` definitions have named a `Cell` in the `Row` which contains the string to use as the `Row` `Id` in the remote `Table` - like the `species` `Cell` in the previous guides' examples. Sometimes you may wish to derive a remote `Row` `Id` for each `Row` that is not in a single `Cell`, and in this case you can replace the fourth parameter with a function which can process the `Row` in any way you wish. For example, we could link our pets to a remote `Table` that is keyed off both color and species: import {createRelationships, createStore} from 'tinybase'; const store = createStore() .setTable('pets', { fido: {species: 'dog', color: 'brown'}, felix: {species: 'cat', color: 'black'}, cujo: {species: 'dog', color: 'black'}, }) .setTable('species_color', { dog_brown: {price: 6}, dog_black: {price: 5}, cat_brown: {price: 4}, cat_black: {price: 2}, }); const relationships = createRelationships(store); relationships.setRelationshipDefinition( 'petSpeciesColor', // relationshipId 'pets', // localTableId to link from 'species_color', // remote TableId to link to (getCell) => `${getCell('species')}_${getCell('color')}`, // => remote Row Id ); console.log(relationships.getRemoteRowId('petSpeciesColor', 'fido')); // -> 'dog_brown' console.log(relationships.getLocalRowIds('petSpeciesColor', 'dog_black')); // -> ['cujo'] And with that, we have covered most of the basics of using the `relationships` module. Let's move on to keeping track of changes to your data in the Using Checkpoints guide. --- ## Page: https://tinybase.org/guides/using-checkpoints/an-intro-to-checkpoints/ * TinyBase * Guides * Using Checkpoints * An Intro To Checkpoints This guide describes how the `checkpoints` module gives you the ability to create and track changes to a `Store`'s data for the purposes of undo and redo functionality. The main entry point to using the `checkpoints` module is the `createCheckpoints` function, which returns a new `Checkpoints` object. That object in turn has methods that let you set checkpoints, move between them (altering the underlying `Store` accordingly), and register listeners for when they change. `Checkpoints` let you undo and redo both keyed value and tabular data changes. ### The Basics Here's a simple example to show a `Checkpoints` object in action. The `fido` `Row` starts off with the `sold` `Cell` set to `false`. We set a checkpoint when this field changes, and which then allows us to return later to that initial state. import {createCheckpoints, createStore} from 'tinybase'; const store = createStore().setTables({pets: {fido: {sold: false}}}); const checkpoints = createCheckpoints(store); console.log(checkpoints.getCheckpointIds()); // -> [[], '0', []] store.setCell('pets', 'fido', 'sold', true); checkpoints.addCheckpoint('sale'); console.log(checkpoints.getCheckpointIds()); // -> [['0'], '1', []] checkpoints.goBackward(); console.log(store.getCell('pets', 'fido', 'sold')); // -> false console.log(checkpoints.getCheckpointIds()); // -> [[], '0', ['1']] The `getCheckpointIds` method deserves a quick explanation. It returns a `CheckpointIds` array which has three parts: * The 'backward' checkpoint `Ids` that can be rolled backward to (in other words, the checkpoints in the undo stack for this `Store`). They are in chronological order with the oldest checkpoint at the start of the array. * The current checkpoint `Id` of the `Store`'s state, or `undefined` if the current state has not been checkpointed. * The 'forward' checkpoint `Ids` that can be rolled forward to (in other words, the checkpoints in the redo stack for this `Store`). They are in chronological order with the newest checkpoint at the end of the array. The `goBackward` method is only one of the ways to move around the checkpoint stack. The `goForward` method lets you redo changes, and the `goTo` method lets you skip multiple checkpoints to undo or redo many changes at once. ### Checkpoint Reactivity As with `Metrics`, `Indexes`, and `Relationships` objects, you can add a listener to the `Checkpoints` object for whenever the checkpoint stack changes: const listenerId = checkpoints.addCheckpointIdsListener(() => { console.log(checkpoints.getCheckpointIds()); }); store.setCell('pets', 'fido', 'species', 'dog'); // -> [['0'], undefined, []] checkpoints.addCheckpoint(); // -> [['0'], '2', []] checkpoints.delListener(listenerId); Also note that when a new change is layered onto the original state, the previous redo of checkpoint '1' is now not available. A given `Store` can only have one `Checkpoints` object associated with it. If you call this function twice on the same `Store`, your second call will return a reference to the `Checkpoints` object created by the first. Finally, let's find out how to include checkpoints in a user interface in the Building A UI With Checkpoints guide. --- ## Page: https://tinybase.org/guides/using-checkpoints/building-a-ui-with-checkpoints/ This guide covers how the `ui-react` module supports the `Checkpoints` object. After all, if you have undo functionality in your app, you probably want an undo button! As with all the other React-based bindings we've discussed, the `ui-react` module provides both hooks and components to connect your checkpoints to your interface. ### `Checkpoints` Hooks Firstly, the `useCheckpointIds` hook is the reactive version of the `getCheckpointIds` method and returns the three-part `CheckpointIds` array. import React from 'react'; import {createRoot} from 'react-dom/client'; import {createCheckpoints, createStore} from 'tinybase'; import {useCheckpointIds} from 'tinybase/ui-react'; const store = createStore().setTable('pets', {fido: {species: 'dog'}}); const checkpoints = createCheckpoints(store); const App = () => <span>{JSON.stringify(useCheckpointIds(checkpoints))}</span>; const app = document.createElement('div'); const root = createRoot(app); root.render(<App />); console.log(app.innerHTML); // -> '<span>[[],"0",[]]</span>' store.setCell('pets', 'fido', 'sold', true); console.log(app.innerHTML); // -> '<span>[["0"],null,[]]</span>' checkpoints.addCheckpoint('sale'); console.log(app.innerHTML); // -> '<span>[["0"],"1",[]]</span>' This is not yet extremely useful for constructing an undo and redo UI! The `useCheckpoint` hook returns the label of a checkpoint so that the user knows what they are undoing, for example: import {useCheckpoint} from 'tinybase/ui-react'; const App2 = () => <span>{useCheckpoint('2', checkpoints)}</span>; root.render(<App2 />); console.log(app.innerHTML); // -> '<span></span>' store.setCell('pets', 'fido', 'color', 'brown'); checkpoints.addCheckpoint('color'); console.log(app.innerHTML); // -> '<span>color</span>' Further, hooks like the `useGoBackwardCallback` hook and the `useGoForwardCallback` hook are self-explanatory, providing a callback that can move the `Store` backwards or forwards through the checkpoints stack in response to a user event. ### `UndoOrRedoInformation` Perhaps more useful than each of those hooks individually, the `useUndoInformation` hook and `useRedoInformation` hook provide a collection of information (in an array of the `UndoOrRedoInformation` type) - including information about whether the action is possible, the event handler, and the label - that is fully sufficient to be able to construct an undo/redo UI: import {useUndoInformation} from 'tinybase/ui-react'; store.setTables({pets: {nemo: {species: 'fish'}}}); checkpoints.clear(); const App3 = () => { const [canUndo, handleUndo, id, label] = useUndoInformation(checkpoints); return canUndo ? ( <span onClick={handleUndo}>Undo {label}</span> ) : ( <span>Nothing to undo</span> ); }; root.render(<App3 />); console.log(app.innerHTML); // -> '<span>Nothing to undo</span>' store.setCell('pets', 'nemo', 'color', 'orange'); checkpoints.addCheckpoint('color'); console.log(app.innerHTML); // -> '<span>Undo color</span>' ### `Checkpoints` Views The `BackwardCheckpointsView` component, `CurrentCheckpointView` component, and `ForwardCheckpointsView` component are the main three components for the `Checkpoints` object, and list the checkpoints behind or ahead of the current state, so that a list of possible undo and redo actions is visible: import { BackwardCheckpointsView, CurrentCheckpointView, ForwardCheckpointsView, } from 'tinybase/ui-react'; const App4 = () => ( <div> <BackwardCheckpointsView checkpoints={checkpoints} debugIds={true} />/ <CurrentCheckpointView checkpoints={checkpoints} debugIds={true} />/ <ForwardCheckpointsView checkpoints={checkpoints} debugIds={true} /> </div> ); root.render(<App4 />); console.log(app.innerHTML); // -> '<div>0:{}/1:{color}/</div>' store.setCell('pets', 'nemo', 'stripes', true); checkpoints.addCheckpoint('stripes'); console.log(app.innerHTML); // -> '<div>0:{}1:{color}/2:{stripes}/</div>' Each of these components takes a `checkpointComponent` prop which allows you to customize how each checkpoint is rendered. Undoubtedly you will want something nicer than the default debug example above! ### `Checkpoints` Context In the same way that a `Store` can be passed into a `Provider` component context and used throughout the app, a `Checkpoints` object can also be provided to be used by default: import { Provider, useCreateCheckpoints, useCreateStore, } from 'tinybase/ui-react'; const App5 = () => { const store = useCreateStore(() => createStore().setTable('species', {pets: {nemo: {species: 'fish'}}}), ); const checkpoints = useCreateCheckpoints(store, createCheckpoints); return ( <Provider checkpoints={checkpoints}> <Pane /> </Provider> ); }; const Pane = () => <span>{JSON.stringify(useCheckpointIds())}</span>; root.render(<App5 />); console.log(app.innerHTML); // -> '<span>[[],"0",[]]</span>' The `checkpointsById` prop can be used in the same way that the `storesById` prop is, to let you reference multiple `Checkpoints` objects by `Id`. ### Summary The support for `Checkpoints` objects in the `ui-react` module is very similar to that for all the other types of top level object, making it easy to attach checkpoints and undo/redo functionality to your user interface. Let's move on to the ways in which we can create more advanced queries in the Using Queries guide. --- ## Page: https://tinybase.org/guides/using-queries/an-intro-to-queries/ * TinyBase * Guides * Using Queries * An Intro To Queries This guide describes how the `queries` module gives you the ability to create queries against `Tables` in the `Store` - such as selecting specific `Row` and `Cell` combinations from each `Table`, or performing powerful features like grouping and aggregation. The main entry point to using the `queries` module is the `createQueries` function, which returns a new `Queries` object. That object in turn has methods that let you create new query definitions, access their results directly, and register listeners for when those results change. The `Queries` module provides a generalized query concept for `Store` data. If you just want to create and track metrics, indexes, or relationships between rows, you may prefer to use the dedicated `Metrics`, `Indexes`, and `Relationships` objects, which have simpler APIs. ### The Basics Here's a simple example to show a `Queries` object in action. The `pets` `Table` has three `Row` objects, each with two Cells. We create a query definition called `dogColors` which selects just one of those, and filters the Rows based on the value in the other: import {createQueries, createStore} from 'tinybase'; const store = createStore().setTable('pets', { fido: {species: 'dog', color: 'brown'}, felix: {species: 'cat', color: 'black'}, cujo: {species: 'dog', color: 'black'}, }); const queries = createQueries(store); queries.setQueryDefinition('dogColors', 'pets', ({select, where}) => { select('color'); where('species', 'dog'); }); console.log(queries.getResultTable('dogColors')); // -> {fido: {color: 'brown'}, cujo: {color: 'black'}} The key to understanding how the `Queries` API works is in the `setQueryDefinition` line above. You provide a function which will be called with a selection of 'keyword' functions that you can use to define the query. These include `select`, `join`, `where`, `group`, and `having` and are described in the TinyQL guide. Note that, for getting data out, the `Queries` object has methods analogous to those in the `Store` object, prefixed with the word 'Result': * The `getResultTable` method is the `Queries` equivalent of the `getTable` method. * The `getResultRowIds` method is the `Queries` equivalent of the `getRowIds` method. * The `getResultSortedRowIds` method is the `Queries` equivalent of the `getSortedRowIds` method. * The `getResultRow` method is the `Queries` equivalent of the `getRow` method. * The `getResultCellIds` method is the `Queries` equivalent of the `getCellIds` method. * The `getResultCell` method is the `Queries` equivalent of the `getCell` method. The same conventions apply for registering listeners with the `Queries` object, as described in the following section. ### `Queries` Reactivity As with `Metrics`, `Indexes`, and `Relationships`, `Queries` objects take care of tracking changes that will affect the query results. The familiar paradigm is used to let you add a listener to the `Queries` object. The listener fires when there's a change to any of the resulting data: const listenerId = queries.addResultTableListener('dogColors', () => { console.log(queries.getResultTable('dogColors')); }); store.setCell('pets', 'cujo', 'species', 'wolf'); // -> {fido: {color: 'brown'}} Hopefully the pattern of the method naming is now familiar: * The `addResultTableListener` method is the `Queries` equivalent of the `addTableListener` method. * The `addResultRowIdsListener` method is the `Queries` equivalent of the `addRowIdsListener` method. * The `addResultSortedRowIdsListener` method is the `Queries` equivalent of the `addSortedRowIdsListener` method. * The `addResultRowListener` method is the `Queries` equivalent of the `addRowListener` method. * The `addResultCellIdsListener` method is the `Queries` equivalent of the `addCellIdsListener` method. * The `addResultCellListener` method is the `Queries` equivalent of the `addCellListener` method. You can set multiple query definitions on each `Queries` object. However, a given `Store` can only have one `Queries` object associated with it. If you call this function twice on the same `Store`, your second call will return a reference to the `Queries` object created by the first. Let's find out how to create different types of queries in the TinyQL guide. --- ## Page: https://tinybase.org/guides/using-queries/tinyql/ * TinyBase * Guides * Using Queries * TinyQL This guide describes how to build a query against `Store` data, using the API provided by the `setQueryDefinition` method in the `Queries` object. This guide is called 'TinyQL', rather flippantly. `Queries` to TinyBase are not made with a standard parsable language in the same way you would use SQL to access a traditional relational database. Rather, the API is typed and programmatic, making it a performant, unambiguous, and composable way to transform data. import {createQueries, createStore} from 'tinybase'; const store = createStore().setTable('pets', { fido: {species: 'dog', color: 'brown'}, felix: {species: 'cat', color: 'black'}, cujo: {species: 'dog', color: 'black'}, }); const queries = createQueries(store); queries.setQueryDefinition('query', 'pets', (keywords) => { // TinyQL goes here }); One downside might seem that you can't directly use your experience of SQL syntax to work with TinyBase queries - but, as you'll see, many of the similar concepts will be very familiar. You define a query with the `setQueryDefinition` method. It takes an `Id`, you'd like to assign to the query, and the `root` `Table` from which the data will be queried. The root table is like the first table that is named in a traditional SQL query, except that only 'left' joins can be made from it. (This means that the result of a query can never have more Rows than that underlying `Table` did.) The third parameter, `build`, is where the magic happens: you provide a function to define the query. that will be called with with an object that contains the five named 'keyword' functions for the query: * `select`: a function that lets you specify a `Cell` or calculated value for including into the query's result. * `join` describes a function that lets you specify a `Cell` or calculated value to join the main query `Table` to others, by `Row` `Id`. * `where` describes a function that lets you specify conditions to filter results, based on the underlying Cells of the main or joined `Tables`. * `group` describes a function that lets you specify that the values of a `Cell` in multiple result Rows should be aggregated together. * `having` describes a function that lets you specify conditions to filter results, based on the grouped Cells resulting from a `Group` clause. All five can be destructured from the callback's single parameter: queries.setQueryDefinition( 'query', 'pets', ({select, join, where, group, having}) => { select(/* ... */); select(/* ... */); join(/* ... */); where(/* ... */); group(/* ... */); having(/* ... */); // and so on... }, ); Any of these keyword functions can be called multiple times (even imperatively, such as in a loop). The only requirement for a valid query is that at least one `select` function call is made. Here's a quick summary of each of the five keyword functions. Some of them are overloaded and have different effects based on the number of arguments, but they are all fully typed with TypeScript and well documented with examples. ### `Select` The `Select` type describes the `select` function that lets you specify a `Cell` or calculated value for including into the query's result. You can chain the `as` function to change the name of the `Cell`. Calling this function with one `Id` parameter will indicate that the query should select the value of that specified `Cell` from the query's main `Table`: queries.setQueryDefinition('query', 'pets', ({select}) => select('color')); console.log(queries.getResultTable('query')); // -> {fido: {color: 'brown'}, felix: {color: 'black'}, cujo: {color: 'black'}} Calling this function with one _callback_ parameter will indicate that the query should select a calculated value, based on one or more `Cell` values: queries.setQueryDefinition('query', 'pets', ({select}) => select((getCell) => getCell('color')?.toUpperCase()).as('COLOR'), ); console.log(queries.getResultTable('query')); // -> {fido: {COLOR: 'BROWN'}, felix: {COLOR: 'BLACK'}, cujo: {COLOR: 'BLACK'}} If you are joining multiple `Tables` in the query, use an additional first parameter to indicate which the `Cell` should come from: store .setTable('pets', { fido: {species: 'dog', ownerId: '1'}, felix: {species: 'cat', ownerId: '2'}, cujo: {species: 'dog', ownerId: '3'}, }) .setTable('owners', { 1: {name: 'Alice'}, 2: {name: 'Bob'}, 3: {name: 'Carol'}, }); queries.setQueryDefinition('query', 'pets', ({select, join}) => { select('species'); select('owners', 'name'); join('owners', 'ownerId'); }); queries.forEachResultRow('query', (rowId) => { console.log({[rowId]: queries.getResultRow('query', rowId)}); }); // -> {fido: {species: 'dog', name: 'Alice'}} // -> {felix: {species: 'cat', name: 'Bob'}} // -> {cujo: {species: 'dog', name: 'Carol'}} ### `Join` We just saw the `join` keyword in action. The `Join` type describes a function that lets you specify a `Cell` or calculated value to join the main query `Table` to other `Tables`, by their `Row` `Id`. Calling this function with two `Id` parameters will indicate that the join to a `Row` in an adjacent `Table` is made by finding its `Id` in a `Cell` of the query's main `Table`, as in the example above. You can join zero, one, or many `Tables`. You can even join the same underlying `Table` multiple times, (though in that case you will need to use the 'as' function to distinguish them from each other): store .setTable('pets', { fido: {species: 'dog', buyerId: '1', sellerId: '2'}, felix: {species: 'cat', buyerId: '2'}, cujo: {species: 'dog', buyerId: '3', sellerId: '1'}, }) .setTable('humans', { 1: {name: 'Alice'}, 2: {name: 'Bob'}, 3: {name: 'Carol'}, }); queries.setQueryDefinition('query', 'pets', ({select, join}) => { select('buyers', 'name').as('buyer'); select('sellers', 'name').as('seller'); join('humans', 'buyerId').as('buyers'); join('humans', 'sellerId').as('sellers'); }); queries.forEachResultRow('query', (rowId) => { console.log({[rowId]: queries.getResultRow('query', rowId)}); }); // -> {fido: {buyer: 'Alice', seller: 'Bob'}} // -> {felix: {buyer: 'Bob'}} // -> {cujo: {buyer: 'Carol', seller: 'Alice'}} Because a `Join` clause is used to identify which unique `Row` `Id` of the joined `Table` will be joined to each `Row` of the main `Table`, queries follow the 'left join' semantics you may be familiar with from SQL. By default, each join is made from the main query `Table` to the joined table, but it is also possible to connect via an intermediate join `Table` to a more distant join `Table`. The `Join` type documentation provides many more examples of joining in queries. ### `Where` The `Where` type describes the `where` function that lets you specify conditions to filter results, based on the underlying Cells of the main or joined `Tables`. Calling this function with two parameters is used to include only those Rows for which a specified `Cell` in the query's main `Table` has a specified value: queries.setQueryDefinition('query', 'pets', ({select, where}) => { select('species'); where('species', 'dog'); }); console.log(queries.getResultTable('query')); // -> {fido: {species: 'dog'}, cujo: {species: 'dog'}} A `Where` condition has to be true for a `Row` to be included in the results. Each `Where` class is additive, as though combined with a logical 'and'. If you wish to create an 'or' expression, or just create a more complex criterion, use the single parameter that allows arbitrary programmatic conditions: queries.setQueryDefinition('query', 'pets', ({select, where}) => { select('species'); where((getCell) => getCell('species')?.[0] == 'c'); }); console.log(queries.getResultTable('query')); // -> {felix: {species: 'cat'}} ### `Group` The `Group` type describes the `group` function that lets you specify that the values of a `Cell` in multiple result Rows should be aggregated together. This is applied after any joins or where-based filtering. If you provide a `Group` for every `Select`, the result will be a single `Row` with every `Cell` having been aggregated. If you provide a `Group` for only one, or some, of the `Select` clauses, the _others_ will be automatically used as dimensional values (analogous to the 'group by`semantics in SQL), within which the aggregations of [`Group\`\](/api/queries/type-aliases/definition/group/) Cells will be performed. You can join the same underlying `Cell` multiple times, but in that case you will need to use the 'as' function to distinguish them from each other. store.setTable('pets', { fido: {species: 'dog', price: 5}, felix: {species: 'cat', price: 4}, cujo: {species: 'dog', price: 4}, tom: {species: 'cat', price: 3}, carnaby: {species: 'parrot', price: 3}, polly: {species: 'parrot', price: 3}, }); queries.setQueryDefinition('query', 'pets', ({select, group}) => { select('species'); select('price'); group('price', 'count').as('count'); group('price', 'avg').as('avgPrice'); }); queries.forEachResultRow('query', (rowId) => { console.log({[rowId]: queries.getResultRow('query', rowId)}); }); // -> {0: {species: 'dog', count: 2, avgPrice: 4.5}} // -> {1: {species: 'cat', count: 2, avgPrice: 3.5}} // -> {2: {species: 'parrot', count: 2, avgPrice: 3}} You can also provide your own aggregate function, with optional 'shortcut' functions if you can avoid calculating the new value for the result without scanning every value again (much like you can with calculated `Metrics`). See the `Group` documentation for more details and examples. ### `Having` The `Having` type describes the `having` function that lets you specify conditions to filter results, based on the grouped Cells resulting from a `Group` clause. Like the `where` function, call this with two parameters is used to include only those Rows for which a specified `Cell` in the query's main `Table` has a specified value. _Unlike_ the `where` function, this filtering is applied _after_ the grouping has been performed, much like you would use `HAVING` instead of `WHERE` in a classic SQL environment. queries.setQueryDefinition('query', 'pets', ({select, group, having}) => { select('pets', 'species'); select('pets', 'price'); group('price', 'min').as('minPrice'); group('price', 'max').as('maxPrice'); having('minPrice', 3); }); queries.forEachResultRow('query', (rowId) => { console.log({[rowId]: queries.getResultRow('query', rowId)}); }); // -> {0: {species: 'cat', minPrice: 3, maxPrice: 4}} // -> {1: {species: 'parrot', minPrice: 3, maxPrice: 3}} (Note that you shouldn't make any assumptions about the `Row` `Ids` on a result `Table` that has been grouped.) Again, multiple `having` functions will behave as though combined with a logical 'and'. If you wish to create an 'or' expression, or just create a more complex criterion, use the single parameter version that allows arbitrary programmatic conditions queries.setQueryDefinition('query', 'pets', ({select, group, having}) => { select('pets', 'species'); select('pets', 'price'); group('price', 'min').as('minPrice'); group('price', 'max').as('maxPrice'); having((getCell) => getCell('minPrice') == getCell('maxPrice')); }); console.log(queries.getResultTable('query')); // -> {0: {species: 'parrot', minPrice: 3, maxPrice: 3}} ### Putting It All Together To finish off, let's look at a more complex complex TinyQL query that includes all the keywords in use together, and shows how something similar might have been expressed in SQL. First let's build an interesting dataset: store .setTable('pets', { fido: {speciesId: '1', colorId: '1', ownerId: '1'}, rex: {speciesId: '1', colorId: '2', ownerId: '6'}, cujo: {speciesId: '1', colorId: '3', ownerId: '3'}, felix: {speciesId: '2', colorId: '2', ownerId: '2'}, tom: {speciesId: '2', colorId: '1', ownerId: '5'}, lowly: {speciesId: '3', colorId: '1', ownerId: '4'}, smaug: {speciesId: '3', colorId: '4', ownerId: '1'}, }) .setTable('species', { 1: {name: 'dog', price: 5}, 2: {name: 'cat', price: 4}, 3: {name: 'worm', price: 1}, }) .setTable('color', { 1: {name: 'brown', premium: 1.0}, 2: {name: 'black', premium: 1.5}, 3: {name: 'white', premium: 2}, 4: {name: 'silver', premium: 4}, }) .setTable('owner', { 1: {name: 'Alice', regionId: '1'}, 2: {name: 'Bob', regionId: '1'}, 3: {name: 'Carol', regionId: '2'}, 4: {name: 'Dennis', regionId: '2'}, 5: {name: 'Errol', regionId: '3'}, 6: {name: 'Fiona', regionId: '4'}, }) .setTable('region', { 1: {name: 'California', country: 'US'}, 2: {name: 'New York', country: 'US'}, 3: {name: 'Washington', country: 'US'}, 4: {name: 'British Columbia', country: 'CA'}, }); And then an interesting query against it: queries.setQueryDefinition( 'query', 'pets', ({select, join, where, group, having}) => { // SELECT... select('state', 'name').as('stateName'); select( (getTableCell) => getTableCell('species', 'price') * getTableCell('color', 'premium'), ).as('fullPrice'); // FROM... ['species', 'color', 'owner'].forEach((table) => join(table, `${table}Id`)); join('region', 'owner', 'regionId').as('state'); // WHERE... where('state', 'country', 'US'); // GROUP group('fullPrice', 'avg').as('avgFullPrice'); // HAVING having((getCell) => getCell('avgFullPrice') >= 5); }, ); (Notice how the joins to the `species`, `color`, and `owner` tables are performed programmatically here - just to prove a point! - because there's a useful convention on the `Cell` `Ids` used. Also see the Movie Database demo for an example of modular query composition.) This query is roughly expressed in English as "The average price of pets per state (based on their color and species) sold to owners who live in the US, for states where that average price is 5 or more, and listing the top three states in descending order" But for the "top three states in descending order" part, you've probably noticed by now that there is no TinyQL equivalent to SQL's `ORDER BY` or `LIMIT`. Instead you perform sorting and optional pagination when you actually extract the data, using the `getResultSortedRowIds` method: queries .getResultSortedRowIds('query', 'avgFullPrice', true, 0, 3) .forEach((rowId) => console.log(queries.getResultRow('query', rowId))); // -> {"stateName": "New York", "avgFullPrice": 5.5} // -> {"stateName": "California", "avgFullPrice": 5} The results should check out: the owners who are in the US are Alice, Bob, Carol, Dennis, Errol. In California, the pets are Fido (a brown dog, costing 5), Felix (a black cat, costing 6) and Smaug (a silver worm, costing 4), averaging 5 for that state. In New York, the pets are Cujo (a white dog, costing 10) and Lowly (a brown worm, costing 1), averaging 5.5 for that state. In Washington, the only pet is Tom (a brown cat, costing 4), so that whole state fails the minimum average price. The equivalent SQL for what we've just done here would be something like: SELECT state.name AS stateName, AVG(species.price * color.premium) AS avgFullPrice, FROM pets LEFT JOIN species ON species._rowId = pets.speciesId LEFT JOIN color ON color._rowId = pets.colorId LEFT JOIN owner ON owner._rowId = pets.ownerId LEFT JOIN region AS state ON region._rowId = owner.regionId WHERE state.country = 'US' GROUP BY stateName HAVING avgFullPrice >= 5; ORDER BY avgFullPrice DESC LIMIT 3 Hopefully that makes sense! Wait... did we mention it's also reactive? It's probably worth showcasing something that SQL struggles to do :) queries.addResultSortedRowIdsListener( 'query', 'avgFullPrice', true, 0, 3, (_queries, _queryId, _cellId, _descending, _offset, _limit, rowIds) => console.log(queries.getResultTable('query')), ); // Bob is actually in British Columbia! store.setCell('owner', 2, 'regionId', '4'); // -> {1: {"stateName": "New York", "avgFullPrice": 5.5}} Now that Bob is in Canada, removing Felix (cost 6) from California lowers its average to 4.5 - too low for our results, and we only see New York! Magic. ### Summary Next, let's find out how to include queries in a user interface in the Building A UI With Queries guide. --- ## Page: https://tinybase.org/guides/using-queries/building-a-ui-with-queries/ * TinyBase * Guides * Using Queries * Building A UI With Queries This guide covers how the `ui-react` module supports the `Queries` object. As with the React-based bindings to a `Store` object, the `ui-react` module provides both hooks and components to connect your queries to your interface. ### `Queries` Hooks In previous guides, we've seen how the `Queries` methods follow the same conventions as raw `Table` methods, such as how The `getResultRow` method is the equivalent of the `getRow` method. So it should be no surprise that the ui-react hooks follow the same convention, and that the hooks correspond to each of the Query getter methods: * The `useResultTable` hook is the reactive equivalent of the getResultTable method. * The `useResultRowIds` hook is the reactive equivalent of the getResultRowIds method. * The `useResultSortedRowIds` hook is the reactive equivalent of the `getResultSortedRowIds` method. * The `useResultRow` hook is the reactive equivalent of the `getResultRow` method. * The `useResultCellIds` hook is the reactive equivalent of the getResultCellIds method. * The `useResultCell` hook is the reactive equivalent of the `getResultCell` method. Each hook registers a listener so that any relevant changes will cause a re-render. As an example: import React from 'react'; import {createRoot} from 'react-dom/client'; import {createQueries, createStore} from 'tinybase'; import {useResultRowIds} from 'tinybase/ui-react'; const store = createStore().setTable('pets', { fido: {species: 'dog', color: 'brown'}, felix: {species: 'cat', color: 'black'}, cujo: {species: 'dog', color: 'black'}, }); const queries = createQueries(store).setQueryDefinition( 'dogColors', 'pets', ({select, where}) => { select('color'); where('species', 'dog'); }, ); const App = () => ( <span>{JSON.stringify(useResultRowIds('dogColors', queries))}</span> ); const app = document.createElement('div'); const root = createRoot(app); root.render(<App />); console.log(app.innerHTML); // -> '<span>["fido","cujo"]</span>' store.setCell('pets', 'cujo', 'species', 'wolf'); console.log(app.innerHTML); // -> '<span>["fido"]</span>' The `useCreateQueries` hook is used to create a `Queries` object within a React application with convenient memoization: import {useCreateQueries, useCreateStore} from 'tinybase/ui-react'; const App2 = () => { const store = useCreateStore(() => createStore().setTable('pets', { fido: {species: 'dog', color: 'brown'}, felix: {species: 'cat', color: 'black'}, cujo: {species: 'dog', color: 'black'}, }), ); const queries = useCreateQueries(store, (store) => createQueries(store).setQueryDefinition( 'dogColors', 'pets', ({select, where}) => { select('color'); where('species', 'dog'); }, ), ); return <span>{JSON.stringify(useResultRowIds('dogColors', queries))}</span>; }; root.render(<App2 />); console.log(app.innerHTML); // -> '<span>["fido","cujo"]</span>' ### `Queries` Views Entirely following convention, there are also components for rendering the contents of queries: the `ResultTableView` component, the `ResultSortedTableView` component, the `ResultRowView` component, and the `ResultCellView` component. Again, simple prefix the component names with `Result`. And of course, each registers a listener so that any changes to that result will cause a re-render. Just like their `Store` equivalents, these components can be given a custom components to render their children: import {ResultCellView, ResultTableView} from 'tinybase/ui-react'; const MyResultRowView = (props) => ( <span> {props.rowId}: <ResultCellView {...props} cellId="color" /> </span> ); const App3 = () => ( <div> <ResultTableView queryId="dogColors" resultRowComponent={MyResultRowView} queries={queries} /> </div> ); store.setTable('pets', { fido: {species: 'dog', color: 'brown'}, felix: {species: 'cat', color: 'black'}, cujo: {species: 'dog', color: 'black'}, }); root.render(<App3 />); console.log(app.innerHTML); // -> '<div><span>fido: brown</span><span>cujo: black</span></div>' ### `Queries` Context In the same way that other objects can be passed into a `Provider` component context and used throughout the app, a `Queries` object can also be provided to be used by default: import {Provider, ResultRowView, useRemoteRowId} from 'tinybase/ui-react'; const App4 = () => { const store = useCreateStore(() => createStore().setTable('pets', { fido: {species: 'dog', color: 'brown'}, felix: {species: 'cat', color: 'black'}, cujo: {species: 'dog', color: 'black'}, }), ); const queries = useCreateQueries(store, (store) => createQueries(store).setQueryDefinition( 'dogColors', 'pets', ({select, where}) => { select('color'); where('species', 'dog'); }, ), ); return ( <Provider queries={queries}> <Pane /> </Provider> ); }; const Pane = () => ( <span> <ResultRowView queryId="dogColors" rowId="cujo" debugIds={true} />/ {useRemoteRowId('dogColors', 'cujo')} </span> ); root.render(<App4 />); console.log(app.innerHTML); // -> '<span>cujo:{color:{black}}/</span>' The `queriesById` prop can be used in the same way that the `storesById` prop is, to let you reference multiple `Queries` objects by `Id`. ### Summary The support for `Queries` objects in the `ui-react` module is very similar to that for the `Store` object, making it easy to attach queries to your user interface. We now move on to learning about the Inspector tool that TinyBase provides. Read more about that in the Inspector Data guide. --- ## Page: https://tinybase.org/guides/how-tinybase-is-built/developing-tinybase/ This guide is for people who would like to checkout the TinyBase code and build it from source. It's a quick overview of the common workflows. ### Checking Out The Code Check out the TinyBase repository with the following command (assuming you have all the dependent tools installed), and install the developer dependencies: git clone git@github.com:tinyplex/tinybase.git cd tinybase npm install You are good to get started! There are a number of gulp-based workflows available if you are working on TinyBase, and the following are some of the important ones: ### Compilation To compile the Typescript into executable JavaScript you can either choose to do it quickly into one module for the purposes of testing: npm run compileForTest It should take a few seconds at most. Production-ready minified modules take a little longer: npm run compileForProd ### Code Quality Your IDE should be able to do this continuously. But should you wish to check that Typescript validates the entire codebase and that there are no type errors or unused exports, use the following: npm run ts Similarly, this task ensure the code lints and is all formatted correctly with prettier: npm run lint You can also check that there are no spelling mistakes in the code or documentation - which is very important! npm run spell ### Testing There are three major sets of tests: unit tests, performance tests, and end-to-end tests of the demos in the TinyBase site. The unit tests, interestingly, include testing all the inline code and examples in the documentation. Unit tests are the most common to run. If the code is already compiled, you can run these with: npm run testUnit Or, while you are iterating on code, the following will compile _and_ unit test it: npm run compileAndTestUnit Performance tests are very similar: npm run testPerf ...or: npm run compileAndTestPerf Every performance test will render an ASCII chart which should be relatively flat. The idea here is that you can check visually that you haven't introduced a high time-complexity algorithm bug. Finally, end-to-end testing validates that the demos on the website work. This requires you to compile for production, and generate the documentation first: npm run compileForProd npm run compileDocs npm run testE2e ### Documentation The end-to-end tests will start up their own web server to test the documentation site, but if you separately want to serve the TinyBase website, you can use: npm run compileDocs npm run serveDocs This will make the website available on `localhost:8080` (and you probably want to run the `serveDocs` task in a separate window since it's long-running). The `compileDocs` task involves generating all the API documentation which can be a little slow. If you just want to work on the website styling or interactivity you can instead just compile the assets: npm run compileDocsAssetsOnly And if you are working on just the demos or guides, the following generates just those: npm run compileDocsPagesOnly This removes all the API documentation though, so don't forget to run a full `compileDocs` task again before finally committing code. ### The Master Check To make sure everything is in decent shape before committing or publishing to npm, this task is a superset of everything: npm run prePublishPackage That's it, and see you in the pull requests! --- ## Page: https://tinybase.org/guides/how-tinybase-is-built/architecture/ The architecture of TinyBase is pretty straightforward. This guide runs through the main file structure and principles. The top level directory contains lots of configuration files: for the package as a whole (`package.json`), for Jest (`jest.config.js`), for Prettier (`.prettierrc`) and for ESLint (`.eslintrc.json`). TypeScript configuration is _not_ in the top-level directory, but is co-located with the `src` and `test` files independently. `gulpfile.mjs` is an important file, since it describes all the build steps for the project, many of which are described in the Developing TinyBase guide. ### src The main source code is in the top-level `src` directory, where there is a pair of files for each major module: the `.d.ts` files in the `@types` folder containing the Typescript definitions (and the source of truth for the documentation), and the `.ts` file containing the main logic. Most modules have a similar pattern: a single creation function (such as the `createStore` function in the case of the `store` module), and a major interface that the returned object conforms to (such as the `Store` interface). As well as those, there are a set of common functions and utilities in the `common` subdirectory, implementations used by the `persisters` module in the `persisters` subdirectory, and hooks and components for the `ui-react` module in the `ui-react` subdirectory. There is a top-level `tinybase.d.ts` and `tinybase.ts` pair to wrap everything together into a single convenient package: these contain just imports. ### test There are Jest unit tests for all modules in the `unit` subdirectory. The `perf` subdirectory contains the performance tests. The `e2e` subdirectory contains Puppeteer tests for checking all the demos on the website work. The `jest` subdirectory contains some extensions that allow TinyBase to do things like count and report the total number of assertions. There is more about TinyBase's testing in the following Testing guide. ### site This is the folder containing content and assets required for the TinyBase website. `demos` and `guides` subdirectories are self-explanatory and contain markdown files for site pages. The `ui` folder contains some React components that are server-side rendered at build time to populate the `docs` directory which is used as a straightforward GitHub pages deployment. API documentation comes from the `.d.ts` files by way of the TinyDocs library (which is essentially a custom wrapper around TypeDoc). Other static assets like CSS and JS are built into the final site from other folders in this directory. ### dist Not checked in, but distributed via NPM, is a `dist` directory, which contains the `.d.ts` type definitions, the compiled `.js` files, and their compressed `.js.gz` equivalents. The `exports` field of the `package.json` wire everything together into the right place when people use the library. These build configurations are all defined in the `compileForProd` task in `gulpfile.mjs`, and instructions for importing them are in the Importing TinyBase guide. --- ## Page: https://tinybase.org/guides/how-tinybase-is-built/testing/ Testing is a first-class citizen in TinyBase, as you may see from the high coverage it enjoys. All testing is coordinated by Jest, and test are all found in the `test` directory within the project. The unit tests (in the `unit` subdirectory), given their names and test titles, should be self-explanatory - there's typically one or two per main module. Note that the tests are run against the debug build of the modules, so they require the `compileForTest` task to have been run, as described in the Developing TinyBase guide. Interestingly, the unit tests also include testing all the code snippets in the API documentation and the guides. This means that it's guaranteed that all example code will run correctly, and also (given the number of docs) provides a little bit of extra coverage. The magic to do this (which is alarmingly dependent on regular expressions) is in the `documentation.test.ts` file. There are a few helper functions for the tests. These do things like track listener calls and enumerate complex object structures so they can be easily asserted with. The `perf` subdirectory contains the performance tests. These benchmark large numbers of operations to check for time-complexity. Note that TinyBase performance tests do not have particularly aggressive pass or fail thresholds per se, since the absolute numbers will depend on underlying hardware. However, every test will render an ASCII chart which should be relatively flat. The idea here is that you can check visually that you haven't introduced a high time-complexity algorithm bug. For example: Grow store, different table to index with multiple row count, ยตs per row First: 16.3 ยตs per row Last: 3.61 ยตs per row Min: 2.76 ยตs per row Max: 29.89 ยตs per row Avg: 7.33 ยตs per row 90.00 โผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ 84.00 โค 78.00 โค 72.00 โค 66.00 โค 60.00 โค 54.00 โค 48.00 โค 42.00 โค 36.00 โค 30.00 โคโญโฎ 24.00 โคโโ โญโฎ 18.00 โผโฏโ โโ โญโฎ 12.00 โค โฐโฎ โญโฏโ โญโฎ โโ โญโฎ 6.00 โผโโโฐโโโโฏโโฐโโโโโฏโฐโโโโฏโฐโโโโโโโโโโโฏโฐโฎโโโญโโโ 0.00 โค โฐโโโฏ Finally, the end-to-end tests in the `e2e` subdirectory run Puppeteer scripts to drive simple transactions through the demos on the TinyBase website. These are good ways of catching major regressions when more complex apps are put together. --- ## Page: https://tinybase.org/guides/how-tinybase-is-built/documentation/ * TinyBase * Guides * How TinyBase Is Built * Documentation Like testing, documentation is a first-class citizen of TinyBase, and most of it is structured as API documentation (from the `.d.ts` files) and from markdown pages. TinyBase uses a library called TinyDocs that wraps TypeDoc to parse and render documentation that's inline in the type declarations. Every type, function, and interface is documented, categorized, and given an example - there's even an ESLint rule to guarantee it! The documentation is rendered as static HTML pages to be served by GitHub Pages, and there is some light JavaScript added to provide a pseudo single-page navigation experience. All documentation is spell-checked with CSpell. --- ## Page: https://tinybase.org/guides/how-tinybase-is-built/how-the-demos-work/ The demos on the TinyBase site deserve a little explanation. For example, take a look at the Todo App v1 (the basics) demo. It contains a guide that explains each part of the demo step-by-step, whether HTML, JavaScript, or LESS. And then at the top of the page, the demo actually runs. Rather than trying to keep prose and running demo in sync, something interesting here is that the code _in_ the guide is exactly what is compiled together at build time to create the running demo. Basically all of the code from the guide is concatenated together into a single file: the HTML snippets at the start, then the JavaScript snippets inline, and then the LESS (processed into CSS) inline as styles. THe resulting 'single-file' HTML is then minified and attached to an iframe (via the `srcdoc` attribute) so that it is nicely sandboxed. When a demo is an enhancement to an existing one (such as the Todo App v2 (indexes) demo), the guide will contain diffs. These are also actively applied to the previous code. It's a little tricky to make sure the diffs always apply cleanly when you update preceding demo guides, but it reduces repetition. Finally, some client-side JavaScript makes it possible to launch the demos into CodePen via their pre-fill API. --- ## Page: https://tinybase.org/guides/how-tinybase-is-built/credits/ * TinyBase * Guides * How TinyBase Is Built * Credits I'm James. Building TinyBase was an interesting exercise in API design, minification, and documentation. But now people seem to like using it! I had the privilege of building this project just as I wanted, with as much time as I needed, and for nothing other than my personal creativity. I wrote about some of the interesting things I learned about having fewer constraints on a software project, here. ### Giants... ...upon whose shoulders TinyBase stands are numerous. There are no required run-time dependencies, but at develop- and build- time, things are made a lot easier by the following (non-exhaustive list) of awesome projects: * TypeScript for typing * ESLint (and various plugins) for linting * Prettier for formatting * CSpell for spell-checking * ESBuild for transpilation * React for UI and documentation * LESS for styling * Rollup for bundling * Gulp for build orchestration * Jest for testing * TypeDoc for documentation * asciichart for performance charting * Puppeteer for browser automation ### ...And Friends Such as those of you who provided feedback on TinyBase before it launched. You were very polite, even when it didn't make sense. And maybe it still doesn't, but I tried! * Christopher Chedeau * Dion Almaer * Adam Wolff * Michael Bolin * Michael Mullaney * Adrien Friggeri * Paul Yiu Thank you all!