Skip to content

Commit 33b85fa

Browse files
ryanbas21claude
andcommitted
refactor(bridge)!: rename attachDevToolsBridge → attachDaVinciBridge, consolidate BridgeHandle
BREAKING CHANGE: `attachDevToolsBridge` is now `attachDaVinciBridge`. `BridgeHandle` is the single shared type for all bridges (replaces `JourneyBridgeHandle` and `OidcBridgeHandle`). File renamed from `bridge.ts` to `davinci-bridge.ts`. Also adds missing test coverage: - Ring buffer cap (emit.ts splices at 500 entries) - Cache/responseBody passthrough via client.cache.getCache - Cookie diff tracking (session:cookie events) - Multi-key storage diffs in a single transition - Multi-bridge coexistence (independent event streams) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f976bb6 commit 33b85fa

8 files changed

Lines changed: 392 additions & 39 deletions

File tree

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
export { attachDevToolsBridge } from './lib/bridge.js';
2-
export type { BridgeHandle } from './lib/bridge.js';
1+
export { attachDaVinciBridge } from './lib/davinci-bridge.js';
32
export { attachJourneyBridge } from './lib/journey-bridge.js';
4-
export type { JourneyBridgeHandle } from './lib/journey-bridge.js';
53
export { attachOidcBridge } from './lib/oidc-bridge.js';
6-
export type { OidcBridgeHandle } from './lib/oidc-bridge.js';
74
export { DEVTOOLS_EVENT_NAME, emitAuthEvent, emitConfigEvent } from './lib/emit.js';
8-
export type { DevtoolsOptions } from './lib/emit.js';
5+
export type { BridgeHandle, DevtoolsOptions } from './lib/emit.js';

packages/devtools-bridge/src/lib/bridge.test.ts renamed to packages/devtools-bridge/src/lib/davinci-bridge.test.ts

Lines changed: 140 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2-
import { attachDevToolsBridge, nodeToSdkData } from './bridge.js';
2+
import { attachDaVinciBridge, nodeToSdkData } from './davinci-bridge.js';
33
import { DEVTOOLS_EVENT_NAME } from './emit.js';
44
import type { AuthEvent } from '@wolfcola/devtools-types';
55

@@ -100,13 +100,22 @@ describe('nodeToSdkData', () => {
100100
const result = nodeToSdkData({ status: 'continue', cache: null } as never, undefined);
101101
expect(result.requestId).toBeUndefined();
102102
});
103+
104+
it('passes responseBody through to the result', () => {
105+
const body = { access_token: 'tok-abc', token_type: 'Bearer' };
106+
const result = nodeToSdkData({ status: 'success' }, 'continue', body);
107+
expect(result.responseBody).toBe(body);
108+
});
103109
});
104110

105111
// ---------------------------------------------------------------------------
106112
// Mock client factory
107113
// ---------------------------------------------------------------------------
108114

109-
function makeClient(initialNode: Record<string, unknown>) {
115+
function makeClient(
116+
initialNode: Record<string, unknown>,
117+
cache?: { getCache: (key: string) => unknown },
118+
) {
110119
let listener: (() => void) | null = null;
111120
let node = initialNode;
112121
return {
@@ -117,6 +126,7 @@ function makeClient(initialNode: Record<string, unknown>) {
117126
};
118127
}),
119128
getNode: vi.fn(() => node),
129+
cache,
120130
/** Test helper: update internal node and fire the subscribed listener. */
121131
trigger: (newNode: Record<string, unknown>) => {
122132
node = newNode;
@@ -140,7 +150,7 @@ function captureDevtoolsEvents(): { events: CustomEvent<AuthEvent>[]; stop: () =
140150
// Tests
141151
// ---------------------------------------------------------------------------
142152

143-
describe('attachDevToolsBridge', () => {
153+
describe('attachDaVinciBridge', () => {
144154
beforeEach(() => {
145155
// Simulate extension presence for all tests except the no-op test.
146156
(window as unknown as Record<string, unknown>)['__PING_DEVTOOLS_EXTENSION__'] = true;
@@ -154,7 +164,7 @@ describe('attachDevToolsBridge', () => {
154164

155165
it('returns a BridgeHandle with a detach function', () => {
156166
const client = makeClient({ status: 'start' });
157-
const handle = attachDevToolsBridge(client);
167+
const handle = attachDaVinciBridge(client);
158168

159169
expect(handle).toHaveProperty('detach');
160170
expect(typeof handle.detach).toBe('function');
@@ -166,7 +176,7 @@ describe('attachDevToolsBridge', () => {
166176
const client = makeClient({ status: 'start' });
167177
const { events, stop } = captureDevtoolsEvents();
168178

169-
const handle = attachDevToolsBridge(client);
179+
const handle = attachDaVinciBridge(client);
170180

171181
// Trigger a status transition.
172182
client.trigger({ status: 'continue' });
@@ -202,7 +212,7 @@ describe('attachDevToolsBridge', () => {
202212
const client = makeClient({ status: 'start' });
203213
const { events, stop } = captureDevtoolsEvents();
204214

205-
const handle = attachDevToolsBridge(client);
215+
const handle = attachDaVinciBridge(client);
206216
client.trigger(continueNode);
207217

208218
handle.detach();
@@ -228,7 +238,7 @@ describe('attachDevToolsBridge', () => {
228238
const client = makeClient({ status: 'start' });
229239
const { events, stop } = captureDevtoolsEvents();
230240

231-
const handle = attachDevToolsBridge(client);
241+
const handle = attachDaVinciBridge(client);
232242

233243
// First trigger sets previousStatus = 'start'.
234244
client.trigger({ status: 'start' });
@@ -249,7 +259,7 @@ describe('attachDevToolsBridge', () => {
249259
const client = makeClient({ status: 'start' });
250260
const { events, stop } = captureDevtoolsEvents();
251261

252-
const handle = attachDevToolsBridge(client);
262+
const handle = attachDaVinciBridge(client);
253263

254264
// Verify subscribe was wired up.
255265
expect(client.subscribe).toHaveBeenCalledTimes(1);
@@ -268,7 +278,7 @@ describe('attachDevToolsBridge', () => {
268278
const client = makeClient({ status: 'start' });
269279
const { events, stop } = captureDevtoolsEvents();
270280

271-
const handle = attachDevToolsBridge(client, {
281+
const handle = attachDaVinciBridge(client, {
272282
clientId: 'my-app',
273283
redirectUri: 'https://app.example.com/callback',
274284
});
@@ -293,7 +303,7 @@ describe('attachDevToolsBridge', () => {
293303
const client = makeClient({ status: 'start' });
294304
const { events, stop } = captureDevtoolsEvents();
295305

296-
const handle = attachDevToolsBridge(client, { clientId: 'my-app' });
306+
const handle = attachDaVinciBridge(client, { clientId: 'my-app' });
297307

298308
client.trigger({ status: 'continue' });
299309
client.trigger({ status: 'success' });
@@ -309,7 +319,7 @@ describe('attachDevToolsBridge', () => {
309319
const client = makeClient({ status: 'start' });
310320
const { events, stop } = captureDevtoolsEvents();
311321

312-
const handle = attachDevToolsBridge(client);
322+
const handle = attachDaVinciBridge(client);
313323

314324
client.trigger({ status: 'continue' });
315325

@@ -327,7 +337,7 @@ describe('attachDevToolsBridge', () => {
327337
const client = makeClient({ status: 'start' });
328338
const { events, stop } = captureDevtoolsEvents();
329339

330-
const handle = attachDevToolsBridge(client);
340+
const handle = attachDaVinciBridge(client);
331341

332342
// subscribe is called (the bridge still subscribes), but no events should be dispatched.
333343
expect(client.subscribe).toHaveBeenCalledTimes(1);
@@ -346,7 +356,7 @@ describe('attachDevToolsBridge', () => {
346356
const client = makeClient({ status: 'start' });
347357
const { events, stop } = captureDevtoolsEvents();
348358

349-
const handle = attachDevToolsBridge(client, { clientId: 'my-app' });
359+
const handle = attachDaVinciBridge(client, { clientId: 'my-app' });
350360
client.trigger({ status: 'continue' });
351361

352362
handle.detach();
@@ -360,7 +370,7 @@ describe('attachDevToolsBridge', () => {
360370
});
361371
});
362372

363-
describe('attachDevToolsBridge session tracking', () => {
373+
describe('attachDaVinciBridge session tracking', () => {
364374
beforeEach(() => {
365375
(window as unknown as Record<string, unknown>)['__PING_DEVTOOLS_EXTENSION__'] = true;
366376
localStorage.clear();
@@ -380,7 +390,7 @@ describe('attachDevToolsBridge session tracking', () => {
380390
const client = makeClient({ status: 'start' });
381391
const { events, stop } = captureDevtoolsEvents();
382392

383-
const handle = attachDevToolsBridge(client);
393+
const handle = attachDaVinciBridge(client);
384394

385395
// Trigger a node transition, then mutate storage in the same tick
386396
client.trigger({ status: 'continue' });
@@ -410,7 +420,7 @@ describe('attachDevToolsBridge session tracking', () => {
410420
const client = makeClient({ status: 'start' });
411421
const { events, stop } = captureDevtoolsEvents();
412422

413-
const handle = attachDevToolsBridge(client);
423+
const handle = attachDaVinciBridge(client);
414424
client.trigger({ status: 'continue' });
415425

416426
await new Promise((r) => setTimeout(r, 10));
@@ -423,4 +433,118 @@ describe('attachDevToolsBridge session tracking', () => {
423433
);
424434
expect(sessionEvents).toHaveLength(0);
425435
});
436+
437+
it('emits session:cookie event when document.cookie changes after a node transition', async () => {
438+
const client = makeClient({ status: 'start' });
439+
const { events, stop } = captureDevtoolsEvents();
440+
441+
const handle = attachDaVinciBridge(client);
442+
443+
client.trigger({ status: 'continue' });
444+
// Mutate cookie after the transition in the same tick
445+
document.cookie = 'sid=new-session-id';
446+
447+
await new Promise((r) => setTimeout(r, 10));
448+
449+
handle.detach();
450+
stop();
451+
452+
const cookieEvents = events.filter((e) => e.detail.type === 'session:cookie');
453+
expect(cookieEvents).toHaveLength(1);
454+
const data = cookieEvents[0].detail.data as {
455+
_tag: string;
456+
key: string;
457+
before?: string;
458+
after?: string;
459+
};
460+
expect(data._tag).toBe('session');
461+
expect(data.key).toBe('document.cookie');
462+
expect(data.before).toBeUndefined();
463+
expect(data.after).toBe('sid=new-session-id');
464+
});
465+
466+
it('emits separate session:storage events for multiple keys changing at once', async () => {
467+
const client = makeClient({ status: 'start' });
468+
const { events, stop } = captureDevtoolsEvents();
469+
470+
// Pre-populate a key that will be removed
471+
localStorage.setItem('old-key', 'old-value');
472+
473+
const handle = attachDaVinciBridge(client);
474+
475+
// First transition to capture the initial snapshot (which includes old-key)
476+
client.trigger({ status: 'continue' });
477+
await new Promise((r) => setTimeout(r, 10));
478+
479+
// Now mutate multiple keys and trigger another transition
480+
localStorage.setItem('new-key', 'new-value');
481+
localStorage.setItem('old-key', 'changed-value');
482+
localStorage.setItem('another-key', 'another-value');
483+
484+
client.trigger({ status: 'success' });
485+
await new Promise((r) => setTimeout(r, 10));
486+
487+
handle.detach();
488+
stop();
489+
490+
const storageEvents = events.filter((e) => e.detail.type === 'session:storage');
491+
const changedKeys = storageEvents.map(
492+
(e) => (e.detail.data as { key: string }).key,
493+
);
494+
495+
expect(changedKeys).toContain('new-key');
496+
expect(changedKeys).toContain('old-key');
497+
expect(changedKeys).toContain('another-key');
498+
expect(storageEvents).toHaveLength(3);
499+
});
500+
});
501+
502+
describe('attachDaVinciBridge cache passthrough', () => {
503+
beforeEach(() => {
504+
(window as unknown as Record<string, unknown>)['__PING_DEVTOOLS_EXTENSION__'] = true;
505+
});
506+
507+
afterEach(async () => {
508+
delete (window as unknown as Record<string, unknown>)['__PING_DEVTOOLS_EXTENSION__'];
509+
await new Promise((r) => setTimeout(r, 10));
510+
});
511+
512+
it('passes cached response body through to the emitted SdkData', () => {
513+
const cachedResponse = { access_token: 'tok-xyz', token_type: 'Bearer' };
514+
const cache = { getCache: vi.fn(() => cachedResponse) };
515+
const client = makeClient({ status: 'start' }, cache);
516+
const { events, stop } = captureDevtoolsEvents();
517+
518+
const handle = attachDaVinciBridge(client);
519+
client.trigger({
520+
status: 'continue',
521+
cache: { key: 'req-42' },
522+
server: { interactionId: 'iid-1' },
523+
});
524+
525+
handle.detach();
526+
stop();
527+
528+
expect(cache.getCache).toHaveBeenCalledWith('req-42');
529+
expect(events).toHaveLength(1);
530+
const data = events[0].detail.data as { responseBody?: unknown };
531+
expect(data.responseBody).toBe(cachedResponse);
532+
});
533+
534+
it('does not call getCache when node has no cache key', () => {
535+
const cache = { getCache: vi.fn() };
536+
const client = makeClient({ status: 'start' }, cache);
537+
const { events, stop } = captureDevtoolsEvents();
538+
539+
const handle = attachDaVinciBridge(client);
540+
client.trigger({ status: 'continue' });
541+
542+
handle.detach();
543+
stop();
544+
545+
expect(cache.getCache).not.toHaveBeenCalled();
546+
expect(events).toHaveLength(1);
547+
const data = events[0].detail.data as { responseBody?: unknown };
548+
expect(data.responseBody).toBeUndefined();
549+
});
426550
});

packages/devtools-bridge/src/lib/bridge.ts renamed to packages/devtools-bridge/src/lib/davinci-bridge.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Schema, Option, pipe } from 'effect';
22
import type { SdkData } from '@wolfcola/devtools-types';
33
import { SdkErrorSchema, SdkAuthorizationSchema } from '@wolfcola/devtools-types';
44
import { emitAuthEvent, emitConfigEvent } from './emit.js';
5-
import type { DevtoolsOptions } from './emit.js';
5+
import type { BridgeHandle, DevtoolsOptions } from './emit.js';
66

77
interface Subscribable {
88
subscribe: (listener: () => void) => () => void;
@@ -12,10 +12,6 @@ interface Subscribable {
1212
};
1313
}
1414

15-
export interface BridgeHandle {
16-
detach: () => void;
17-
}
18-
1915
export interface SdkConfig {
2016
clientId?: string;
2117
redirectUri?: string;
@@ -198,7 +194,7 @@ function emitNodeChange(data: SdkData, options?: DevtoolsOptions): void {
198194
*
199195
* Returns a no-op handle when run outside a browser. Always call `detach()` on cleanup.
200196
*/
201-
export function attachDevToolsBridge(
197+
export function attachDaVinciBridge(
202198
client: Subscribable,
203199
config?: object,
204200
devtoolsOptions?: DevtoolsOptions,

packages/devtools-bridge/src/lib/emit.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,34 @@ describe('emitAuthEvent options', () => {
119119
});
120120
});
121121

122+
describe('emitAuthEvent ring buffer', () => {
123+
beforeEach(() => {
124+
delete window.__PING_DEVTOOLS_STATE__;
125+
});
126+
127+
it('caps __PING_DEVTOOLS_STATE__ at 500 entries and drops the oldest', () => {
128+
for (let i = 0; i < 501; i++) {
129+
emitAuthEvent(makeEvent({ id: `evt-${i}` }));
130+
}
131+
132+
expect(window.__PING_DEVTOOLS_STATE__).toHaveLength(500);
133+
// The first event (evt-0) should have been evicted
134+
expect(window.__PING_DEVTOOLS_STATE__![0].id).toBe('evt-1');
135+
// The last event should be the most recent
136+
expect(window.__PING_DEVTOOLS_STATE__![499].id).toBe('evt-500');
137+
});
138+
139+
it('continues to evict as more events arrive beyond the cap', () => {
140+
for (let i = 0; i < 510; i++) {
141+
emitAuthEvent(makeEvent({ id: `evt-${i}` }));
142+
}
143+
144+
expect(window.__PING_DEVTOOLS_STATE__).toHaveLength(500);
145+
expect(window.__PING_DEVTOOLS_STATE__![0].id).toBe('evt-10');
146+
expect(window.__PING_DEVTOOLS_STATE__![499].id).toBe('evt-509');
147+
});
148+
});
149+
122150
describe('emitConfigEvent', () => {
123151
beforeEach(() => {
124152
delete window.__PING_DEVTOOLS_STATE__;

packages/devtools-bridge/src/lib/emit.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export interface DevtoolsOptions {
66
consoleLog?: boolean;
77
}
88

9+
export interface BridgeHandle {
10+
detach: () => void;
11+
}
12+
913
declare global {
1014
interface Window {
1115
__PING_DEVTOOLS_STATE__?: AuthEvent[];

packages/devtools-bridge/src/lib/journey-bridge.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { Schema, Option, pipe } from 'effect';
22
import { emitAuthEvent, emitConfigEvent } from './emit.js';
3-
import type { DevtoolsOptions } from './emit.js';
3+
import type { BridgeHandle, DevtoolsOptions } from './emit.js';
44
import type { JourneyData } from '@wolfcola/devtools-types';
55

6-
export interface JourneyBridgeHandle {
7-
detach: () => void;
8-
}
9-
106
interface JourneySubscribable {
117
subscribe: (listener: () => void) => () => void;
128
getState: () => unknown;
@@ -104,7 +100,7 @@ export function attachJourneyBridge(
104100
client: JourneySubscribable,
105101
config?: object,
106102
devtoolsOptions?: DevtoolsOptions,
107-
): JourneyBridgeHandle {
103+
): BridgeHandle {
108104
if (typeof window === 'undefined') {
109105
return { detach: () => undefined };
110106
}

0 commit comments

Comments
 (0)