@@ -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"
1214import type { DocumentNode , NormalizedCacheObject } from "@apollo/client"
1315import { getOperationName } from "@apollo/client/utilities"
1416import type { QueryInfo } from "@apollo/client/core/QueryInfo"
1517
1618import type { ASTNode } from "graphql"
1719import { print } from "graphql"
1820
21+ import { flatten , uniq } from "ramda"
22+ import pathObject from "./helpers/pathObject"
23+
1924type ApolloClientType = ApolloClient < NormalizedCacheObject >
2025
2126type 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