@@ -7,7 +7,7 @@ import { useEffect, useRef } from "react";
77
88import { postMessage } from "../api" ;
99
10- import type { IpcResponse } from "@repo/shared" ;
10+ import type { IpcNotification , IpcResponse } from "@repo/shared" ;
1111
1212interface PendingRequest {
1313 resolve : ( value : unknown ) => void ;
@@ -20,10 +20,10 @@ const DEFAULT_TIMEOUT_MS = 30000;
2020export interface UseIpcOptions {
2121 /** Request timeout in ms (default: 30000) */
2222 timeoutMs ?: number ;
23- /** Scope for message routing */
24- scope ?: string ;
2523}
2624
25+ type NotificationHandler = ( data : unknown ) => void ;
26+
2727/**
2828 * Hook for type-safe IPC with the extension.
2929 *
@@ -32,11 +32,21 @@ export interface UseIpcOptions {
3232 * const ipc = useIpc();
3333 * const tasks = await ipc.request(getTasks); // Type: Task[]
3434 * ipc.command(viewInCoder, { taskId: "123" }); // Fire-and-forget
35+ *
36+ * // Subscribe to notifications
37+ * useEffect(() => {
38+ * return ipc.onNotification(tasksUpdated, (tasks) => {
39+ * setTasks(tasks); // tasks is typed as Task[]
40+ * });
41+ * }, []);
3542 * ```
3643 */
3744export function useIpc ( options : UseIpcOptions = { } ) {
38- const { timeoutMs = DEFAULT_TIMEOUT_MS , scope } = options ;
45+ const { timeoutMs = DEFAULT_TIMEOUT_MS } = options ;
3946 const pendingRequests = useRef < Map < string , PendingRequest > > ( new Map ( ) ) ;
47+ const notificationHandlers = useRef < Map < string , Set < NotificationHandler > > > (
48+ new Map ( ) ,
49+ ) ;
4050
4151 // Cleanup on unmount
4252 useEffect ( ( ) => {
@@ -46,27 +56,43 @@ export function useIpc(options: UseIpcOptions = {}) {
4656 req . reject ( new Error ( "Component unmounted" ) ) ;
4757 }
4858 pendingRequests . current . clear ( ) ;
59+ notificationHandlers . current . clear ( ) ;
4960 } ;
5061 } , [ ] ) ;
5162
52- // Handle responses
63+ // Handle responses and notifications
5364 useEffect ( ( ) => {
5465 const handler = ( event : MessageEvent ) => {
55- const msg = event . data as IpcResponse | undefined ;
56- if ( ! msg || typeof msg . requestId !== "string" || ! ( "success" in msg ) ) {
66+ const msg = event . data as IpcResponse | IpcNotification | undefined ;
67+
68+ if ( ! msg || typeof msg !== "object" ) {
5769 return ;
5870 }
5971
60- const pending = pendingRequests . current . get ( msg . requestId ) ;
61- if ( ! pending ) return ;
72+ // Response handling (has requestId + success)
73+ if ( "requestId" in msg && "success" in msg ) {
74+ const pending = pendingRequests . current . get ( msg . requestId ) ;
75+ if ( ! pending ) return ;
76+
77+ clearTimeout ( pending . timeout ) ;
78+ pendingRequests . current . delete ( msg . requestId ) ;
6279
63- clearTimeout ( pending . timeout ) ;
64- pendingRequests . current . delete ( msg . requestId ) ;
80+ if ( msg . success ) {
81+ pending . resolve ( msg . data ) ;
82+ } else {
83+ pending . reject ( new Error ( msg . error || "Request failed" ) ) ;
84+ }
85+ return ;
86+ }
6587
66- if ( msg . success ) {
67- pending . resolve ( msg . data ) ;
68- } else {
69- pending . reject ( new Error ( msg . error || "Request failed" ) ) ;
88+ // Notification handling (has type, no requestId)
89+ if ( "type" in msg && ! ( "requestId" in msg ) ) {
90+ const handlers = notificationHandlers . current . get ( msg . type ) ;
91+ if ( handlers ) {
92+ for ( const h of handlers ) {
93+ h ( msg . data ) ;
94+ }
95+ }
7096 }
7197 } ;
7298
@@ -78,7 +104,6 @@ export function useIpc(options: UseIpcOptions = {}) {
78104 function request < P , R > (
79105 definition : {
80106 method : string ;
81- scope ?: string ;
82107 _types ?: { params : P ; response : R } ;
83108 } ,
84109 ...args : P extends void ? [ ] : [ params : P ]
@@ -102,7 +127,6 @@ export function useIpc(options: UseIpcOptions = {}) {
102127
103128 postMessage ( {
104129 method : definition . method ,
105- scope : scope ?? definition . scope ,
106130 requestId,
107131 params,
108132 } ) ;
@@ -111,15 +135,51 @@ export function useIpc(options: UseIpcOptions = {}) {
111135
112136 /** Send command without waiting (fire-and-forget) */
113137 function command < P > (
114- definition : { method : string ; scope ?: string ; _types ?: { params : P } } ,
138+ definition : { method : string ; _types ?: { params : P } } ,
115139 ...args : P extends void ? [ ] : [ params : P ]
116140 ) : void {
117141 postMessage ( {
118142 method : definition . method ,
119- scope : scope ?? definition . scope ,
120143 params : args [ 0 ] ,
121144 } ) ;
122145 }
123146
124- return { request, command } ;
147+ /**
148+ * Subscribe to push notifications from the extension.
149+ * Returns an unsubscribe function that should be called on cleanup.
150+ *
151+ * @example
152+ * ```tsx
153+ * useEffect(() => {
154+ * return ipc.onNotification(tasksUpdated, (tasks) => {
155+ * setTasks(tasks);
156+ * });
157+ * }, []);
158+ * ```
159+ */
160+ function onNotification < D > (
161+ definition : { method : string ; _types ?: { data : D } } ,
162+ callback : ( data : D ) => void ,
163+ ) : ( ) => void {
164+ const method = definition . method ;
165+ let handlers = notificationHandlers . current . get ( method ) ;
166+ if ( ! handlers ) {
167+ handlers = new Set ( ) ;
168+ notificationHandlers . current . set ( method , handlers ) ;
169+ }
170+ handlers . add ( callback as NotificationHandler ) ;
171+
172+ // Return unsubscribe function
173+ return ( ) => {
174+ const h = notificationHandlers . current . get ( method ) ;
175+ if ( h ) {
176+ h . delete ( callback as NotificationHandler ) ;
177+ if ( h . size === 0 ) {
178+ notificationHandlers . current . delete ( method ) ;
179+ }
180+ }
181+ } ;
182+ }
183+
184+ return { request, command, onNotification } ;
125185}
0 commit comments