refactor(sidecar): create shared RPC module for async requests

This commit introduces a new `rpc.ts` module to centralize the asynchronous request/response logic between the sidecar and the main Tauri process. Previously, files like `clipboard.ts`, `environment.ts`, and `browserExtension.ts` each had their own duplicate implementation for managing pending requests and timeouts. By abstracting this boilerplate into a single RPC utility, we eliminate redundant code, reduce the surface area for bugs, and simplify the creation of future native API bridges.
This commit is contained in:
ByteAtATime 2025-06-24 09:52:41 -07:00
parent 06f823873c
commit 8218e39ae7
No known key found for this signature in database
5 changed files with 67 additions and 164 deletions

View file

@ -1,39 +1,4 @@
import { writeOutput } from '../io';
import * as crypto from 'crypto';
const pendingRequests = new Map<
string,
{ resolve: (value: unknown) => void; reject: (reason?: unknown) => void }
>();
export function handleBrowserExtensionResponse(requestId: string, result: unknown, error?: string) {
const promise = pendingRequests.get(requestId);
if (promise) {
if (error) {
promise.reject(new Error(error));
} else {
promise.resolve(result);
}
pendingRequests.delete(requestId);
}
}
function sendRequest<T>(method: string, params: unknown): Promise<T> {
return new Promise((resolve, reject) => {
const requestId = crypto.randomUUID();
pendingRequests.set(requestId, { resolve: resolve as (value: unknown) => void, reject });
writeOutput({
type: 'browser-extension-request',
payload: { requestId, method, params }
});
setTimeout(() => {
if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId);
reject(new Error(`Request for ${method} timed out`));
}
}, 5000);
});
}
import { sendRequest } from './rpc';
type Tab = {
active: boolean;
@ -51,9 +16,13 @@ type RawTab = {
active: boolean;
};
const sendBrowserRequest = <T>(method: string, params: unknown) => {
return sendRequest<T>('browser-extension-request', { method, params });
};
export const BrowserExtensionAPI = {
async getTabs(): Promise<Tab[]> {
const result = await sendRequest<{ value: RawTab[] }>('getTabs', {});
const result = await sendBrowserRequest<{ value: RawTab[] }>('getTabs', {});
return result.value.map((tab) => ({
id: tab.tabId,
url: tab.url,
@ -84,7 +53,7 @@ export const BrowserExtensionAPI = {
params.tabId = options.tabId;
}
const result = await sendRequest<{ value: string }>('getTab', params);
const result = await sendBrowserRequest<{ value: string }>('getTab', params);
return result.value;
}
};

View file

@ -1,5 +1,4 @@
import { writeOutput } from '../io';
import * as crypto from 'crypto';
import { sendRequest } from './rpc';
import type * as api from '@raycast/api';
type ClipboardContent = {
@ -14,40 +13,6 @@ type ReadResult = {
file?: string;
};
const pendingRequests = new Map<
string,
{ resolve: (value: unknown) => void; reject: (reason?: unknown) => void }
>();
function sendRequest<T>(type: string, payload: object): Promise<T> {
return new Promise((resolve, reject) => {
const requestId = crypto.randomUUID();
pendingRequests.set(requestId, { resolve: resolve as (value: unknown) => void, reject });
writeOutput({
type,
payload: { requestId, ...payload }
});
setTimeout(() => {
if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId);
reject(new Error(`Request for ${type} timed out`));
}
}, 5000);
});
}
export function handleClipboardResponse(requestId: string, result: unknown, error?: string) {
const promise = pendingRequests.get(requestId);
if (promise) {
if (error) {
promise.reject(new Error(error));
} else {
promise.resolve(result);
}
pendingRequests.delete(requestId);
}
}
function normalizeContent(content: string | number | api.Clipboard.Content): ClipboardContent {
if (typeof content === 'string' || typeof content === 'number') {
return { text: String(content) };

View file

@ -4,7 +4,7 @@ import { writeOutput } from '../io';
import type { Application } from './types';
import { config } from '../config';
import { browserExtensionState } from '../state';
import * as crypto from 'crypto';
import { sendRequest } from './rpc';
const supportPath = config.supportDir;
try {
@ -21,40 +21,6 @@ export interface FileSystemItem {
export const BrowserExtension = { name: 'BrowserExtension' };
const pendingSystemRequests = new Map<
string,
{ resolve: (value: unknown) => void; reject: (reason?: unknown) => void }
>();
function sendSystemRequest<T>(type: string, payload: object = {}): Promise<T> {
return new Promise((resolve, reject) => {
const requestId = crypto.randomUUID();
pendingSystemRequests.set(requestId, { resolve, reject });
writeOutput({
type: `system-${type}`,
payload: { requestId, ...payload }
});
setTimeout(() => {
if (pendingSystemRequests.has(requestId)) {
pendingSystemRequests.delete(requestId);
reject(new Error(`Request for ${type} timed out`));
}
}, 5000); // 5-second timeout
});
}
export function handleSystemResponse(requestId: string, result: unknown, error?: string) {
const promise = pendingSystemRequests.get(requestId);
if (promise) {
if (error) {
promise.reject(new Error(error));
} else {
promise.resolve(result);
}
pendingSystemRequests.delete(requestId);
}
}
export const environment = {
appearance: 'dark' as const,
assetsPath: config.assetsDir,
@ -76,11 +42,11 @@ export const environment = {
};
export async function getSelectedFinderItems(): Promise<FileSystemItem[]> {
return sendSystemRequest<FileSystemItem[]>('get-selected-finder-items');
return sendRequest<FileSystemItem[]>('system-get-selected-finder-items');
}
export async function getSelectedText(): Promise<string> {
return sendSystemRequest<string>('get-selected-text');
return sendRequest<string>('system-get-selected-text');
}
export async function open(target: string, application?: Application | string): Promise<void> {
@ -103,22 +69,22 @@ export async function open(target: string, application?: Application | string):
export async function getApplications(path?: fs.PathLike): Promise<Application[]> {
const pathString = path ? path.toString() : undefined;
return sendSystemRequest<Application[]>('get-applications', { path: pathString });
return sendRequest<Application[]>('system-get-applications', { path: pathString });
}
export async function getDefaultApplication(path: fs.PathLike): Promise<Application> {
return sendSystemRequest<Application>('get-default-application', { path: path.toString() });
return sendRequest<Application>('system-get-default-application', { path: path.toString() });
}
export async function getFrontmostApplication(): Promise<Application> {
return sendSystemRequest<Application>('get-frontmost-application');
return sendRequest<Application>('system-get-frontmost-application');
}
export async function showInFinder(path: fs.PathLike): Promise<void> {
return sendSystemRequest<void>('show-in-finder', { path: path.toString() });
return sendRequest<void>('system-show-in-finder', { path: path.toString() });
}
export async function trash(path: fs.PathLike | fs.PathLike[]): Promise<void> {
const paths = (Array.isArray(path) ? path : [path]).map((p) => p.toString());
return sendSystemRequest<void>('trash', { paths });
return sendRequest<void>('system-trash', { paths });
}

38
sidecar/src/api/rpc.ts Normal file
View file

@ -0,0 +1,38 @@
import { writeOutput } from '../io';
import * as crypto from 'crypto';
const pendingRequests = new Map<
string,
{ resolve: (value: unknown) => void; reject: (reason?: unknown) => void }
>();
export function sendRequest<T>(type: string, payload: object = {}): Promise<T> {
return new Promise((resolve, reject) => {
const requestId = crypto.randomUUID();
pendingRequests.set(requestId, { resolve: resolve as (value: unknown) => void, reject });
writeOutput({
type,
payload: { requestId, ...payload }
});
setTimeout(() => {
if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId);
reject(new Error(`Request for ${type} timed out`));
}
}, 5000);
});
}
export function handleResponse(requestId: string, result: unknown, error?: string) {
const promise = pendingRequests.get(requestId);
if (promise) {
if (error) {
promise.reject(new Error(error));
} else {
promise.resolve(result);
}
pendingRequests.delete(requestId);
}
}

View file

@ -5,9 +5,7 @@ import { instances, navigationStack, toasts, browserExtensionState } from './sta
import { batchedUpdates, updateContainer } from './reconciler';
import { preferencesStore } from './preferences';
import type { RaycastInstance } from './types';
import { handleSystemResponse } from './api/environment';
import { handleBrowserExtensionResponse } from './api/browserExtension';
import { handleClipboardResponse } from './api/clipboard';
import { handleResponse } from './api/rpc';
import { handleOAuthResponse, handleTokenResponse } from './api/oauth';
process.on('unhandledRejection', (reason: unknown) => {
@ -23,13 +21,22 @@ rl.on('line', (line) => {
try {
const command: { action: string; payload: unknown } = JSON.parse(line);
if (command.action.startsWith('system-') && command.action.endsWith('-response')) {
const { requestId, result, error } = command.payload as {
if (command.action.endsWith('-response')) {
const { requestId, result, error, state, code } = command.payload as {
requestId: string;
result?: unknown;
error?: string;
state?: string;
code?: string;
};
handleSystemResponse(requestId, result, error);
if (command.action === 'oauth-authorize-response') {
handleOAuthResponse(state!, code!, state, error);
} else if (command.action.startsWith('oauth-')) {
handleTokenResponse(requestId, result, error);
} else {
handleResponse(requestId, result, error);
}
return;
}
@ -126,53 +133,11 @@ rl.on('line', (line) => {
toast?.hide();
break;
}
case 'browser-extension-response': {
const { requestId, result, error } = command.payload as {
requestId: string;
result?: unknown;
error?: string;
};
handleBrowserExtensionResponse(requestId, result, error);
break;
}
case 'browser-extension-connection-status': {
const { isConnected } = command.payload as { isConnected: boolean };
browserExtensionState.isConnected = isConnected;
break;
}
case 'oauth-authorize-response': {
const { code, state, error } = command.payload as {
code: string;
state: string;
error?: string;
};
handleOAuthResponse(state, code, state, error);
break;
}
case 'oauth-get-tokens-response':
case 'oauth-set-tokens-response':
case 'oauth-remove-tokens-response': {
const { requestId, result, error } = command.payload as {
requestId: string;
result?: unknown;
error?: string;
};
handleTokenResponse(requestId, result, error);
break;
}
case 'clipboard-read-text-response':
case 'clipboard-read-response':
case 'clipboard-copy-response':
case 'clipboard-paste-response':
case 'clipboard-clear-response': {
const { requestId, result, error } = command.payload as {
requestId: string;
result?: unknown;
error?: string;
};
handleClipboardResponse(requestId, result, error);
break;
}
default:
writeLog(`Unknown command action: ${command.action}`);
}