Skip to content
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

refactor(thread): refactor hasCapability call with a better readable way #3732

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
65467d8
to: adding a draft approach
Sma1lboy Jan 20, 2025
f7fa804
refactor(api): improve type definitions for ClientApiMethods and Supp…
Sma1lboy Jan 20, 2025
4b83730
refactor(chat): convert client creation to async and enhance logging
Sma1lboy Jan 21, 2025
9d8fbbe
refactor(iframe): enhance method exchange and logging in thread creation
Sma1lboy Jan 21, 2025
02e80a6
refactor(api): update SupportProxy type to use boolean and enhance cr…
Sma1lboy Jan 21, 2025
9e2fc10
refactor(api): update exchangeMethods to return a Promise and enhance…
Sma1lboy Jan 22, 2025
1024b74
refactor(chat-panel): remove unused eslint-disable for console statem…
Sma1lboy Jan 22, 2025
986119f
refactor(client): simplify createClient function by returning thread …
Sma1lboy Jan 22, 2025
524913d
refactor(iframe): streamline message event listener by removing unnec…
Sma1lboy Jan 22, 2025
77f14b6
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 22, 2025
2daa34d
refactor(target): remove exchangeMethods function and related references
Sma1lboy Jan 22, 2025
5312275
refactor(client): simplify createClient and related functions by remo…
Sma1lboy Jan 24, 2025
ea16035
refactor(iframe): streamline thread creation and message handling in …
Sma1lboy Jan 24, 2025
d03eeb5
refactor(iframe): remove unnecessary line break in createThreadFromIf…
Sma1lboy Jan 24, 2025
f7412bc
refactor(chat): remove unnecessary console log for session state support
Sma1lboy Jan 24, 2025
03f11bf
refactor(client): remove unused support proxy type and simplify metho…
Sma1lboy Jan 24, 2025
2d177e7
refactor(target): simplify listener and property handler logic by rem…
Sma1lboy Jan 24, 2025
17859e1
refactor(client): remove unnecessary async keyword from createServer …
Sma1lboy Jan 24, 2025
911f783
refactor(target): enhance property handler logic by adding undefined …
Sma1lboy Jan 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion clients/tabby-chat-panel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ export function createClient(target: HTMLIFrameElement, api: ClientApiMethods):
})
}

export function createServer(api: ServerApi): ClientApi {
export function createServer(api: ServerApi): Promise<ClientApi> {
return createThreadFromInsideIframe({
expose: {
init: api.init,
Expand Down
2 changes: 1 addition & 1 deletion clients/tabby-chat-panel/src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function useServer(api: ServerApi) {
const isInIframe = window.self !== window.top
if (isInIframe && !isCreated) {
isCreated = true
setServer(createServer(api))
createServer(api).then(setServer)
}
}, [])

Expand Down
6 changes: 4 additions & 2 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 Down Expand Up @@ -74,7 +74,7 @@ export function createThreadFromInsideIframe<
);
}

return createThread(
const thread = createThread(
{
send(message, transfer) {
return parent.postMessage(message, targetOrigin, transfer);
Expand All @@ -92,4 +92,6 @@ export function createThreadFromInsideIframe<
},
options
);
await thread.requestMethods();
return thread;
}
213 changes: 155 additions & 58 deletions clients/tabby-threads/source/targets/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import type {
ThreadEncoder,
ThreadEncoderApi,
AnyFunction,
} from "../types.ts";

import {
RELEASE_METHOD,
RETAINED_BY,
RETAIN_METHOD,
StackFrame,
isMemoryManageable,
} from "../memory";
} from "../types";

import { RELEASE_METHOD, RETAINED_BY, RETAIN_METHOD } from "../constants";

import { StackFrame, isMemoryManageable } from "../memory";
import { createBasicEncoder } from "../encoding/basic";
import { RESPONSE_MESSAGE } from "./iframe/shared";

export type { ThreadTarget };

Expand Down Expand Up @@ -69,6 +66,7 @@ const RELEASE = 3;
const FUNCTION_APPLY = 5;
const FUNCTION_RESULT = 6;
const CHECK_CAPABILITY = 7;
const EXPOSE_LIST = 8;

interface MessageMap {
[CALL]: [string, string | number, any];
Expand All @@ -78,6 +76,7 @@ interface MessageMap {
[FUNCTION_APPLY]: [string, string, any];
[FUNCTION_RESULT]: [string, Error?, any?];
[CHECK_CAPABILITY]: [string, string];
[EXPOSE_LIST]: [string, string[]];
}

type MessageData = {
Expand Down Expand Up @@ -107,6 +106,9 @@ export function createThread<
const idsToFunction = new Map<string, AnyFunction>();
const idsToProxy = new Map<string, AnyFunction>();

// Cache for the other side's methods
let theirMethodsCache: string[] | null = null;

if (expose) {
for (const key of Object.keys(expose)) {
const value = expose[key as keyof typeof expose];
Expand All @@ -121,7 +123,17 @@ export function createThread<
) => void
>();

const call = createCallable<Thread<Target>>(handlerForCall, callable);
// Create proxy for method calls
const call = createCallable<Thread<Target>>(
handlerForCall,
callable,
{
requestMethods,
},
{
isTerminated: () => terminated,
}
);

const encoderApi: ThreadEncoderApi = {
functions: {
Expand Down Expand Up @@ -207,6 +219,7 @@ export function createThread<
functionsToId.clear();
idsToFunction.clear();
idsToProxy.clear();
theirMethodsCache = null; // Clear the cache when connection is terminated
};

signal?.addEventListener(
Expand All @@ -231,7 +244,38 @@ export function createThread<
target.send([type, args], transferables);
}

// Create a function to request methods from the other side
async function requestMethods() {
// If we have cached methods and connection is still active, return them
if (theirMethodsCache !== null && !terminated) {
return theirMethodsCache;
}

const id = uuid();

// Create a promise that will resolve with the other side's methods
const methodsPromise = new Promise<string[]>((resolve) => {
callIdsToResolver.set(id, (_, __, value) => {
const theirMethods = encoder.decode(value, encoderApi) as string[];
// Cache the methods for future use
theirMethodsCache = theirMethods;
resolve(theirMethods);
});
});

// Send EXPOSE_LIST with empty methods array to request other side's methods
send(EXPOSE_LIST, [id, []]);

return methodsPromise;
}

async function listener(rawData: unknown) {
// this method receives messages from the other side means the other side is ready
if (rawData === RESPONSE_MESSAGE) {
requestMethods().catch(() => {});
return;
}

const isThreadMessageData =
Array.isArray(rawData) &&
typeof rawData[0] === "number" &&
Expand Down Expand Up @@ -333,6 +377,23 @@ export function createThread<
send(RESULT, [id, undefined, encoder.encode(hasMethod, encoderApi)[0]]);
break;
}
case EXPOSE_LIST: {
const [id, theirMethods] = data[1];

// Store their methods for future use
const theirMethodsList = theirMethods as string[];
// Save their methods in cache
theirMethodsCache = theirMethodsList;
// Send back our methods as RESULT
const ourMethods = Array.from(activeApi.keys()).map(String);

send(RESULT, [
id,
undefined,
encoder.encode(ourMethods, encoderApi)[0],
]);
break;
}
}
}

Expand All @@ -345,7 +406,7 @@ export function createThread<

if (typeof property !== "string" && typeof property !== "number") {
throw new Error(
`Cant call a symbol method on a thread: ${property.toString()}`
`Can't call a symbol method on a thread: ${property.toString()}`
);
}

Expand Down Expand Up @@ -410,6 +471,89 @@ export function createThread<
callIdsToResolver.delete(callId);
}
}

function createCallable<T>(
handlerForCall: (
property: string | number | symbol
) => AnyFunction | undefined,
callable?: (keyof T)[],
methods?: {
requestMethods: () => Promise<string[]>;
},
state?: {
isTerminated: () => boolean;
}
): T {
let call: any;

if (callable == null) {
if (typeof Proxy !== "function") {
throw new Error(
`You must pass an array of callable methods in environments without Proxies.`
);
}

const cache = new Map<
string | number | symbol,
AnyFunction | undefined
>();

call = new Proxy(
{},
{
get(_target, property) {
switch (property) {
// FIXME: remove this, now is hack way
case "then":
return undefined;
case "requestMethods":
return methods?.requestMethods;
case "hasCapability":
return handlerForCall(property);
default:
if (!theirMethodsCache?.includes(String(property))) {
return undefined;
}
if (cache.has(property)) {
return cache.get(property);
}

const handler = handlerForCall(property);
cache.set(property, handler);
return handler;
}
},
has(_target, property) {
if (property === "then" || property === "requestMethods") {
return true;
}
if (!state) {
return false;
}
const cache = theirMethodsCache;
console.log("cache for has", cache);
if (cache !== null && !state.isTerminated()) {
return cache.includes(String(property));
}
return false;
},
}
);
} else {
call = {};

for (const method of callable) {
Object.defineProperty(call, method, {
value: handlerForCall(method),
writable: false,
configurable: true,
enumerable: true,
});
}
}

return call;
}
}

class ThreadTerminatedError extends Error {
Expand All @@ -425,50 +569,3 @@ function defaultUuid() {
function uuidSegment() {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
}

function createCallable<T>(
handlerForCall: (
property: string | number | symbol
) => AnyFunction | undefined,
callable?: (keyof T)[]
): T {
let call: any;

if (callable == null) {
if (typeof Proxy !== "function") {
throw new Error(
`You must pass an array of callable methods in environments without Proxies.`
);
}

const cache = new Map<string | number | symbol, AnyFunction | undefined>();

call = new Proxy(
{},
{
get(_target, property) {
if (cache.has(property)) {
return cache.get(property);
}

const handler = handlerForCall(property);
cache.set(property, handler);
return handler;
},
}
);
} else {
call = {};

for (const method of callable) {
Object.defineProperty(call, method, {
value: handlerForCall(method),
writable: false,
configurable: true,
enumerable: true,
});
}
}

return call;
}
14 changes: 11 additions & 3 deletions clients/tabby-threads/source/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
RETAIN_METHOD,
ENCODE_METHOD,
RETAINED_BY,
} from './constants.ts';
} from "./constants.ts";

/**
* A thread represents a target JavaScript environment that exposes a set
Expand All @@ -18,6 +18,14 @@ export type Thread<Target> = {
? Target[K]
: never
: never;
} & {
/**
* Request methods from the other side and wait for response.
* Returns a promise that resolves with the list of available methods.
* This is useful when you want to get the methods list from the other side
* without sending your own methods.
*/
requestMethods(): Promise<string[]>;
};

/**
Expand Down Expand Up @@ -82,7 +90,7 @@ export interface ThreadEncoder {
decode(
value: unknown,
api: ThreadEncoderApi,
retainedBy?: Iterable<MemoryRetainer>,
retainedBy?: Iterable<MemoryRetainer>
): unknown;
}

Expand Down Expand Up @@ -112,7 +120,7 @@ export interface ThreadEncoderApi {
* An object that provides a custom process to encode its value.
*/
export interface ThreadEncodable {
[ENCODE_METHOD](api: {encode(value: any): unknown}): any;
[ENCODE_METHOD](api: { encode(value: any): unknown }): any;
}

export type AnyFunction = Function;
Loading