Skip to content

feat(broadcastQueryClient): experimental support for tab/window syncing #15

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

Merged
merged 1 commit into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/src/pages/plugins/broadcastQueryClient.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
id: broadcastQueryClient
title: broadcastQueryClient (Experimental)
---

> VERY IMPORTANT: This utility is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases. Use at your own risk. If you choose to rely on this in production in an experimental stage, please lock your version to a patch-level version to avoid unexpected breakages.

`broadcastQueryClient` is a utility for broadcasting and syncing the state of your queryClient between browser tabs/windows with the same origin.

## Installation

This utility comes packaged with `svelte-query` and is available under the `@sveltestack/svelte-query` import.

## Usage

Import the `broadcastQueryClient` function, and pass it your `QueryClient` instance, and optionally, set a `broadcastChannel`.

```ts
import { broadcastQueryClient } from '@sveltestack/svelte-query'

const queryClient = new QueryClient()

broadcastQueryClient({
queryClient,
broadcastChannel: 'my-app',
})
```

## API

### `broadcastQueryClient`

Pass this function a `QueryClient` instance and optionally, a `broadcastChannel`.

```ts
broadcastQueryClient({ queryClient, broadcastChannel })
```

### `Options`

An object of options:

```ts
interface broadcastQueryClient {
/** The QueryClient to sync */
queryClient: QueryClient
/** This is the unique channel name that will be used
* to communicate between tabs and windows */
broadcastChannel?: string
}
```

The default options are:

```ts
{
broadcastChannel = 'svelte-query',
}
```
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@sveltestack/svelte-query",
"private": false,
"version": "1.1.0",
"version": "1.2.0",
"description": "Hooks for managing, caching and syncing asynchronous and remote data in Svelte",
"license": "MIT",
"svelte": "svelte/index.js",
Expand Down Expand Up @@ -38,6 +38,7 @@
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"@babel/preset-typescript": "^7.10.4",
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-node-resolve": "^6.0.0",
"@rollup/plugin-typescript": "^6.0.0",
"@storybook/addon-actions": "^6.0.26",
Expand Down Expand Up @@ -78,7 +79,7 @@
"prettier-plugin-svelte": "^1.4.0",
"react-is": "^16.13.1",
"replace": "^1.2.0",
"rollup": "^1.20.0",
"rollup": "^2.39.1",
"rollup-plugin-svelte": "^6.1.1",
"rollup-plugin-terser": "^5.3.0",
"svelte": "^3.29.0",
Expand All @@ -94,7 +95,9 @@
"svelte",
"react-query"
],
"dependencies": {},
"dependencies": {
"broadcast-channel": "^3.4.1"
},
"husky": {
"hooks": {
"pre-commit": "yarn test:ci"
Expand Down
4 changes: 3 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import svelte from 'rollup-plugin-svelte';
import autoPreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import pkg from './package.json';

Expand All @@ -22,6 +23,7 @@ export default {
preprocess: autoPreprocess()
}),
typescript(),
resolve()
resolve(),
commonjs()
]
};
89 changes: 89 additions & 0 deletions src/queryCore/broadcastQueryClient-experimental/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { BroadcastChannel } from 'broadcast-channel'
import { QueryClient } from '../core'

interface BroadcastQueryClientOptions {
queryClient: QueryClient
broadcastChannel: string
}

export function broadcastQueryClient({
queryClient,
broadcastChannel = 'svelte-query',
}: BroadcastQueryClientOptions) {
let transaction = false
const tx = (cb: () => void) => {
transaction = true
cb()
transaction = false
}

const channel = new BroadcastChannel(broadcastChannel, {
webWorkerSupport: false,
})

const queryCache = queryClient.getQueryCache()

queryClient.getQueryCache().subscribe(queryEvent => {
if (transaction || !queryEvent?.query) {
return
}

const {
query: { queryHash, queryKey, state },
} = queryEvent

if (
queryEvent.type === 'queryUpdated' &&
queryEvent.action?.type === 'success'
) {
channel.postMessage({
type: 'queryUpdated',
queryHash,
queryKey,
state,
})
}

if (queryEvent.type === 'queryRemoved') {
channel.postMessage({
type: 'queryRemoved',
queryHash,
queryKey,
})
}
})

channel.onmessage = action => {
if (!action?.type) {
return
}

tx(() => {
const { type, queryHash, queryKey, state } = action

if (type === 'queryUpdated') {
const query = queryCache.get(queryHash)

if (query) {
query.setState(state)
return
}

queryCache.build(
queryClient,
{
queryKey,
queryHash,
},
state
)
} else if (type === 'queryRemoved') {
const query = queryCache.get(queryHash)

if (query) {
queryCache.remove(query)
}
}
})
}
}
20 changes: 14 additions & 6 deletions src/queryCore/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ interface ContinueAction {
interface SetStateAction<TData, TError> {
type: 'setState'
state: QueryState<TData, TError>
setStateOptions?: SetStateOptions
}

export type Action<TData, TError> =
Expand All @@ -118,6 +119,10 @@ export type Action<TData, TError> =
| SetStateAction<TData, TError>
| SuccessAction<TData>

export interface SetStateOptions {
meta?: any
}

// CLASS

export class Query<
Expand Down Expand Up @@ -217,8 +222,11 @@ export class Query<
return data
}

setState(state: QueryState<TData, TError>): void {
this.dispatch({ type: 'setState', state })
setState(
state: QueryState<TData, TError>,
setStateOptions?: SetStateOptions
): void {
this.dispatch({ type: 'setState', state, setStateOptions })
}

cancel(options?: CancelOptions): Promise<void> {
Expand Down Expand Up @@ -290,7 +298,7 @@ export class Query<
// Stop the query from being garbage collected
this.clearGcTimeout()

this.cache.notify(this)
this.cache.notify({ type: 'observerAdded', query: this, observer })
}
}

Expand All @@ -316,7 +324,7 @@ export class Query<
}
}

this.cache.notify(this)
this.cache.notify({ type: 'observerRemoved', query: this, observer })
}
}

Expand Down Expand Up @@ -402,7 +410,7 @@ export class Query<
this.optionalRemove()
}
},
onError: error => {
onError: (error: TError | { silent?: boolean }) => {
// Optimistically update state if needed
if (!(isCancelledError(error) && error.silent)) {
this.dispatch({
Expand Down Expand Up @@ -452,7 +460,7 @@ export class Query<
observer.onQueryUpdate(action)
})

this.cache.notify(this)
this.cache.notify({ query: this, type: 'queryUpdated', action })
})
}

Expand Down
57 changes: 51 additions & 6 deletions src/queryCore/core/queryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import {
matchQuery,
parseFilterArgs,
} from './utils'
import { Query, QueryState } from './query'
import { Action, Query, QueryState } from './query'
import type { QueryKey, QueryOptions } from './types'
import { notifyManager } from './notifyManager'
import type { QueryClient } from './queryClient'
import { Subscribable } from './subscribable'
import { QueryObserver } from './queryObserver'

// TYPES

Expand All @@ -20,7 +21,48 @@ interface QueryHashMap {
[hash: string]: Query<any, any>
}

type QueryCacheListener = (query?: Query) => void
interface NotifyEventQueryAdded {
type: 'queryAdded'
query: Query<any, any>
}

interface NotifyEventQueryRemoved {
type: 'queryRemoved'
query: Query<any, any>
}

interface NotifyEventQueryUpdated {
type: 'queryUpdated'
query: Query<any, any>
action: Action<any, any>
}

interface NotifyEventObserverAdded {
type: 'observerAdded'
query: Query<any, any>
observer: QueryObserver<any, any, any, any>
}

interface NotifyEventObserverRemoved {
type: 'observerRemoved'
query: Query<any, any>
observer: QueryObserver<any, any, any, any>
}

interface NotifyEventObserverResultsUpdated {
type: 'observerResultsUpdated'
query: Query<any, any>
}

type QueryCacheNotifyEvent =
| NotifyEventQueryAdded
| NotifyEventQueryRemoved
| NotifyEventQueryUpdated
| NotifyEventObserverAdded
| NotifyEventObserverRemoved
| NotifyEventObserverResultsUpdated

type QueryCacheListener = (event?: QueryCacheNotifyEvent) => void

// CLASS

Expand Down Expand Up @@ -66,7 +108,10 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
if (!this.queriesMap[query.queryHash]) {
this.queriesMap[query.queryHash] = query
this.queries.push(query)
this.notify(query)
this.notify({
type: 'queryAdded',
query,
})
}
}

Expand All @@ -82,7 +127,7 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
delete this.queriesMap[query.queryHash]
}

this.notify(query)
this.notify({ type: 'queryRemoved', query })
}
}

Expand Down Expand Up @@ -127,10 +172,10 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
: this.queries
}

notify(query?: Query<any, any>) {
notify(event: QueryCacheNotifyEvent) {
notifyManager.batch(() => {
this.listeners.forEach(listener => {
listener(query)
listener(event)
})
})
}
Expand Down
4 changes: 3 additions & 1 deletion src/queryCore/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,9 @@ export class QueryObserver<

// Then the cache listeners
if (notifyOptions.cache) {
this.client.getQueryCache().notify(this.currentQuery)
this.client
.getQueryCache()
.notify({ query: this.currentQuery, type: 'observerResultsUpdated' })
}
})
}
Expand Down
Loading