Open aboodman opened 2 years ago
I think the onRaw
is quite important. There are also in react land several use cases to have an imperative API for data changes:
For me having the access to an imperative API via the experimental watch was great not only due to performance characteristics, but also due to the flexibility of the API.
Good point @KeKs0r - thanks for the feedback.
Somewhere along the line we lost the fact that when watch
fires it has to communicate the root hash of the database somewhere. This is needed for one of the original motivations of this bug -- keeping some external storage (e.g., a text index) up to date correctly. This can be an extra param to onResult
and onChange
. See https://github.com/rocicorp/replicache/issues/839 for the background.
curious about what stage this is at
I'm currently debating between replicache and rxdb and this is the feature that it's coming down to
We actually already have experimentalWatch: https://doc.replicache.dev//api/classes/Replicache. It’s deployed in current release. The new api will be a superset of this so you can start using it now.
watch
is very much appreciated. Below you'll find the result of some internal training for implementing replicache as vue3 store. In this case taskList
is exported as reactive javascript Map(). subscribe
basically triggers re-render check for all items, while we can limit that to changes only with watch
- hope this will make it's way to stable api
// ./states/tasks.js
import { ref } from 'vue'
import { Replicache } from 'replicache'
import { id as newId } from '@rnvf/id-generator'
export const taskList = ref(new Map())
const rep = new Replicache({
// eslint-disable-next-line no-undef
licenseKey: import.meta.env.VITE_REPLICACHE_LICENSE_KEY,
name: `tasks`,
pushURL: '/api/tasks/push',
pullURL: '/api/tasks/pull',
mutators: {
async addTask(tx, task) {
const id = newId('task')
await tx.put(`task/${id}`, { ...task, id, done: false })
},
async toggleTask(tx, task) {
await tx.put(`task/${task.id}`, { ...task, done: !task.done })
},
async removeTask(tx, task) {
await tx.del(`task/${task.id}`)
}
}
})
/**
* stable but rerenders whole list (on each change)
*/
rep.subscribe(async (tx) => tx.scan({ prefix: 'task/' }).entries().toArray(), {
onData(data) {
// used for initial load only
if (taskList.value.size <= 0) {
taskList.value = new Map(data)
}
}
})
/**
* experimental but rerenders only changed items
*/
rep.experimentalWatch(
(diff) => {
for (const { op, key, newValue } of diff) {
if (op !== 'del') {
taskList.value.set(key, newValue)
} else {
taskList.value.delete(key)
}
}
},
{
prefix: 'task/'
// initialValuesInFirstDiff: true // might have performance impact, too
}
)
export const addTask = rep.mutate.addTask
export const removeTask = rep.mutate.removeTask
export const toggleTask = rep.mutate.toggleTask
only one thing left for testing: Is list = new Map(data)
better than data.forEach((d) => list.set(d.id, d))
?
Very cool @mashpie - is there anything missing from watch for your use in Vue?
Thanks @aboodman - so far I don't miss anything but time to play and setup further examples ;)
Replicache UIs are currently built using the
subscribe()
method.The idea of subscribe is that it is an arbitrary function of a Replicache state that can return any
JSONValue
as a result. Since it is an arbitrary function there's a limit to how far it can be optimized. For example, the overwhelmingly most common subscription is just getting all entries with a particular key prefix:We can optimize this subscription to only run when one of the accessed keys is modified (and we do currently do this). But since we can't know what the function does with that data, we are forced to run the function again each time it is invalidated, in its entirety.
This sucks since in this common case, Replicache knows how those keys changed and so it could in theory fix up the returned data surgically without re-running the scan.
We plan to support this common use case efficiently by introducing a new
watch()
method that looks something like: