Skip to content

Commit a073d83

Browse files
authored
feat(broadcastQueryClient): experimental support for tab/window syncing (#15)
1 parent fe675d4 commit a073d83

File tree

12 files changed

+346
-41
lines changed

12 files changed

+346
-41
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
id: broadcastQueryClient
3+
title: broadcastQueryClient (Experimental)
4+
---
5+
6+
> 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.
7+
8+
`broadcastQueryClient` is a utility for broadcasting and syncing the state of your queryClient between browser tabs/windows with the same origin.
9+
10+
## Installation
11+
12+
This utility comes packaged with `svelte-query` and is available under the `@sveltestack/svelte-query` import.
13+
14+
## Usage
15+
16+
Import the `broadcastQueryClient` function, and pass it your `QueryClient` instance, and optionally, set a `broadcastChannel`.
17+
18+
```ts
19+
import { broadcastQueryClient } from '@sveltestack/svelte-query'
20+
21+
const queryClient = new QueryClient()
22+
23+
broadcastQueryClient({
24+
queryClient,
25+
broadcastChannel: 'my-app',
26+
})
27+
```
28+
29+
## API
30+
31+
### `broadcastQueryClient`
32+
33+
Pass this function a `QueryClient` instance and optionally, a `broadcastChannel`.
34+
35+
```ts
36+
broadcastQueryClient({ queryClient, broadcastChannel })
37+
```
38+
39+
### `Options`
40+
41+
An object of options:
42+
43+
```ts
44+
interface broadcastQueryClient {
45+
/** The QueryClient to sync */
46+
queryClient: QueryClient
47+
/** This is the unique channel name that will be used
48+
* to communicate between tabs and windows */
49+
broadcastChannel?: string
50+
}
51+
```
52+
53+
The default options are:
54+
55+
```ts
56+
{
57+
broadcastChannel = 'svelte-query',
58+
}
59+
```

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@sveltestack/svelte-query",
33
"private": false,
4-
"version": "1.1.0",
4+
"version": "1.2.0",
55
"description": "Hooks for managing, caching and syncing asynchronous and remote data in Svelte",
66
"license": "MIT",
77
"svelte": "svelte/index.js",
@@ -38,6 +38,7 @@
3838
"@babel/core": "^7.9.0",
3939
"@babel/preset-env": "^7.9.0",
4040
"@babel/preset-typescript": "^7.10.4",
41+
"@rollup/plugin-commonjs": "^17.1.0",
4142
"@rollup/plugin-node-resolve": "^6.0.0",
4243
"@rollup/plugin-typescript": "^6.0.0",
4344
"@storybook/addon-actions": "^6.0.26",
@@ -78,7 +79,7 @@
7879
"prettier-plugin-svelte": "^1.4.0",
7980
"react-is": "^16.13.1",
8081
"replace": "^1.2.0",
81-
"rollup": "^1.20.0",
82+
"rollup": "^2.39.1",
8283
"rollup-plugin-svelte": "^6.1.1",
8384
"rollup-plugin-terser": "^5.3.0",
8485
"svelte": "^3.29.0",
@@ -94,7 +95,9 @@
9495
"svelte",
9596
"react-query"
9697
],
97-
"dependencies": {},
98+
"dependencies": {
99+
"broadcast-channel": "^3.4.1"
100+
},
98101
"husky": {
99102
"hooks": {
100103
"pre-commit": "yarn test:ci"

rollup.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import svelte from 'rollup-plugin-svelte';
22
import autoPreprocess from 'svelte-preprocess';
33
import typescript from '@rollup/plugin-typescript';
44
import resolve from '@rollup/plugin-node-resolve';
5+
import commonjs from '@rollup/plugin-commonjs';
56
import { terser } from 'rollup-plugin-terser';
67
import pkg from './package.json';
78

@@ -22,6 +23,7 @@ export default {
2223
preprocess: autoPreprocess()
2324
}),
2425
typescript(),
25-
resolve()
26+
resolve(),
27+
commonjs()
2628
]
2729
};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { BroadcastChannel } from 'broadcast-channel'
2+
import { QueryClient } from '../core'
3+
4+
interface BroadcastQueryClientOptions {
5+
queryClient: QueryClient
6+
broadcastChannel: string
7+
}
8+
9+
export function broadcastQueryClient({
10+
queryClient,
11+
broadcastChannel = 'svelte-query',
12+
}: BroadcastQueryClientOptions) {
13+
let transaction = false
14+
const tx = (cb: () => void) => {
15+
transaction = true
16+
cb()
17+
transaction = false
18+
}
19+
20+
const channel = new BroadcastChannel(broadcastChannel, {
21+
webWorkerSupport: false,
22+
})
23+
24+
const queryCache = queryClient.getQueryCache()
25+
26+
queryClient.getQueryCache().subscribe(queryEvent => {
27+
if (transaction || !queryEvent?.query) {
28+
return
29+
}
30+
31+
const {
32+
query: { queryHash, queryKey, state },
33+
} = queryEvent
34+
35+
if (
36+
queryEvent.type === 'queryUpdated' &&
37+
queryEvent.action?.type === 'success'
38+
) {
39+
channel.postMessage({
40+
type: 'queryUpdated',
41+
queryHash,
42+
queryKey,
43+
state,
44+
})
45+
}
46+
47+
if (queryEvent.type === 'queryRemoved') {
48+
channel.postMessage({
49+
type: 'queryRemoved',
50+
queryHash,
51+
queryKey,
52+
})
53+
}
54+
})
55+
56+
channel.onmessage = action => {
57+
if (!action?.type) {
58+
return
59+
}
60+
61+
tx(() => {
62+
const { type, queryHash, queryKey, state } = action
63+
64+
if (type === 'queryUpdated') {
65+
const query = queryCache.get(queryHash)
66+
67+
if (query) {
68+
query.setState(state)
69+
return
70+
}
71+
72+
queryCache.build(
73+
queryClient,
74+
{
75+
queryKey,
76+
queryHash,
77+
},
78+
state
79+
)
80+
} else if (type === 'queryRemoved') {
81+
const query = queryCache.get(queryHash)
82+
83+
if (query) {
84+
queryCache.remove(query)
85+
}
86+
}
87+
})
88+
}
89+
}

src/queryCore/core/query.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ interface ContinueAction {
106106
interface SetStateAction<TData, TError> {
107107
type: 'setState'
108108
state: QueryState<TData, TError>
109+
setStateOptions?: SetStateOptions
109110
}
110111

111112
export type Action<TData, TError> =
@@ -118,6 +119,10 @@ export type Action<TData, TError> =
118119
| SetStateAction<TData, TError>
119120
| SuccessAction<TData>
120121

122+
export interface SetStateOptions {
123+
meta?: any
124+
}
125+
121126
// CLASS
122127

123128
export class Query<
@@ -217,8 +222,11 @@ export class Query<
217222
return data
218223
}
219224

220-
setState(state: QueryState<TData, TError>): void {
221-
this.dispatch({ type: 'setState', state })
225+
setState(
226+
state: QueryState<TData, TError>,
227+
setStateOptions?: SetStateOptions
228+
): void {
229+
this.dispatch({ type: 'setState', state, setStateOptions })
222230
}
223231

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

293-
this.cache.notify(this)
301+
this.cache.notify({ type: 'observerAdded', query: this, observer })
294302
}
295303
}
296304

@@ -316,7 +324,7 @@ export class Query<
316324
}
317325
}
318326

319-
this.cache.notify(this)
327+
this.cache.notify({ type: 'observerRemoved', query: this, observer })
320328
}
321329
}
322330

@@ -402,7 +410,7 @@ export class Query<
402410
this.optionalRemove()
403411
}
404412
},
405-
onError: error => {
413+
onError: (error: TError | { silent?: boolean }) => {
406414
// Optimistically update state if needed
407415
if (!(isCancelledError(error) && error.silent)) {
408416
this.dispatch({
@@ -452,7 +460,7 @@ export class Query<
452460
observer.onQueryUpdate(action)
453461
})
454462

455-
this.cache.notify(this)
463+
this.cache.notify({ query: this, type: 'queryUpdated', action })
456464
})
457465
}
458466

src/queryCore/core/queryCache.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import {
44
matchQuery,
55
parseFilterArgs,
66
} from './utils'
7-
import { Query, QueryState } from './query'
7+
import { Action, Query, QueryState } from './query'
88
import type { QueryKey, QueryOptions } from './types'
99
import { notifyManager } from './notifyManager'
1010
import type { QueryClient } from './queryClient'
1111
import { Subscribable } from './subscribable'
12+
import { QueryObserver } from './queryObserver'
1213

1314
// TYPES
1415

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

23-
type QueryCacheListener = (query?: Query) => void
24+
interface NotifyEventQueryAdded {
25+
type: 'queryAdded'
26+
query: Query<any, any>
27+
}
28+
29+
interface NotifyEventQueryRemoved {
30+
type: 'queryRemoved'
31+
query: Query<any, any>
32+
}
33+
34+
interface NotifyEventQueryUpdated {
35+
type: 'queryUpdated'
36+
query: Query<any, any>
37+
action: Action<any, any>
38+
}
39+
40+
interface NotifyEventObserverAdded {
41+
type: 'observerAdded'
42+
query: Query<any, any>
43+
observer: QueryObserver<any, any, any, any>
44+
}
45+
46+
interface NotifyEventObserverRemoved {
47+
type: 'observerRemoved'
48+
query: Query<any, any>
49+
observer: QueryObserver<any, any, any, any>
50+
}
51+
52+
interface NotifyEventObserverResultsUpdated {
53+
type: 'observerResultsUpdated'
54+
query: Query<any, any>
55+
}
56+
57+
type QueryCacheNotifyEvent =
58+
| NotifyEventQueryAdded
59+
| NotifyEventQueryRemoved
60+
| NotifyEventQueryUpdated
61+
| NotifyEventObserverAdded
62+
| NotifyEventObserverRemoved
63+
| NotifyEventObserverResultsUpdated
64+
65+
type QueryCacheListener = (event?: QueryCacheNotifyEvent) => void
2466

2567
// CLASS
2668

@@ -66,7 +108,10 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
66108
if (!this.queriesMap[query.queryHash]) {
67109
this.queriesMap[query.queryHash] = query
68110
this.queries.push(query)
69-
this.notify(query)
111+
this.notify({
112+
type: 'queryAdded',
113+
query,
114+
})
70115
}
71116
}
72117

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

85-
this.notify(query)
130+
this.notify({ type: 'queryRemoved', query })
86131
}
87132
}
88133

@@ -127,10 +172,10 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
127172
: this.queries
128173
}
129174

130-
notify(query?: Query<any, any>) {
175+
notify(event: QueryCacheNotifyEvent) {
131176
notifyManager.batch(() => {
132177
this.listeners.forEach(listener => {
133-
listener(query)
178+
listener(event)
134179
})
135180
})
136181
}

src/queryCore/core/queryObserver.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,9 @@ export class QueryObserver<
615615

616616
// Then the cache listeners
617617
if (notifyOptions.cache) {
618-
this.client.getQueryCache().notify(this.currentQuery)
618+
this.client
619+
.getQueryCache()
620+
.notify({ query: this.currentQuery, type: 'observerResultsUpdated' })
619621
}
620622
})
621623
}

0 commit comments

Comments
 (0)