Skip to content

Commit 6eb8d10

Browse files
committed
feat(apollo-client): cache subscriptions
1 parent 6250cda commit 6eb8d10

File tree

2 files changed

+123
-7
lines changed

2 files changed

+123
-7
lines changed
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export default function pathObject(path: string, obj: any) {
2+
if (!path) return obj
3+
4+
const splitPaths = path.split(".")
5+
6+
let pathedObj = obj
7+
8+
for (let i = 0; i < splitPaths.length; i++) {
9+
const curPath = splitPaths[i]
10+
pathedObj = pathedObj[curPath]
11+
12+
if (i < splitPaths.length - 1 && typeof pathedObj !== "object") {
13+
pathedObj = undefined
14+
break
15+
}
16+
}
17+
18+
return pathedObj
19+
}

Diff for: lib/reactotron-apollo-client/src/reactotron-apollo-client.ts

+104-7
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@ import {
77
assertHasLoggerPlugin,
88
InferFeatures,
99
LoggerPlugin,
10+
assertHasStateResponsePlugin,
11+
StateResponsePlugin,
1012
} from "reactotron-core-client"
11-
13+
import type { Command } from "reactotron-core-contract"
1214
import type { DocumentNode, NormalizedCacheObject } from "@apollo/client"
1315
import { getOperationName } from "@apollo/client/utilities"
1416
import type { QueryInfo } from "@apollo/client/core/QueryInfo"
1517

1618
import type { ASTNode } from "graphql"
1719
import { print } from "graphql"
1820

21+
import { flatten, uniq } from "ramda"
22+
import pathObject from "./helpers/pathObject"
23+
1924
type ApolloClientType = ApolloClient<NormalizedCacheObject>
2025

2126
type Variables = QueryInfo["variables"]
@@ -206,31 +211,123 @@ function debounce(func: (...args: any) => any, timeout = 500): () => any {
206211
}
207212
}
208213

209-
interface ApolloPluginConfig {
214+
export interface ApolloPluginConfig {
210215
apolloClient: ApolloClient<NormalizedCacheObject>
211216
}
212217

213-
const apolloPlugin =
218+
export const apolloPlugin =
214219
(options: ApolloPluginConfig) =>
215220
<Client extends ReactotronCore>(reactotronClient: Client) => {
216221
const { apolloClient } = options
217222
assertHasLoggerPlugin(reactotronClient)
218-
const reactotron = reactotronClient as unknown as ReactotronCore &
219-
InferFeatures<ReactotronCore, LoggerPlugin>
223+
assertHasStateResponsePlugin(reactotronClient)
224+
const reactotron = reactotronClient as Client &
225+
InferFeatures<Client, LoggerPlugin> &
226+
InferFeatures<Client, StateResponsePlugin>
227+
228+
// --- Plugin-scoped variables ---------------------------------
229+
230+
// hang on to the apollo state
231+
let apolloData = { cache: {}, queries: {}, mutations: {} }
232+
233+
// a list of subscriptions the client is subscribing to
234+
let subscriptions: string[] = []
235+
236+
function subscribe(command: Command<"state.values.subscribe">) {
237+
const paths: string[] = (command && command.payload && command.payload.paths) || []
238+
239+
if (paths) {
240+
// TODO ditch ramda
241+
subscriptions = uniq(flatten(paths))
242+
}
243+
244+
sendSubscriptions()
245+
}
246+
247+
function getChanges() {
248+
// TODO also check if cache state is empty
249+
if (!reactotron) return []
250+
251+
const changes = []
252+
253+
const state = apolloData.cache
254+
255+
subscriptions.forEach((path) => {
256+
let cleanedPath = path
257+
let starredPath = false
258+
259+
if (path && path.endsWith("*")) {
260+
// Handle the star!
261+
starredPath = true
262+
cleanedPath = path.substring(0, path.length - 2)
263+
}
264+
265+
const values = pathObject(cleanedPath, state)
266+
267+
if (starredPath && cleanedPath && values) {
268+
changes.push(
269+
...Object.entries(values).map((val) => ({
270+
path: `${cleanedPath}.${val[0]}`,
271+
value: val[1],
272+
}))
273+
)
274+
} else {
275+
changes.push({ path: cleanedPath, value: state[cleanedPath] })
276+
}
277+
})
278+
279+
return changes
280+
}
281+
282+
function sendSubscriptions() {
283+
const changes = getChanges()
284+
reactotron.stateValuesChange(changes)
285+
}
286+
287+
// --- Reactotron Hooks ---------------------------------
288+
289+
// maps inbound commands to functions to run
290+
// TODO clear cache command?
291+
const COMMAND_MAP = {
292+
"state.values.subscribe": subscribe,
293+
} satisfies { [name: string]: (command: Command) => void }
294+
295+
/**
296+
* Fires when we receive a command from the reactotron app.
297+
*/
298+
function onCommand(command: Command) {
299+
// lookup the command and execute
300+
const handler = COMMAND_MAP[command && command.type]
301+
handler && handler(command)
302+
}
303+
304+
// --- Reactotron plugin interface ---------------------------------
220305

221306
return {
307+
// Fires when we receive a command from the Reactotron app.
308+
onCommand,
309+
222310
onConnect() {
223-
reactotron.log("Apollo Client Connected")
311+
reactotron.display({ name: "APOLLO CLIENT", preview: "Connected" })
312+
224313
const poll = () =>
225314
getCurrentState(apolloClient).then((state) => {
315+
apolloData = state
316+
317+
sendSubscriptions()
318+
226319
reactotron.display({
227320
name: "APOLLO CLIENT",
228-
preview: `Apollo client updated at ${state.lastUpdateAt}`,
321+
preview: `State Updated`,
229322
value: state,
230323
})
231324
})
232325
apolloClient.__actionHookForDevTools(debounce(poll))
233326
},
327+
onDisconnect() {
328+
// Does this do anything? How do we clean up?
329+
apolloClient.__actionHookForDevTools(null)
330+
},
234331
} satisfies Plugin<Client>
235332
}
236333

0 commit comments

Comments
 (0)