Open alecgibson opened 1 year ago
From discussion today:
submitBatch
option could provide the mutable doc data for you, which prevents issues with forgetting to handle op submission errors that would cause unhandled errors / promise rejectionsI think for simple scenarios, proxy can avoid using json0 to construct op object, but for some cases, there will be problems. For example, if I have an array and I want to swap the position of 1,3, maybe use
tmp = data[3];
data[3] = data[1];
data[1] = tmp;
In this case we cannot identify whether to swap the order of the array or to assign values to two elements.
@Qquanwei yes I agree; I've stated that in the "Disadvantages" section. I think for things like this, consumers would be expected to use the "traditional" submitOp()
machinery they want specific semantics.
Introduction
Mutating objects in
sharedb
is not necessarily the most intuitive thing to do, which is a shame for a library whose purpose is to mutate objects.It often requires in-depth understanding of the types being used (by default
json0
) and how to construct ops for that type.For users of TypeScript, op construction like this also removes type safety, and autocompletion suggestions.
For example, consider a simple
json0
document:To mutate this, we need to consult the
json0
docs, and then manually assemble an op:As well as being cumbersome to write (and to read!), this can also be error-prone. If we're using TypeScript, we run in to these potentially avoidable errors:
Proposal
I'm proposing extension of the
sharedb
API to allow for a more fluent syntax, where consumers can (seemingly) mutatedoc.data
directly:This update would be far easier for consumers to use, and would keep TypeScript checks.
It would also stop consumers from accidentally mutating the
doc.data
object (since this is now allowed!).Implementation
For a fully-worked
json0
example, please see below.The basic concept is to use JavaScript
Proxy
s to convert setter calls into ops.Since conversion into ops is type-dependent, the implementation would belong in the types. We could potentially add an optional method to the type spec:
accessor()
, which returns a proxy object which may be "mutated" directly and instead submits ops.If we want the type to be unaware of the
Doc
class (probably for the best), this may be done by having theaccessor
emit events that theDoc
can subscribe to and then submit.If we wanted to use super-modern JS features, we could potentially use
for await...of
and have the doc asynchronously iterate ops from theaccessor
.Acknowledgement & error handling
Since consumers don't directly call
submit()
with this API, they also won't have an opportunity to register an error handler. For simple use cases, this may be "good enough" — ShareDB will still emit anerror
event if anything goes wrong, which can either be handled or leaved to let Node.js processes crash.However, consumers may also want to wait for the op to be acknowledged by the server for some reason (eg UI updates; performing a dependent task; etc.). They may also want to handle errors on a per-op basis (especially if they want sensible stack traces).
Since setters aren't asynchronous, this would require addition of some mechanism to capture errors from these
accessor
updates.One possible way is to just add something like:
Since ops will only be submitted in the next event loop tick, we can synchronously queue a number of changes, and then synchronously attach a callback:
We could alternatively add some sort of helper method that will actively batch ops and attach a callback. Consumer code might look like:
Disadvantages
accessor()
lm
instead ofli
+ld
; usingna
instead ofoi
+od
; etc.)Proxy
was only finalised in the ES6 spec (and we currently target ES3)json0
exampleBelow is a basic example of what this might look like for
json0
.The main complications come about from handling arrays (we manually override a number of array methods, taken from Vue's reactivity docs. There are also some corner cases around creating sparse arrays, which
json0
doesn't support.