Skip to content

Commit

Permalink
refactor(messaging): post data through one-time requests (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmkx authored Feb 3, 2024
1 parent 6a16ada commit 11b6e75
Show file tree
Hide file tree
Showing 16 changed files with 241 additions and 457 deletions.
4 changes: 2 additions & 2 deletions apps/ai-assistant/src/content-scripts/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import {
isSelectionValid,
rangeToReference,
} from '@webx-kit/runtime/content-scripts';
import { createTrpcHandler } from '@webx-kit/messaging/content-script';
import { createTrpcClient } from '@webx-kit/messaging/content-script';
import clsx from 'clsx';
import type { AppRouter } from '@/background/router';
import { DialogTrigger, TooltipTrigger } from 'react-aria-components';
import { Button, Popover, Tooltip } from '@/components';
import { Provider } from './features/provider';
import './global.less';

const { client } = createTrpcHandler<AppRouter>({});
const { client } = createTrpcClient<AppRouter>({});

export const App = () => {
const [visible, setVisible] = useState(false);
Expand Down
8 changes: 3 additions & 5 deletions packages/messaging/demo/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { createCustomHandler } from '@/background';

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const { connections } = createCustomHandler({
// @ts-expect-error
globalThis.__messaging = createCustomHandler({
async requestHandler(message) {
return {
reply: 'background',
data: message.data,
data: message,
};
},
async streamHandler(_message, subscriber) {
Expand All @@ -19,6 +20,3 @@ const { connections } = createCustomHandler({
subscriber.complete();
},
});

// @ts-expect-error
globalThis.__webxConnections = connections;
4 changes: 2 additions & 2 deletions packages/messaging/demo/content-scripts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { messaging } = createCustomHandler({
requestHandler(message) {
return {
reply: 'content-script',
data: message.data,
data: message,
};
},
async streamHandler(_message, subscriber) {
Expand All @@ -21,4 +21,4 @@ const { messaging } = createCustomHandler({
});

// @ts-expect-error
globalThis.__client = messaging;
globalThis.__clientMessaging = messaging;
4 changes: 2 additions & 2 deletions packages/messaging/demo/pages/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { messaging } = createCustomHandler({
requestHandler(message) {
return {
reply: 'options',
data: message.data,
data: message,
};
},
async streamHandler(_message, subscriber) {
Expand All @@ -22,4 +22,4 @@ const { messaging } = createCustomHandler({
});

// @ts-expect-error
globalThis.__client = messaging;
globalThis.__clientMessaging = messaging;
4 changes: 2 additions & 2 deletions packages/messaging/demo/pages/popup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { messaging } = createCustomHandler({
requestHandler(message) {
return {
reply: 'popup',
data: message.data,
data: message,
};
},
async streamHandler(_message, subscriber) {
Expand All @@ -22,4 +22,4 @@ const { messaging } = createCustomHandler({
});

// @ts-expect-error
globalThis.__client = messaging;
globalThis.__clientMessaging = messaging;
33 changes: 22 additions & 11 deletions packages/messaging/e2e/basic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,54 @@
import { setupStaticServer } from '@webx-kit/test-utils/playwright';
import { expect, test } from './context';
import type { Messaging } from '@/core';
import type { WrappedMessaging } from '@/client-base';
import type { WrappedMessaging } from '@/shared';

const getWebpageURL = setupStaticServer(test);

declare module globalThis {
/** only in background */
const __webxConnections: Set<Messaging> | undefined;
const __client: WrappedMessaging;
const __messaging: Messaging;
const __clientMessaging: WrappedMessaging;
}

test('Background', async ({ background }) => {
await expect(background.evaluate(() => typeof globalThis.__webxConnections)).resolves.toBe('object');
await expect(background.evaluate(() => typeof globalThis.__messaging)).resolves.toBe('object');
});

test('Messaging', async ({ context, getURL }) => {
const optionsPage = await context.newPage();
const popupPage = await context.newPage();
const contentScript = await context.newPage();
const contentPage = await context.newPage();

await Promise.all([
optionsPage.goto(await getURL('options.html')),
popupPage.goto(await getURL('popup.html')),
contentScript.goto(getWebpageURL()),
contentPage.goto(getWebpageURL()),
]);

await expect(optionsPage.evaluate(() => globalThis.__client.request('options', 'popup'))).resolves.toEqual({
reply: 'popup',
await expect(optionsPage.evaluate(() => globalThis.__clientMessaging.request('options'))).resolves.toEqual({
reply: 'background',
data: 'options',
});

await expect(popupPage.evaluate(() => globalThis.__client.request('popup', 'options'))).resolves.toEqual({
await expect(popupPage.evaluate(() => globalThis.__clientMessaging.requestTo('options', 'popup'))).resolves.toEqual({
reply: 'options',
data: 'popup',
});

const [webpage] = await popupPage.evaluate(() =>
chrome.tabs.query({}).then((tabs) =>
tabs
.filter((tab) => tab.url?.startsWith('http'))
.map((tab) => ({
id: tab.id,
url: tab.url,
}))
)
);

await expect(
popupPage.evaluate(() => globalThis.__client.request('popup to content-script', 'content-script'))
popupPage.evaluate((tabId) => globalThis.__clientMessaging.requestTo(tabId!, 'popup to content-script'), webpage.id)
).resolves.toEqual({
reply: 'content-script',
data: 'popup to content-script',
Expand All @@ -52,7 +63,7 @@ test('Stream', async ({ context, getURL }) => {
const result = await optionsPage.evaluate(() => {
return new Promise<unknown[]>((resolve, reject) => {
const result: unknown[] = [];
globalThis.__client.stream('options', {
globalThis.__clientMessaging.stream('options', {
next: (value) => result.push(value),
error: reject,
complete: () => resolve(result),
Expand Down
1 change: 1 addition & 0 deletions packages/messaging/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
retries: 2,
timeout: 5000,
});
136 changes: 37 additions & 99 deletions packages/messaging/src/background.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,56 @@
import { AnyTRPCRouter } from '@trpc/server';
import { Messaging, createMessaging, fromChromePort } from './core';
import { Port, RequestHandler, StreamHandler, createMessaging } from './core';
import { applyMessagingHandler } from './core/trpc';
import { NAMESPACE, RequestHandler, StreamHandler, WebxMessage, isWebxMessage } from './shared';

export type WebxMessageMiddleware = (
message: WebxMessage,
port: chrome.runtime.Port
) => WebxMessage | false | void | Promise<WebxMessage | false | void>;

// #region middlewares
const senderInfoMiddleware: WebxMessageMiddleware = (message, port) => {
return {
from: port.name.slice(NAMESPACE.length),
tabId: port.sender?.tab?.id,
...message,
};
};
// #endregion

const middlewares = new Set<WebxMessageMiddleware>([senderInfoMiddleware]);

async function applyMiddlewares(message: WebxMessage, port: chrome.runtime.Port) {
for (const middleware of middlewares) {
const modifiedMessage = await middleware(message, port);
if (modifiedMessage === false) return message;
if (modifiedMessage) message = modifiedMessage;
}
return message;
}
import { WebxMessage, isWebxMessage } from './shared';

export interface CustomHandlerOptions {
requestHandler?: RequestHandler;
streamHandler?: StreamHandler;
}

export function createCustomHandler({
requestHandler = () => Promise.reject(),
streamHandler = (_, subscriber) => {
subscriber.error('unimplemented');
},
}: CustomHandlerOptions) {
const connections = new Set<Messaging>();

const listener = (port: chrome.runtime.Port): void => {
if (!port.name.startsWith(NAMESPACE)) return;
const messaging = createMessaging(fromChromePort(port), {
async onRequest(message) {
if (!isWebxMessage(message)) return Promise.reject('unknown message');
const webxMessage = await applyMiddlewares(message, port);
const NAME = 'background';

if (!webxMessage.to) return requestHandler(webxMessage);

for (const connection of connections) {
if (connection.name === port.name) continue;
if (connection.name.slice(NAMESPACE.length).startsWith(webxMessage.to)) {
return connection.request(webxMessage);
}
}

return Promise.reject('no target');
},
async onStream(message, subscriber) {
if (!isWebxMessage(message)) return subscriber.error('unknown message');
const webxMessage = await applyMiddlewares(message, port);

if (!webxMessage.to) return streamHandler(webxMessage, subscriber);
const backgroundPort: Port = {
name: NAME,
onMessage(listener) {
chrome.runtime.onMessage.addListener(listener);
return () => {
chrome.runtime.onMessage.removeListener(listener);
};
},
send(message, originMessage?: Parameters<Parameters<typeof chrome.runtime.onMessage.addListener>[0]>) {
if (!originMessage) return chrome.runtime.sendMessage(message);
const [, sender] = originMessage;
if (!sender.tab?.id) return chrome.runtime.sendMessage(message);
chrome.tabs.sendMessage(sender.tab.id, message, { documentId: sender.documentId, frameId: sender.frameId });
},
};

for (const connection of connections) {
if (connection.name === port.name) continue;
if (connection.name.slice(NAMESPACE.length).startsWith(webxMessage.to)) {
return connection.stream(webxMessage, subscriber);
}
}
function shouldSkip(data: unknown) {
if (!isWebxMessage(data)) return true;
return !(!data.to || data.to === NAME);
}

subscriber.error('no target');
},
onDispose() {
connections.delete(messaging);
},
});
connections.add(messaging);
};
chrome.runtime.onConnect.addListener(listener);
return {
connections,
dispose() {
chrome.runtime.onConnect.removeListener(listener);
},
};
export function createCustomHandler({ requestHandler, streamHandler }: CustomHandlerOptions) {
return createMessaging(backgroundPort, {
intercept: (data: WebxMessage, abort) => (shouldSkip(data) ? abort : data.data),
onRequest: requestHandler || (() => Promise.reject()),
onStream:
streamHandler ||
((_, subscriber) => {
subscriber.error('unimplemented');
}),
});
}

export interface TrpcHandlerOptions<TRouter extends AnyTRPCRouter> {
router: TRouter;
}

export function createTrpcHandler<TRouter extends AnyTRPCRouter>({ router }: TrpcHandlerOptions<TRouter>) {
const connections = new Set<Messaging>();

const listener = (port: chrome.runtime.Port): void => {
if (!port.name.startsWith(NAMESPACE)) return;
const messaging = applyMessagingHandler({
port: fromChromePort(port),
router,
onDispose() {
connections.delete(messaging);
},
});
connections.add(messaging);
};
chrome.runtime.onConnect.addListener(listener);
return {
connections,
dispose() {
chrome.runtime.onConnect.removeListener(listener);
},
};
return applyMessagingHandler({
port: backgroundPort,
router,
intercept: (data: WebxMessage, abort) => (shouldSkip(data) ? abort : data.data),
});
}
Loading

0 comments on commit 11b6e75

Please sign in to comment.