Skip to content

Commit

Permalink
refactor(chat): enhance client and server initialization with async/a…
Browse files Browse the repository at this point in the history
…wait and logging
  • Loading branch information
Sma1lboy committed Jan 21, 2025
1 parent f7fa804 commit 6edd0dd
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 110 deletions.
64 changes: 7 additions & 57 deletions clients/tabby-chat-panel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,9 @@ export interface ClientApi extends ClientApiMethods {
supports: SupportProxy
}

export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): ServerApi {
return createThreadFromIframe(target, {
// TODO: change this to async
export async function createClient(target: HTMLIFrameElement, api: ClientApiMethods): Promise<ServerApi> {
const thread = await createThreadFromIframe(target, {
expose: {
refresh: api.refresh,
onApplyInEditor: api.onApplyInEditor,
Expand All @@ -388,10 +389,11 @@ export function createClient(target: HTMLIFrameElement, api: ClientApiMethods):
readFileContent: api.readFileContent,
},
})
return thread as unknown as ServerApi
}

export function createServer(api: ServerApi): ClientApi {
const clientApi = createThreadFromInsideIframe({
export async function createServer(api: ServerApi): Promise<ClientApi> {
const clientApi = await createThreadFromInsideIframe({
expose: {
init: api.init,
executeCommand: api.executeCommand,
Expand All @@ -402,76 +404,24 @@ export function createServer(api: ServerApi): ClientApi {
updateActiveSelection: api.updateActiveSelection,
},
}) as unknown as ClientApi

console.log('clientApi', Object.keys(clientApi))

const supportCache = new Map<string, boolean>()
let cacheInitialized = false

const initializeCache = async () => {
if (cacheInitialized)
return
console.log('Initializing cache...')
try {
// TODO: remove this workaround after the server is updated
const methods = [
'refresh',
'onApplyInEditor',
'onApplyInEditorV2',
'onLoaded',
'onCopy',
'onKeyboardEvent',
'lookupSymbol',
'openInEditor',
'openExternal',
'readWorkspaceGitRepositories',
'getActiveEditorSelection',
]

await Promise.all(
methods.map(async (method) => {
try {
console.log('Checking method:', method)
const supported = await clientApi.hasCapability(method as keyof ClientApiMethods)
supportCache.set(method, supported)
console.log(`Method ${method} supported:`, supported)
}
catch (e) {
console.log('Error checking method:', method, e)
supportCache.set(method, false)
}
}),
)
cacheInitialized = true
console.log('Cache initialized:', supportCache)
}
catch (e) {
console.error('Failed to initialize cache:', e)
}
}

initializeCache()

return new Proxy(clientApi, {
get(target, property, receiver) {
// Approach 1: use supports keyword to check if the method is supported
// support get and has method for supports
// get method for 'supports' operator e.g. server.supports['refresh'].then(setSupportRefresh)
// has for 'in' operator if('refresh' in server.supports) { ... }
// TODO: put this part into createThread{}
if (property === 'supports') {
return new Proxy({}, {
get: async (_target, capability: string) => {
const cleanCapability = capability.replace('?', '')
await initializeCache()
return supportCache.has(cleanCapability)
? supportCache.get(cleanCapability)
: false
},
has: (_target, capability: string) => {
// FIXME: bug here, always return false when server just load, need to fix
const cleanCapability = capability.replace('?', '')
if (!cacheInitialized)
return false
return supportCache.has(cleanCapability)
? supportCache.get(cleanCapability) ?? false
: false
Expand Down
29 changes: 22 additions & 7 deletions clients/tabby-chat-panel/src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ function useClient(iframeRef: RefObject<HTMLIFrameElement>, api: ClientApiMethod
let isCreated = false

useEffect(() => {
if (iframeRef.current && !isCreated) {
isCreated = true
setClient(createClient(iframeRef.current!, api))
const init = async () => {
if (iframeRef.current && !isCreated) {
isCreated = true
setClient(await createClient(iframeRef.current!, api))
}
}
init()
}, [iframeRef.current])

return client
Expand All @@ -23,11 +26,23 @@ function useServer(api: ServerApi) {
let isCreated = false

useEffect(() => {
const isInIframe = window.self !== window.top
if (isInIframe && !isCreated) {
isCreated = true
setServer(createServer(api))
// TODO: solving .then proxy issue
const init = async () => {
const isInIframe = window.self !== window.top
// eslint-disable-next-line no-console
console.log('[useServer] isInIframe:', isInIframe)
if (isInIframe && !isCreated) {
isCreated = true
// eslint-disable-next-line no-console
console.log('[useServer] Creating server...')
setServer(await createServer(api))
// eslint-disable-next-line no-console
console.log('[useServer] Server created:', server)
}
}
// eslint-disable-next-line no-console
console.log('[useServer] Starting initialization...')
init()
}, [])

return server
Expand Down
65 changes: 59 additions & 6 deletions clients/tabby-threads/source/targets/iframe/iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { CHECK_MESSAGE, RESPONSE_MESSAGE } from "./shared";
* const thread = createThreadFromInsideIframe(iframe);
* await thread.sendMessage('Hello world!');
*/
export function createThreadFromIframe<
export async function createThreadFromIframe<
Self = Record<string, never>,
Target = Record<string, never>,
>(
Expand All @@ -35,8 +35,13 @@ export function createThreadFromIframe<
} = {}
) {
let connected = false;
console.log(
"[createThreadFromIframe] Starting connection process with iframe:",
iframe
);

const sendMessage: ThreadTarget["send"] = function send(message, transfer) {
console.log("[createThreadFromIframe] Sending message:", message);
iframe.contentWindow?.postMessage(message, targetOrigin, transfer);
};

Expand All @@ -45,12 +50,22 @@ export function createThreadFromIframe<
? new NestedAbortController(options.signal)
: new AbortController();

console.log("[createThreadFromIframe] Setting up message listener");
window.addEventListener(
"message",
(event) => {
if (event.source !== iframe.contentWindow) return;
if (event.source !== iframe.contentWindow) {
console.log(
"[createThreadFromIframe] Ignoring message from unknown source"
);
return;
}

console.log("[createThreadFromIframe] Received message:", event.data);
if (event.data === RESPONSE_MESSAGE) {
console.log(
"[createThreadFromIframe] Received RESPONSE_MESSAGE, connection established"
);
connected = true;
abort.abort();
resolve();
Expand All @@ -62,31 +77,66 @@ export function createThreadFromIframe<
abort.signal.addEventListener(
"abort",
() => {
console.log("[createThreadFromIframe] Abort signal received");
resolve();
},
{ once: true }
);

console.log("[createThreadFromIframe] Sending CHECK_MESSAGE");
sendMessage(CHECK_MESSAGE);
});

return createThread(
console.log("[createThreadFromIframe] Waiting for connection...");
await connectedPromise;
console.log(
"[createThreadFromIframe] Connection established, creating thread"
);

const thread = await createThread(
{
send(message, transfer) {
if (!connected) {
console.log(
"[createThreadFromIframe] Message queued until connection:",
message
);
return connectedPromise.then(() => {
if (connected) return sendMessage(message, transfer);
if (connected) {
console.log(
"[createThreadFromIframe] Sending queued message:",
message
);
return sendMessage(message, transfer);
}
console.log(
"[createThreadFromIframe] Connection lost, message dropped:",
message
);
});
}

return sendMessage(message, transfer);
},
listen(listen, { signal }) {
console.log("[createThreadFromIframe] Setting up message listener");
self.addEventListener(
"message",
(event) => {
if (event.source !== iframe.contentWindow) return;
if (event.data === RESPONSE_MESSAGE) return;
if (event.source !== iframe.contentWindow) {
console.log(
"[createThreadFromIframe] Ignoring message from unknown source"
);
return;
}
if (event.data === RESPONSE_MESSAGE) {
console.log("[createThreadFromIframe] Ignoring RESPONSE_MESSAGE");
return;
}
console.log(
"[createThreadFromIframe] Received message:",
event.data
);
listen(event.data);
},
{ signal }
Expand All @@ -95,4 +145,7 @@ export function createThreadFromIframe<
},
options
);

console.log("[createThreadFromIframe] Thread created successfully");
return thread;
}
83 changes: 59 additions & 24 deletions clients/tabby-threads/source/targets/iframe/nested.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { CHECK_MESSAGE, RESPONSE_MESSAGE } from "./shared";
* const thread = createThreadFromInsideIframe();
* await thread.sendMessage('Hello world!');
*/
export function createThreadFromInsideIframe<
export async function createThreadFromInsideIframe<
Self = Record<string, never>,
Target = Record<string, never>,
>({
Expand All @@ -42,48 +42,80 @@ export function createThreadFromInsideIframe<
? new NestedAbortController(options.signal)
: new AbortController();

const ready = () => {
const respond = () => parent.postMessage(RESPONSE_MESSAGE, targetOrigin);
console.log("[createThreadFromInsideIframe] Starting connection process");

const connectionPromise = new Promise<void>((resolve) => {
let isConnected = false;

const respond = () => {
if (!isConnected) {
console.log("[createThreadFromInsideIframe] Sending RESPONSE_MESSAGE");
isConnected = true;
parent.postMessage(RESPONSE_MESSAGE, targetOrigin);
resolve();
}
};

// Handles wrappers that want to connect after the page has already loaded
self.addEventListener(
"message",
({ data }) => {
if (data === CHECK_MESSAGE) respond();
console.log("[createThreadFromInsideIframe] Received message:", data);
if (data === CHECK_MESSAGE) {
console.log("[createThreadFromInsideIframe] Received CHECK_MESSAGE");
respond();
}
},
{ signal: options.signal }
);

respond();
};
if (document.readyState === "complete") {
console.log(
"[createThreadFromInsideIframe] Document already complete, responding"
);
respond();
} else {
console.log(
"[createThreadFromInsideIframe] Waiting for document to complete"
);
document.addEventListener(
"readystatechange",
() => {
if (document.readyState === "complete") {
console.log(
"[createThreadFromInsideIframe] Document completed, responding"
);
respond();
abort.abort();
}
},
{ signal: abort.signal }
);
}
});

// Listening to `readyState` in iframe, though the child iframe could probably
// send a `postMessage` that it is ready to receive messages sooner than that.
if (document.readyState === "complete") {
ready();
} else {
document.addEventListener(
"readystatechange",
() => {
if (document.readyState === "complete") {
ready();
abort.abort();
}
},
{ signal: abort.signal }
);
}
await connectionPromise;
console.log(
"[createThreadFromInsideIframe] Connection established, creating thread"
);

return createThread(
const thread = await createThread(
{
send(message, transfer) {
console.log("[createThreadFromInsideIframe] Sending message:", message);
return parent.postMessage(message, targetOrigin, transfer);
},
listen(listen, { signal }) {
console.log(
"[createThreadFromInsideIframe] Setting up message listener"
);
self.addEventListener(
"message",
(event) => {
if (event.data === CHECK_MESSAGE) return;
console.log(
"[createThreadFromInsideIframe] Received message:",
event.data
);
listen(event.data);
},
{ signal }
Expand All @@ -92,4 +124,7 @@ export function createThreadFromInsideIframe<
},
options
);

console.log("[createThreadFromInsideIframe] Thread created successfully");
return thread;
}
Loading

0 comments on commit 6edd0dd

Please sign in to comment.