TinyBase: A JavaScript library for structured state
tinybase.org
tinybase.org
My app is still alive and kicking and I ended up implementing many of the ideas here myself, in a less reusable fashion. A primary motivator was that as the use cases evolve, a storage model optimized for application specific access patterns becomes compelling.
The convenience of having properly implemented relational semantics and performance enhancements of Indices is huge, but the risk of hitting assumptions that don't fit your use case is also non-trivial
I agree with your very final point. The same might be true of the undo stack implementation too - at some point the usage patterns might invalidate some of my performance assumptions.
Everything clicked quickly for me and the API covers the basic use cases and more.
Of course, reading docs doesn't say anything about maintainability or performance, but at least, it got me curious enough to assess them myself :)
Congrats to the authors!
(That also spits out ascii charts to visually make sure there are no complexity explosions. See https://tinybase.org/guides/how-tinybase-is-built/testing/ for example.)
As for maintainability... I have no data yet, since it's a couple of days old ;)
I thought the author had lost his mind, declaring dozens of new functions to simply wrap inbuilt functions and religiously not using classes.
Then I realized it probably cuts the bundle size by 2/3rds to remove every "this.longBuiltinFnName()" and replace it with a minified function call.
https://github.com/tinyplex/tinybase/blob/main/src/common/ar...
which could be to some extent and example of what the OP is talking about.
I don't think there's dozens, but there's a fair few. Most of the stuff in the common directory seems to be standard patterns they use, so it's doing more than just wrapping a function.
class Foo { foo() {} #bar() {} }
Becomes:
class f { foo() {} b() {} }
Combined with any type of compression the negligible difference is eliminated. Though it is true that static functions might be more optimisable in some cases, I'd say that on balance, the effect on maintainability is too significant for the returns. I'll take DI and testability over maybe a few kb difference
Quite possible. This was a journey! https://tripleodeon.com/2022/01/software-without-compromise/
When I first started thinking about this project, classes still meant a bunch of transpilation overhead (which dates the origin story!). I persuaded myself that plain old objects with TypeScript interfaces would strike a better balance between performance, size, and developer experience.
So I ended up with a pattern like:
const longMethod1 = () => {...};
const longMethod2 = () => {...};
return {longMethod1, longMethod2};
These 'methods' can still call each other by name, and it minifies like a champ.https://svelte.dev/repl/4434d8fcd12242d79887343fd95e429c?ver...
You can wrap pretty much any POD/array data type as a store then index into it to generate new stores with view on to sub elements.
Note that a "store" is a reactive data store. You can set a value on it and also get notified of changes.
This means you can have a single store for your main data and then pass views on that data to sub components.
Views are created using selector expressions.
let data = {
name: 'thomas',
age: 25
}
let root = writable(data)
let rootUndo = undoStore(root)
let nameStore = subStore(rootUndo, v=>v.name)
let ageStore = subStore(rootUndo, v=>v.age)
let undoDisabled = derived(rootUndo.canUndo, u=>!u)
let redoDisabled = derived(rootUndo.canRedo, u=>!u)
----
<input type="text" bind:value={$nameStore}/>
<input type="number" bind:value={$ageStore}/>
<button on:click={undo} disabled= {$undoDisabled}>undo</button>
<button on:click={redo} disabled= {$redoDisabled}>redo</button>
Library is here https://github.com/bradphelan/immer.loves.svelteImagine how simple apps would be if your data model and query interface was exactly the same on the server and client. With automatic caching and fetching logic.
There is such an incredibly complex chain of code to do the simplest of things.
What we lack is a good client-side relational+graph database that has an identical server-side implementation.
I believe Orbit.js was inspired by Ember Data, as I know Dan Gebhardt is involved with Ember.js and https://jsonapi.org
There's WatermelonDB which uses IndexedDB on web and SQLite on native, it's nice for syncing to custom backends.
There's GUN and Orbit for distributed graph databases.
Ontopic: TinyBase looks really really nice, fills the gap in-between hefty client side databases and state system solutions like Redux + Persist. I'd like to see Redux middleware integration for time travel debugging, event lots, and snapshotting if possible. The analytics and rollback APIs are a nice touch. Size is enticing.
The trouble is though, it is now quite an ageing codebase with relatively little maintenance. The original developers have moved on and it’s a little neglected. A few community members have picked up the mantle in the last 6 months but I would be careful picking it for something new.
Last time I looked the bug tracker auto closed tickets after a month if there was no activity. That makes it look like there are few bugs being tracked. Problem is there is loads, they are just all closed.
1. Batching, to avoid network waterfalls (a consequence of network use being more expensive on clients than on the server).
3. Data dependencies, so the framework knows how to stitch queries together when batching them.
3. Consistency, so all UIs update when an update comes in (a consequence of SPAs being long-lived, as opposed to the typical stateless server request).
These all take some code/framework overhead. It’s valuable to pay that cost on clients, but is unnecessarily verbose for the server.
People have been trying to unify local and remote function calls for years, and it’s a similar problem: the two are fundamentally different.
1: https://github.com/tonsky/datascript 2: https://www.datomic.com/ 3: https://github.com/metasoarous/datsync
Combine it with the various JavaScript ORMs and you have a nice developer UX.
I’m waiting for someone with more time than myself to build a syncing feature on top of SQLites Sessions[2] so that changes locally are synced back to the server.
(Feel like I’m a cheer leader posting this comment every week)
I'm not sure but I expect it would depend on your use cases. High frequency i/o of small data might be slower compared to low frequency i/o of very large data.
Mind you I'm working off of a cursory understanding of WASM performance issues from 2 years ago. Maybe this has changed a lot, or my understanding was incorrect in the first place. Do you know much about this?
With the database running in the same process as your app, you could store native JS Objects of your table rows in the cache, which you could bind events to. So if you modify a row in a table in the db, events could be instantly propagated to the UI. Haven't thought it through fully yet.
SQL is also a bit cumbersome for things like deeply nested data, hierarchies, trees, graphs which a lot of client-side application state ends up being.
Sessions looks cool!
> (Feel like I’m a cheer leader posting this comment every week)
I haven't seen it yet, so thank you! And in the name of others like me who haven't seen it yet, keep cheering please.This idea got muddled when people started making websites as SPAs, which necessitated SSR for SEO and first-page load. Now you blurred the lines between client and server, otherwise known as isomorphism, which is a good sounding word for a bad idea.
I’m guessing it’s in memory only, and so has load the whole dataset?
Quite similar to LokiJS:
https://github.com/techfort/LokiJS
I like how’s its modular so you only have to include the bits you want and not a massive library.
You can see this in action in the TinyDraw demo: https://tinybase.org/demos/tinydraw - make some changes, refresh the page, even open it up in two windows of the same browser at the same time.
The website does a great job explaining how to use the api, but it would be nice to see some functional examples as well. Its difficult to imagine for me, in which use cases Tinybase is shining.
We end up pulling in very large amounts of data to the frontend (details on every alert and event in kubernetes clusters) and then filter it multiple ways and graph it on a timeline.
So we end up implementing lots of db like logic in typescript. I've long wished for a simpler and more reusable way to do this that still let's me process the data clientside.
Here's an example of a simple timeline graph (built with vegalite) which needs the ability to filter by several columns and do various groupings
https://ucarecdn.com/0c76c5ef-1eb4-4320-8e46-abb9c3956658/-/...
For a snappy data-centric app, you pretty much need your entire app logic to run client-side.
Everyone always ends up with this terrible denormalized client-side cache with duplicated Server side logic.
What you really want is the exact same db running server-side and client-side.
I thought for a while this would be an sql db but now I think sql and the relational model is the wrong approach for web app data as they are inefficient for watching queries and syncing.
I'm using supabase which essentially autogenerates the entire server-side part. So all my logic is client side. What's missing is tighter integration between swrv/vue-swrv and the supabase client so that when you run a query it can be smart enough to know if you apply it locally or remotely.
I've been using Vuex-Pathify, which makes state management in Vue so, so much cleaner and friendlier than plain Vuex.
I know that redux has a nice guide here on how to make relationships but this library seems to make it an upfront concept. https://redux.js.org/usage/structuring-reducers/normalizing-...
As long as it has good Typescript support this could become very popular.
We did eventually add a `createEntityAdapter` API to Redux Toolkit [0] [1], which handles the process of storing items in normalized form and provides "CRUD"-style reducers for typical operations on that data, but it doesn't provide any support for managing relations specifically.
That topic _has_ come up a few times recently, so I've been contemplating that as a thing we might consider trying in a future version of RTK.
If folks have thoughts on what that API's requirements might look like, I'd be happy to start discussing that over in the RTK repo!
[0] https://redux-toolkit.js.org/api/createEntityAdapter
[1] https://redux.js.org/tutorials/essentials/part-6-performance...
I think for now you'll need to create your own local type definition that models the data you know is in the Store. But let me think a bit more about this.
Looking through the comment seems good to go. Thanks.
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 ┤ ╰──╯When not using React or Vue, people used to these conventions will not know how to properly pass state between objects. This library is for them. It really offers little more than `window.pets.dog.species = "poodle"` would offer but because it is a library it feels "less hacky".
So, you create a thing that creates more things that live in global variables that is impossible to untangle from all the other bits of code that create yet more global variables and end up depending on each other's global variables having particular names that are hard-coded left, right, and center. Basically, any attempt to test anything ends up firing most or all of the code at which point it's not a unit test but an integration test. If you need an entire browser running to test a thing, it's because it breaks this rule of separating glue code from business logic. Don't do that.
If you fix this, you can create stuff in a slightly different way in your tests (e.g. use stubs or mocks) and test them in isolation. And the same properties actually also make it easier to reuse things.
I've been using kotlin-js for the last year with a reactive web framework called fritz2 that is loosely inspired by React. One of the first things I did there was bring in koin, which is a dependency injection framework that many Android developers would know (similar to Dagger, which is a popular alternative). At startup time, koin ensures anything that has dependencies gets their dependencies and that those dependencies get created. Koin doesn't really do anything fancy or expensive. It just forces you to separate your plumbing and glue code from your business logic and makes that easier than doing it manually would be (which isn't all that hard, actually).
Implementing something like koin in javascript would not be hard and there are probably multiple npms that do that. And you can do this manually (it's called DIY dependency injection). But you need to know to do this and be a bit disciplined about using it. You can do this in almost any language actually and it's almost never a bad idea and almost always a mistake not to separate your glue and business logic. Most bigger projects get organized on this front at some point; or they just fail. That happens a lot with Javascript code written by developers that don't understand how to properly structure their code.
[1] https://tinybase.org/guides/relationships-and-checkpoints/us...
This is a stepping stone. A reactive, relational store is very useful, something I've been thinking about for a long time.
This allows you to wrap a batch of mutations together and then the fully reconciled change events are all fired together at the end. Apparently I could do a better job of marketing this secret feature :)
As another commenter has mentioned, there are also 'checkpoints' that let you create a stack of points in history to undo/redo back and forwards through. https://tinybase.org/guides/relationships-and-checkpoints/us...
Now, if we can get some autocomplete going on with those schemas, that would would be great when inevitably the global store becomes a giant blob.