-
-
Notifications
You must be signed in to change notification settings - Fork 98
feat(core): Expose __DC_CONTROLLERS__ for programmatic store access via MCP #3753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| '@data-client/core': patch | ||
| '@data-client/react': patch | ||
| '@data-client/vue': patch | ||
| --- | ||
|
|
||
| Add `globalThis.__DC_CONTROLLERS__` Map in dev mode for programmatic store access from browser DevTools MCP, React Native debuggers, and other development tooling. | ||
|
|
||
| Each [DataProvider](/docs/api/DataProvider) registers its [Controller](/docs/api/Controller) keyed by the devtools connection name, supporting multiple providers on the same page. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../data-client-manager/references/Actions.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| # Debugging @data-client/react with Chrome DevTools MCP | ||
|
|
||
| Use `user-chrome-devtools` MCP tools to programmatically inspect and interact with the | ||
| data-client store in a running browser. Requires dev mode (`NODE_ENV !== 'production'`). | ||
|
|
||
| For full [Controller](./Controller.md) API (fetch, set, invalidate, etc.) and | ||
| [Action](./Actions.md) types (FETCH, SET, INVALIDATE, etc.), see those references. | ||
|
|
||
| ## Setup: Inject Action Logger | ||
|
|
||
| On first navigation (or reload), inject a logging shim via `initScript` to capture all | ||
| dispatched actions before the app boots. This gives you a persistent action log to query. | ||
|
|
||
| ```js | ||
| navigate_page → initScript: | ||
|
|
||
| window.__DC_ACTION_LOG__ = []; | ||
| const _origDispatch = Object.getOwnPropertyDescriptor(Object.getPrototypeOf({}), 'dispatch'); | ||
| Object.defineProperty(globalThis, '__DC_INTERCEPT_DISPATCH__', { | ||
| value(action) { | ||
| const entry = { | ||
| type: action.type, | ||
| key: action.meta?.key, | ||
| schema: action.endpoint?.name ?? action.schema?.constructor?.name, | ||
| timestamp: Date.now(), | ||
| }; | ||
| if (action.error) entry.error = true; | ||
| globalThis.__DC_ACTION_LOG__.push(entry); | ||
| if (globalThis.__DC_ACTION_LOG__.length > 500) | ||
| globalThis.__DC_ACTION_LOG__ = globalThis.__DC_ACTION_LOG__.slice(-250); | ||
| }, | ||
| writable: true, configurable: true, | ||
| }); | ||
| ``` | ||
|
|
||
| Then after the app loads, hook into the controller's dispatch. First discover | ||
| the `devtoolsName` key (see "Accessing the Controller"), then use it: | ||
|
|
||
| ```js | ||
| evaluate_script: | ||
|
|
||
| () => { | ||
| const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); | ||
| if (!ctrl) return 'no controller yet'; | ||
| const orig = ctrl._dispatch.bind(ctrl); | ||
| ctrl._dispatch = (action) => { | ||
| globalThis.__DC_INTERCEPT_DISPATCH__?.(action); | ||
| return orig(action); | ||
| }; | ||
| return 'dispatch intercepted'; | ||
| } | ||
| ``` | ||
|
|
||
| ## Accessing the Controller | ||
|
|
||
| `DevToolsManager` registers controllers in `globalThis.__DC_CONTROLLERS__` (a `Map`) keyed by | ||
| `devtoolsName` — defaults to `"Data Client: <page title>"`. | ||
|
|
||
| **Step 1: Discover available controllers and their keys.** | ||
|
|
||
| ```js | ||
| () => { | ||
| const m = globalThis.__DC_CONTROLLERS__; | ||
| if (!m || m.size === 0) return 'no controllers registered'; | ||
| return [...m.keys()]; | ||
| } | ||
| ``` | ||
|
|
||
| **Step 2: Get a controller by its `devtoolsName` key.** Use the key from Step 1. | ||
|
|
||
| ```js | ||
| () => { | ||
| const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); | ||
| return ctrl ? 'controller found' : 'not found'; | ||
| } | ||
| ``` | ||
|
|
||
| Always use `.get(devtoolsName)` with the actual key from Step 1 — not `.values().next().value` | ||
| — so you target the correct store when multiple `DataProvider`s exist. | ||
|
|
||
| ## Reading State | ||
|
|
||
| The store state shape is `{ entities, endpoints, indexes, meta, entitiesMeta, optimistic, lastReset }`. | ||
|
|
||
| ### Full state overview | ||
|
|
||
| ```js | ||
| () => { | ||
| const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); | ||
| if (!state) return 'no state'; | ||
| return { | ||
| entityTypes: Object.keys(state.entities), | ||
| endpointCount: Object.keys(state.endpoints).length, | ||
| optimisticCount: state.optimistic.length, | ||
| lastReset: state.lastReset, | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| ### List all entities of a type | ||
|
|
||
| ```js | ||
| () => { | ||
| const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); | ||
| const entities = state?.entities?.['Todo']; | ||
| if (!entities) return 'no Todo entities'; | ||
| return Object.values(entities); | ||
| } | ||
| ``` | ||
|
|
||
| ### Inspect a specific entity by pk | ||
|
|
||
| ```js | ||
| () => { | ||
| const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); | ||
| return state?.entities?.['Todo']?.['5']; | ||
| } | ||
| ``` | ||
|
|
||
| ### List cached endpoint keys | ||
|
|
||
| ```js | ||
| () => { | ||
| const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); | ||
| return Object.keys(state?.endpoints ?? {}); | ||
| } | ||
| ``` | ||
|
|
||
| ### Get endpoint response (raw, before denormalization) | ||
|
|
||
| ```js | ||
| () => { | ||
| const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); | ||
| const key = Object.keys(state?.endpoints ?? {}).find(k => k.includes('GET /todos')); | ||
| return key ? { key, data: state.endpoints[key] } : 'not found'; | ||
| } | ||
| ``` | ||
|
|
||
| ### Check endpoint metadata (expiry, errors, invalidation) | ||
|
|
||
| ```js | ||
| () => { | ||
| const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); | ||
| const key = Object.keys(state?.meta ?? {}).find(k => k.includes('/todos')); | ||
| return key ? { key, ...state.meta[key] } : 'no meta found'; | ||
| } | ||
| ``` | ||
|
|
||
| ## Dispatching Actions | ||
|
|
||
| Use the [Controller](./Controller.md) to mutate the store. Always look up by `devtoolsName`. | ||
| See [Actions](./Actions.md) for the full list of action types dispatched. | ||
|
|
||
| ### Reset the entire store | ||
|
|
||
| ```js | ||
| () => { | ||
| const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); | ||
| ctrl?.resetEntireStore(); | ||
| return 'store reset'; | ||
| } | ||
| ``` | ||
|
|
||
| ### Invalidate all endpoints (force refetch) | ||
|
|
||
| ```js | ||
| () => { | ||
| const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); | ||
| ctrl?.invalidateAll({ testKey: () => true }); | ||
| return 'all invalidated'; | ||
| } | ||
| ``` | ||
|
|
||
| ### Invalidate endpoints matching a pattern | ||
|
|
||
| ```js | ||
| () => { | ||
| const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); | ||
| ctrl?.invalidateAll({ testKey: key => key.includes('/todos') }); | ||
| return 'todo endpoints invalidated'; | ||
| } | ||
| ``` | ||
|
|
||
| ### Expire endpoints (mark stale, refetch on next use) | ||
|
|
||
| ```js | ||
| () => { | ||
| const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); | ||
| ctrl?.expireAll({ testKey: key => key.includes('/todos') }); | ||
| return 'todo endpoints expired'; | ||
| } | ||
| ``` | ||
|
|
||
| ## Reading the Action Log | ||
|
|
||
| After injecting the dispatch interceptor (see Setup): | ||
|
|
||
| ### Recent actions | ||
|
|
||
| ```js | ||
| () => globalThis.__DC_ACTION_LOG__?.slice(-20) ?? 'no log' | ||
| ``` | ||
|
|
||
| ### Filter by type | ||
|
|
||
| ```js | ||
| () => globalThis.__DC_ACTION_LOG__?.filter(a => a.type === 'rdc/set') ?? [] | ||
| ``` | ||
|
|
||
| ### Filter errors | ||
|
|
||
| ```js | ||
| () => globalThis.__DC_ACTION_LOG__?.filter(a => a.error) ?? [] | ||
| ``` | ||
|
|
||
| ## Correlating with Network Requests | ||
|
|
||
| Use `list_network_requests` with `resourceTypes: ["fetch", "xhr"]` to see API calls, | ||
| then cross-reference with endpoint keys in state. | ||
|
|
||
| ## Debugging Checklist | ||
|
|
||
| 1. **Verify controller exists**: Check `__DC_CONTROLLERS__` map size | ||
| 2. **Inspect state shape**: Get entity types and endpoint count | ||
| 3. **Check specific data**: Look up entities by type and pk | ||
| 4. **Review endpoint metadata**: Check expiry, errors, invalidation status | ||
| 5. **Track actions**: Read the action log for recent dispatches | ||
| 6. **Correlate network**: Compare `list_network_requests` with endpoint keys | ||
| 7. **Force refresh**: Use `invalidateAll` or `expireAll` to trigger refetches | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -94,20 +94,21 @@ export default class DevToolsManager implements Manager { | |
| protected actions: [ActionTypes, State<unknown>][] = []; | ||
| declare protected controller: Controller; | ||
| declare skipLogging?: (action: ActionTypes) => boolean; | ||
| declare devtoolsName: string; | ||
| maxBufferLength = 100; | ||
|
|
||
| constructor( | ||
| config?: DevToolsConfig, | ||
| skipLogging?: (action: ActionTypes) => boolean, | ||
| ) { | ||
| /* istanbul ignore next */ | ||
| const options = { ...DEFAULT_CONFIG, ...config }; | ||
| this.devtoolsName = | ||
| options.name ?? `Data Client: ${globalThis.document?.title}`; | ||
| this.devTools = | ||
| typeof window !== 'undefined' && | ||
| (window as any).__REDUX_DEVTOOLS_EXTENSION__ && | ||
| (window as any).__REDUX_DEVTOOLS_EXTENSION__.connect({ | ||
| ...DEFAULT_CONFIG, | ||
| ...config, | ||
| }); | ||
| (window as any).__REDUX_DEVTOOLS_EXTENSION__.connect(options); | ||
| // we cut it in half so we should double so we don't lose | ||
| if (config?.maxAge) this.maxBufferLength = config.maxAge * 2; | ||
| if (skipLogging) this.skipLogging = skipLogging; | ||
|
|
@@ -118,8 +119,8 @@ export default class DevToolsManager implements Manager { | |
| /* istanbul ignore next */ | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| this.prototype.middleware = function (controller) { | ||
| if (!this.devTools) return next => action => next(action); | ||
| this.controller = controller; | ||
| if (!this.devTools) return next => action => next(action); | ||
| const reducer = createReducer(controller as any); | ||
| let state = controller.getState(); | ||
| return next => action => { | ||
|
|
@@ -158,6 +159,12 @@ export default class DevToolsManager implements Manager { | |
|
|
||
| /** Called when initial state is ready */ | ||
| init(state: State<any>) { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| ((globalThis as any).__DC_CONTROLLERS__ ??= new Map()).set( | ||
| this.devtoolsName, | ||
| this.controller, | ||
| ); | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (process.env.NODE_ENV !== 'production' && this.devTools) { | ||
| this.devTools.init(state); | ||
| this.devTools.subscribe((msg: any) => { | ||
|
|
@@ -186,5 +193,9 @@ export default class DevToolsManager implements Manager { | |
| } | ||
|
|
||
| /** Ensures all subscriptions are cleaned up. */ | ||
| cleanup() {} | ||
| cleanup() { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| (globalThis as any).__DC_CONTROLLERS__?.delete(this.devtoolsName); | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cleanup removes another provider's active controller entryLow Severity When multiple Additional Locations (1) |
||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dead code in skill documentation example
Low Severity
The
_origDispatchvariable is assigned the result ofObject.getOwnPropertyDescriptor(Object.getPrototypeOf({}), 'dispatch'), which always returnsundefinedsinceObject.prototypehas nodispatchproperty. The variable is never referenced again in the code block. This appears to be leftover from an earlier approach and is dead code in a skill file that AI coding assistants will follow as a template.