Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement live query on @orbit/record-cache #718

Merged
merged 1 commit into from Mar 7, 2020

Conversation

tchak
Copy link
Member

@tchak tchak commented Dec 22, 2019

This a new proposition to introduce query observers. This one is not relying on any known interface such as RX or async iterator. It uses orbit events. It does expose an LiveQuery interface, but it is simply an object with a subscribe method with returns a subscription with an unsubscribe method.

The current one has a downside of looking like an RX/Zen Observable but not being one. I am worried it might confuse folks. One option I considered is to pass the callback directly to liveQuery method, but it makes it for a complex signature because of optional options and id arguments...

What do you think @dgeb?

@tchak
Copy link
Member Author

tchak commented Dec 23, 2019

I changed QueryObserver interface from

interface QueryObserverSubscription {
  unsubscribe(): void;
}

interface QueryObserver {
  subscribe(listener: Listener): QueryObserverSubscription
}

to

interface LiveQuery {
  on(listener: (result: QueryResult) => void): () => void
}

so it is more in line with orbit vocabulary and less prompt to create confusion with an actual observable

@tchak tchak requested a review from dgeb December 23, 2019 13:48
@tchak
Copy link
Member Author

tchak commented Dec 23, 2019

I am not sure about change event. Maybe I should just use recordOperationChange in the QueryObserver implementation.

@tchak
Copy link
Member Author

tchak commented Dec 23, 2019

Right now I added bits all around code base. Maybe it all should be just part of a new @obit/query-observer package used by @orbit/memory to expose observeQuery.

@tchak
Copy link
Member Author

tchak commented Dec 23, 2019

Also maybe observeQuery should live in the record-cache package

@selvagsz
Copy link

@tchak @dgeb sorry for being too naive here. But, what is a "live query" or "query observer" ?

@tchak
Copy link
Member Author

tchak commented Dec 24, 2019

@selvagsz it is a way to observe a query result. Example:

memory.cache.liveQuery(q => q.findRecords('planet')).on((planets => {
  console.log('>', planets);
});

memory.update(t => t.addRecord({ type: 'planet', id: 'earth' }));
memory.update(t => t.addRecord({ type: 'planet', id: 'mars' }));

// will print:
// > []
// > [{ type: 'planet', id: 'earth' }]
// > [{ type: 'planet', id: 'earth' }, { type: 'planet', id: 'mars' }]

@tchak
Copy link
Member Author

tchak commented Dec 24, 2019

I went ahead and moved everything to @orbit/record-cache

@tchak tchak force-pushed the observe-query branch 4 times, most recently from 513e8af to 9e9414c Compare December 25, 2019 14:00
@tchak tchak changed the title [RFC] implement observeQuery on memory cache [RFC] implement live query on @orbit/record-cache Dec 25, 2019
@tchak
Copy link
Member Author

tchak commented Dec 25, 2019

I am getting quite happy with this. The main concern I have is error handling. I am dealing with RecordNotFound exceptions in a meaningful way by returning empty data. I think this is what one might expect on a live query. But all other errors are just thrown into the void. We might want to add a second error callback.

I guess I am just a bit unhappy that we are basically reinventing Observable interface...

@selvagsz
Copy link

@tchak where can I find the motivation or use case for this feature ?

@tchak
Copy link
Member Author

tchak commented Dec 26, 2019

For prior art you can check:

If we want to implement any reactive functionality on top of orbit (like a react hook) we would need this.

@tchak
Copy link
Member Author

tchak commented Dec 26, 2019

@selvagsz I can see you have implemented some react hooks: https://github.com/selvagsz/react-orbit-example/blob/master/src/hooks/useQuery.js
This addition will make such implementations easier and more reliable. Subscribing to transform is a way too low level api.

@tchak tchak force-pushed the observe-query branch 2 times, most recently from 0d3c72b to e1c7928 Compare December 26, 2019 18:22
@tchak
Copy link
Member Author

tchak commented Dec 26, 2019

added proper error handling

@tchak tchak force-pushed the observe-query branch 2 times, most recently from 27a1fef to e3ed0fc Compare December 26, 2019 19:18
@selvagsz
Copy link

selvagsz commented Dec 27, 2019

@tchak I was re-implementing the react orbit hooks on top of this PR. I hit 2 issues,

  1. When the findRecords query is resolved, the liveQuery listener is called for each transform operation. For eg., if the response resulted in 50 records, the listener is called 50 times because of listening to the patch event

  2. When the findRecords response contains side-loaded (included) records, source.cache.query(t => t.findRelatedRecords) results in RecordNotFoundException even though the record being present in the included key in the response.

remove: boolean;
}

export function recordOperationChange(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tchak this impl. is 🔥🔥🔥 Was able to get an optimal implementation of react-orbit hook with this record change detection on the source's transform event instead of cache's patch event.

@tchak
Copy link
Member Author

tchak commented Dec 27, 2019

Thank you for giving it a go!

I definitely remember @dgeb explaining in some issue why listening to transform is probably not what we want. I think we might miss some changes.

When the findRecords query is resolved, the liveQuery listener is called for each transform operation. For eg., if the response resulted in 50 records, the listener is called 50 times because of listening to the patch event

Yeah, this is going to be a problem... I think we can fix this with some sort of async coalescing. I am also pretty sure that the whole "dirty check" logic in LiveQuery can be made better.

When the findRecords response contains side-loaded (included) records, source.cache.query(t => t.findRelatedRecords) results in RecordNotFoundException even though the record being present in the included key in the response.

Not sure what's going on here

@tchak
Copy link
Member Author

tchak commented Dec 27, 2019

Ideally I think a QueryExpression should be serialisable as some sort of cache key. It would make "dirty checking" and coalescing easier.

@tchak tchak force-pushed the observe-query branch 2 times, most recently from 97a5dc4 to e29d57a Compare December 29, 2019 23:49
@tchak
Copy link
Member Author

tchak commented Dec 29, 2019

@selvagsz I think I fixed the over fetching issue. I will add more tests tomorrow to be sure :)

@selvagsz
Copy link

selvagsz commented Dec 30, 2019

@tchak Can confirm. Over-firing of the listener is fixed with the tick change. The other issue related to RecordNotFoundException in the listener of liveQuery also got fixed 🎉

executeQuery();
});

executeQuery();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tchak any reason to immediately execute the listener ? Does it make sense to add an api for this similar to ember debounce method debounce (target, method, args*, wait, immediate)

Copy link
Member Author

@tchak tchak Dec 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made another change to the interface. So first execution needs to be explicit now.

const subscription = store.cache
  .liveQuery(q => q.findRecords('planet'))
  .subscribe(fn);

subscription.execute();
subscribe(): LiveQuerySubscription;

interface LiveQuerySubscription {
  unsubscribe(): void;
  execute(): void;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about the store.cache usage here, does this mean that liveQuery is only available on the local cache, not the actual store? And would this mean that a coordinator strategy that auto-pulls data from a remote would not be part of this liveQuery process?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

liveQuery as defined in this PR is a cache feature not a source feature. I would like to have a conversation about remote subscriptions, but it is a quite different topic. Regarding a setup with a remote source – if your remote source is syncing back to memory source 'liveQuery' will pick up changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, thanks for explaining! Our team is currently looking at orbitjs as a backing store for our json:api connected react application. One thing I'd like to solve is auto-pull of missing data, together with a reactive connection to the query that triggered the pull. It seems like this is not quite there yet - We'll continue to watch this space :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so would it help you if in the query/pull flow there was a subscribe/unsubscribe event on the source? Or something similar. What is your server implementation like? Is your server able to push diff operations for a registered query?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example in one app I am trying to use something like this : https://gist.github.com/tchak/2a23843c35617a265e75c136afc99b77

@selvagsz selvagsz mentioned this pull request Dec 30, 2019
@tchak tchak force-pushed the observe-query branch 2 times, most recently from dd7ccfe to 9c2551e Compare December 30, 2019 11:43
@tchak tchak marked this pull request as ready for review January 5, 2020 22:09
@tchak tchak force-pushed the observe-query branch 3 times, most recently from 1251e92 to 18a4b56 Compare March 1, 2020 21:35
@tchak tchak changed the title [RFC] implement live query on @orbit/record-cache Implement live query on @orbit/record-cache Mar 2, 2020
Copy link
Member

@dgeb dgeb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tchak great work! I really like the new interfaces we settled on.

IMO this just needs some meta and links handling, but that doesn't have to happen in this PR (your choice).

RecordOperation
} from '@orbit/data';

export interface RecordChange extends RecordIdentity {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll also need to track changes to meta and links (which are currently only possible via addRecord / updateRecord ops).


switch (operation.op) {
case 'addRecord':
case 'updateRecord':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for record.meta and record.links.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am realizing that current implementation of change matching is not using attributes or keys (nor meta or links). Right now the algorithm is just to check if the identity or relationship identity is the same, we consider change relevant. So in practice we could only expose identity, relationships, and removed in the RecordChange interface. But we also may improuve the algorith in the future and use extra information.

@tchak tchak force-pushed the observe-query branch 3 times, most recently from ae57ed6 to 21918a2 Compare March 6, 2020 15:02
@dgeb dgeb merged commit 89c9387 into orbitjs:master Mar 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants