Skip to content

Commit

Permalink
feat(apollo-client): cache subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
frankcalise committed Oct 17, 2023
1 parent 6250cda commit 6eb8d10
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 7 deletions.
19 changes: 19 additions & 0 deletions lib/reactotron-apollo-client/src/helpers/pathObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default function pathObject(path: string, obj: any) {
if (!path) return obj

const splitPaths = path.split(".")

let pathedObj = obj

for (let i = 0; i < splitPaths.length; i++) {
const curPath = splitPaths[i]
pathedObj = pathedObj[curPath]

if (i < splitPaths.length - 1 && typeof pathedObj !== "object") {
pathedObj = undefined
break
}
}

return pathedObj
}
111 changes: 104 additions & 7 deletions lib/reactotron-apollo-client/src/reactotron-apollo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import {
assertHasLoggerPlugin,
InferFeatures,
LoggerPlugin,
assertHasStateResponsePlugin,
StateResponsePlugin,
} from "reactotron-core-client"

import type { Command } from "reactotron-core-contract"
import type { DocumentNode, NormalizedCacheObject } from "@apollo/client"
import { getOperationName } from "@apollo/client/utilities"
import type { QueryInfo } from "@apollo/client/core/QueryInfo"

import type { ASTNode } from "graphql"
import { print } from "graphql"

import { flatten, uniq } from "ramda"
import pathObject from "./helpers/pathObject"

type ApolloClientType = ApolloClient<NormalizedCacheObject>

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

interface ApolloPluginConfig {
export interface ApolloPluginConfig {
apolloClient: ApolloClient<NormalizedCacheObject>
}

const apolloPlugin =
export const apolloPlugin =
(options: ApolloPluginConfig) =>
<Client extends ReactotronCore>(reactotronClient: Client) => {
const { apolloClient } = options
assertHasLoggerPlugin(reactotronClient)
const reactotron = reactotronClient as unknown as ReactotronCore &
InferFeatures<ReactotronCore, LoggerPlugin>
assertHasStateResponsePlugin(reactotronClient)
const reactotron = reactotronClient as Client &
InferFeatures<Client, LoggerPlugin> &
InferFeatures<Client, StateResponsePlugin>

// --- Plugin-scoped variables ---------------------------------

// hang on to the apollo state
let apolloData = { cache: {}, queries: {}, mutations: {} }

// a list of subscriptions the client is subscribing to
let subscriptions: string[] = []

function subscribe(command: Command<"state.values.subscribe">) {
const paths: string[] = (command && command.payload && command.payload.paths) || []

if (paths) {
// TODO ditch ramda
subscriptions = uniq(flatten(paths))
}

sendSubscriptions()
}

function getChanges() {
// TODO also check if cache state is empty
if (!reactotron) return []

const changes = []

const state = apolloData.cache

subscriptions.forEach((path) => {
let cleanedPath = path
let starredPath = false

if (path && path.endsWith("*")) {
// Handle the star!
starredPath = true
cleanedPath = path.substring(0, path.length - 2)
}

const values = pathObject(cleanedPath, state)

if (starredPath && cleanedPath && values) {
changes.push(
...Object.entries(values).map((val) => ({
path: `${cleanedPath}.${val[0]}`,
value: val[1],
}))
)
} else {
changes.push({ path: cleanedPath, value: state[cleanedPath] })
}
})

return changes
}

function sendSubscriptions() {
const changes = getChanges()
reactotron.stateValuesChange(changes)
}

// --- Reactotron Hooks ---------------------------------

// maps inbound commands to functions to run
// TODO clear cache command?
const COMMAND_MAP = {
"state.values.subscribe": subscribe,
} satisfies { [name: string]: (command: Command) => void }

/**
* Fires when we receive a command from the reactotron app.
*/
function onCommand(command: Command) {
// lookup the command and execute
const handler = COMMAND_MAP[command && command.type]
handler && handler(command)
}

// --- Reactotron plugin interface ---------------------------------

return {
// Fires when we receive a command from the Reactotron app.
onCommand,

onConnect() {
reactotron.log("Apollo Client Connected")
reactotron.display({ name: "APOLLO CLIENT", preview: "Connected" })

const poll = () =>
getCurrentState(apolloClient).then((state) => {
apolloData = state

sendSubscriptions()

reactotron.display({
name: "APOLLO CLIENT",
preview: `Apollo client updated at ${state.lastUpdateAt}`,
preview: `State Updated`,
value: state,
})
})
apolloClient.__actionHookForDevTools(debounce(poll))
},
onDisconnect() {
// Does this do anything? How do we clean up?
apolloClient.__actionHookForDevTools(null)
},
} satisfies Plugin<Client>
}

Expand Down

0 comments on commit 6eb8d10

Please sign in to comment.