@@ -7,15 +7,20 @@ import {
7
7
assertHasLoggerPlugin ,
8
8
InferFeatures ,
9
9
LoggerPlugin ,
10
+ assertHasStateResponsePlugin ,
11
+ StateResponsePlugin ,
10
12
} from "reactotron-core-client"
11
-
13
+ import type { Command } from "reactotron-core-contract"
12
14
import type { DocumentNode , NormalizedCacheObject } from "@apollo/client"
13
15
import { getOperationName } from "@apollo/client/utilities"
14
16
import type { QueryInfo } from "@apollo/client/core/QueryInfo"
15
17
16
18
import type { ASTNode } from "graphql"
17
19
import { print } from "graphql"
18
20
21
+ import { flatten , uniq } from "ramda"
22
+ import pathObject from "./helpers/pathObject"
23
+
19
24
type ApolloClientType = ApolloClient < NormalizedCacheObject >
20
25
21
26
type Variables = QueryInfo [ "variables" ]
@@ -206,31 +211,123 @@ function debounce(func: (...args: any) => any, timeout = 500): () => any {
206
211
}
207
212
}
208
213
209
- interface ApolloPluginConfig {
214
+ export interface ApolloPluginConfig {
210
215
apolloClient : ApolloClient < NormalizedCacheObject >
211
216
}
212
217
213
- const apolloPlugin =
218
+ export const apolloPlugin =
214
219
( options : ApolloPluginConfig ) =>
215
220
< Client extends ReactotronCore > ( reactotronClient : Client ) => {
216
221
const { apolloClient } = options
217
222
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 ---------------------------------
220
305
221
306
return {
307
+ // Fires when we receive a command from the Reactotron app.
308
+ onCommand,
309
+
222
310
onConnect ( ) {
223
- reactotron . log ( "Apollo Client Connected" )
311
+ reactotron . display ( { name : "APOLLO CLIENT" , preview : "Connected" } )
312
+
224
313
const poll = ( ) =>
225
314
getCurrentState ( apolloClient ) . then ( ( state ) => {
315
+ apolloData = state
316
+
317
+ sendSubscriptions ( )
318
+
226
319
reactotron . display ( {
227
320
name : "APOLLO CLIENT" ,
228
- preview : `Apollo client updated at ${ state . lastUpdateAt } ` ,
321
+ preview : `State Updated ` ,
229
322
value : state ,
230
323
} )
231
324
} )
232
325
apolloClient . __actionHookForDevTools ( debounce ( poll ) )
233
326
} ,
327
+ onDisconnect ( ) {
328
+ // Does this do anything? How do we clean up?
329
+ apolloClient . __actionHookForDevTools ( null )
330
+ } ,
234
331
} satisfies Plugin < Client >
235
332
}
236
333
0 commit comments