-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWebviewProvider.tsx
More file actions
198 lines (177 loc) · 6.34 KB
/
WebviewProvider.tsx
File metadata and controls
198 lines (177 loc) · 6.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createContext, useEffect, useMemo, useRef } from 'react';
import {
isViewApiError,
isViewApiEvent,
isViewApiResponse,
type ClientCalls,
type CtxKey,
type HostCalls,
type RequestContext,
type ViewApiRequest,
} from '../types';
import { type VsCodeApi } from './types';
import { generateId } from '../utils';
import { DeferredPromise } from './types';
import { TypedContexts } from './useWebviewApi';
import type { WebviewContextValue } from './WebviewContext';
declare function acquireVsCodeApi(): VsCodeApi;
const vscodeApi = acquireVsCodeApi();
interface WebviewProviderProps<T extends ClientCalls> {
viewType: string;
children: React.ReactNode;
contextKey: CtxKey<T>;
}
/**
* WebviewProvider provides type-safe API access to webview components
*/
export const WebviewProvider = <T extends ClientCalls, H extends HostCalls>({
children,
viewType,
contextKey,
}: WebviewProviderProps<T>) => {
const pendingRequests = useRef<Map<string, DeferredPromise<any>>>(new Map());
const listeners = useRef<Map<keyof HostCalls, Set<(...args: any[]) => void>>>(new Map());
// Generate context for this webview instance
const contextRef = useRef<RequestContext>({
viewId: generateId('webview'),
viewType: viewType,
timestamp: Date.now(),
sessionId: generateId('session'),
});
/**
* Type-safe API caller with request/response matching
*/
const callApi = <K extends keyof T = keyof T>(
key: K,
...params: Parameters<T[K]>
): ReturnType<T[K]> => {
const id = generateId('req');
const deferred = new DeferredPromise<Awaited<ReturnType<T[K]>>>(key as string);
const request: ViewApiRequest<T, K> = {
type: 'request',
id,
key,
params,
context: contextRef.current,
};
pendingRequests.current.set(id, deferred);
// Send the request
// eslint-disable-next-line sonarjs/no-try-promise
try {
vscodeApi.postMessage(request);
} catch (error) {
console.error(`Failed to send API request ${key as string}:`, error);
deferred.clearTimeout();
pendingRequests.current.delete(id);
deferred.reject(error instanceof Error ? error : new Error(String(error)));
}
return deferred.promise as ReturnType<T[K]>;
};
/**
* Create typed API object using Proxy for dynamic method access
*/
const api = new Proxy({} as WebviewContextValue<T>['api'], {
// eslint-disable-next-line code-complete/enforce-meaningful-names
get: (_, key: string) => {
return (...args: any[]) => {
// Type assertion is safe here because the proxy ensures correct typing at usage
return callApi(key, ...(args as Parameters<T[keyof T]>));
};
},
});
/**
* Add an event listener with type safety
*/
const addListener = <E extends keyof HostCalls>(key: E, callback: HostCalls[E]): void => {
if (!listeners.current.has(key)) {
listeners.current.set(key, new Set());
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
listeners.current.get(key)!.add(callback as (...args: any[]) => void);
};
/**
* Remove an event listener
*/
const removeListener = <E extends keyof HostCalls>(key: E, callback: HostCalls[E]): void => {
listeners.current.get(key)?.delete(callback as (...args: any[]) => void);
};
/**
* Handle messages from the extension host
*/
useEffect(() => {
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleMessage = (event: MessageEvent<unknown>) => {
const message = event.data;
if (isViewApiResponse(message)) {
const deferred = pendingRequests.current.get(message.id);
if (deferred !== undefined) {
deferred.clearTimeout(); // Clear timeout to prevent race condition
pendingRequests.current.delete(message.id);
deferred.resolve(message.value);
}
} else if (isViewApiError(message)) {
// Handle API error
const deferred = pendingRequests.current.get(message.id);
if (deferred) {
console.error('API error received for request %s:', message.id, message.value);
deferred.clearTimeout(); // Clear timeout to prevent race condition
pendingRequests.current.delete(message.id);
deferred.reject(new Error(message.value));
} else {
console.warn(`No pending request found for error ID: ${message.id}`);
}
} else if (isViewApiEvent<H>(message)) {
// Handle event
const callbacks = listeners.current.get(message.key as keyof HostCalls);
if (callbacks && callbacks.size > 0) {
for (const callback of callbacks) {
try {
callback(...message.value);
} catch (error) {
console.error('Error in event listener for %s:', message.key, error);
}
}
}
} else if (message !== null && typeof message === 'object' && 'providerId' in message) {
// No-op, handled by new IPC system
} else {
// Handle legacy messages that don't follow the new format
// This ensures compatibility during migration
console.error('Received legacy message format:', message);
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);
/**
* Cleanup pending requests on unmount
*/
useEffect(() => {
const currentRequests = pendingRequests.current;
return () => {
// Clear timeouts and reject all pending requests
for (const deferred of currentRequests.values()) {
deferred.clearTimeout(); // Clear timeout to prevent late firing
deferred.reject(new Error('WebviewProvider unmounted')); // Reject first while not settled
deferred.markSettled(); // Then mark as settled to prevent subsequent resolve/reject calls
}
currentRequests.clear();
};
}, []);
const context = useMemo(() => {
const context = createContext<WebviewContextValue<T> | undefined>(undefined);
TypedContexts.set(contextKey, context);
return context;
}, [contextKey]);
const contextValue: WebviewContextValue<T> = {
api,
addListener,
removeListener,
isReady: true,
vscode: vscodeApi,
};
return <context.Provider value={contextValue}>{children}</context.Provider>;
};