Skip to content
This repository has been archived by the owner on Mar 26, 2021. It is now read-only.

Implement simple subscriptions #6

Closed
aboodman opened this issue Feb 21, 2020 · 5 comments
Closed

Implement simple subscriptions #6

aboodman opened this issue Feb 21, 2020 · 5 comments

Comments

@aboodman
Copy link
Contributor

Subscriptions are super compelling user feature. It can get arbitrarily fancy on the implementation side, but an initial cut is easy. See https://github.com/rocicorp/replicache/blob/master/setup-flutter.md#5-read-data for the aspirational UI.

@aboodman aboodman added this to the First Flutter Read-Only Customers milestone Mar 27, 2020
@aboodman
Copy link
Contributor Author

Here's a new aspirational API sketch:

const rep = Replicache(...);
rep.register("getTodos", (int listId) {
  // this can be arbitrarily fancy involving multiple reads.
  return rep.scan("/todo").filter(...);
});

register is how you add a named transaction to a Replicache instance. They are just held in memory.

Later on, you can invoke them:

// Note that we need to disallow queries from calling put() or del().
let result = await rep.query("getTodos", 42);

... or subscribe to them:

// subscriptions should also be read-only
let stream = await rep.subscribe("getTodos", 42);

Transactions are also how you do the local side of mutations:

rep.mutate(
  "createTodo", [args],
  "/create-todo", args) 

@arv
Copy link
Contributor

arv commented Mar 27, 2020

I think we should register both local and remote and then the invoke deals with calling both local and remote. Strawman:

final createTodo = rep.register(local: (String text, double order, bool complete) {
  rep.put(newid('/todo/'), {'text': text, 'order': order, 'complete': complete});
}, remote: ???);

...

createTodo('Check me', 123, false);

(more thinking about the ??? needed)

@aboodman
Copy link
Contributor Author

I had not thought about returning a calling function, that's nice. However, the string name is needed particularly for mutations, because Replicache will call these functions (by name) during sync to rebase history. It needs to be able to look them up by name.

So we could do:

final createTodo = rep.register('create-todo', local: (...) { }, remote: (...) {})

As for your idea for registering the remote handler, I could expand that to:

final createTodo = rep.register('create-todo',
  local: (String text, double order, bool complete) {
    rep.put(newid('/todo/'), {'text': text, 'order': order, 'complete': complete});
  },
  remote: (String text, double order, bool complete) {
    return ['/create-todo', {'title': text, 'order': order, ...}];
  },
);

There's still some boilerplate in here, but I think it is important to allow the signatures of the remote and local impls to diverge freely. Also, I don't think it's the case that the payload to the remote will always be JSON.

@arv
Copy link
Contributor

arv commented Mar 27, 2020

Dart is not very expressive in this area. It does not support rest parameters so the returned function would have to take a List but I guess that is fine for now.

@aboodman
Copy link
Contributor Author

aboodman commented Mar 27, 2020

We talked about the design on VC a bit. There are two different ways, broadly to approach how to implement invalidating and replaying subscriptions.

===

In alternative 1, we push more of the smarts down into Go. This requires drawing some kind of protocol between Go and the bindings where the bindings tell Go when a subscription is starting:

// Go interface psuedocode
StartSubscription(id)
...
Get()
Scan()
...
EndSubscription(id)

Then, later, as a result of any call that might change Replicache, we return the subscriptions that should be updated:

Sync() ==> changedSubs := {42,19}
Put() ==> changedSubs := {42,19}
Exec() ==> changedSubs := {42,19}

This would replace or extend the current rootHash values that are returned from these functions and are used to implement onChange in the bindings.

===

In alternative two, we leave the Go layer the same and implement subscriptions purely in Dart. We invalidate subscriptions when the root hash of Replicache changes, re-run all subscriptions and maybe use a hash function to avoid calling user code if the result doesn't really change.

===

Alternative 1 has the advantage of admitting many more optimizations (tracking dependent data of subscriptions and only re-running when it changes), and also sharing code between different bindings impls.

But alternative 1 also imposes a lot of development overhead at the beginning when we are figuring out the best shape of these interfaces.

Both @arv and my instinct is to start with alt 2 and push more into Go as we learn more and/or have more bindings impls and/or need it to go faster.

The final decision will be left to @arv as he gets more into it.

@aboodman aboodman removed this from the First Flutter Read-Only Customers milestone Apr 8, 2020
@aboodman aboodman closed this as completed Apr 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants