diff --git a/bun.lockb b/bun.lockb
index 68eb4fc..c5ace04 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/docs/content/messaging/api.md b/docs/content/messaging/api.md
index 19aa8a5..6275f8a 100644
--- a/docs/content/messaging/api.md
+++ b/docs/content/messaging/api.md
@@ -19,7 +19,7 @@ interface BaseMessagingConfig {
Shared configuration between all the different messengers.
-### Properties
+### Properties
- ***`logger?: Logger`*** (default: `console`)
The logger to use when logging messages. Set to `null` to disable logging.
@@ -35,7 +35,7 @@ interface CustomEventMessage {
Additional fields available on the `Message` from a `CustomEventMessenger`.
-### Properties
+### Properties
- ***`event: CustomEvent`***
The event that was fired, resulting in the message being passed.
@@ -153,7 +153,7 @@ interface ExtensionMessage {
Additional fields available on the `Message` from an `ExtensionMessenger`.
-### Properties
+### Properties
- ***`sender: Runtime.MessageSender`***
Information about where the message came from. See
[`Runtime.MessageSender`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender).
@@ -196,6 +196,12 @@ interface GenericMessenger<
TMessageExtension,
TSendMessageArgs extends any[],
> {
+ sendMessage(
+ type: TType,
+ ...args: GetDataType extends undefined
+ ? [data?: undefined, ...args: TSendMessageArgs]
+ : never
+ ): Promise>;
sendMessage(
type: TType,
data: GetDataType,
@@ -283,7 +289,7 @@ interface Message<
Contains information about the message received.
-### Properties
+### Properties
- ***`id: number`***
A semi-unique, auto-incrementing number used to trace messages being sent.
@@ -306,7 +312,7 @@ interface MessageSender {
An object containing information about the script context that sent a message or request.
-### Properties
+### Properties
- ***`tab?: Tabs.Tab`***
The $(ref:tabs.Tab) which opened the connection, if any. This property will only
be present when the connection was opened from a tab (including content scripts), and only
@@ -332,7 +338,7 @@ interface NamespaceMessagingConfig extends BaseMessagingConfig {
}
```
-### Properties
+### Properties
- ***`namespace: string`***
A string used to ensure the messenger only sends messages to and listens for messages from
other messengers of the same type, with the same namespace.
@@ -354,7 +360,7 @@ Used to add a return type to a message in the protocol map.
> Internally, this is just an object with random keys for the data and return types.
-### Properties
+### Properties
- ***`BtVgCTPYZu: TData`***
Stores the data type. Randomly named so that it isn't accidentally implemented.
@@ -392,7 +398,7 @@ interface SendMessageOptions {
Options for sending a message to a specific tab/frame
-### Properties
+### Properties
- ***`tabId: number`***
The tab to send a message to
@@ -429,4 +435,4 @@ details.
---
-_API reference generated by [`docs/generate-api-references.ts`](https://github.com/aklinker1/webext-core/blob/main/docs/generate-api-references.ts)_
+_API reference generated by [`docs/generate-api-references.ts`](https://github.com/aklinker1/webext-core/blob/main/docs/generate-api-references.ts)_
\ No newline at end of file
diff --git a/docs/content/proxy-service/0.installation.md b/docs/content/proxy-service/0.installation.md
index bf8512f..fcc32c3 100644
--- a/docs/content/proxy-service/0.installation.md
+++ b/docs/content/proxy-service/0.installation.md
@@ -17,17 +17,19 @@ class MathService {
...
}
}
-export const [registerMathService, getMathService] = defineProxyService(
- 'MathService',
- () => new MathService(),
-);
+export const [registerMathService, getMathService] = defineServiceProxy('MathService');
```
```ts [background.ts]
import { registerMathService } from './MathService';
// 2. Register the service at the beginning of the background script
-registerMathService();
+export default defineBackground(() => {
+ registerMathService(async () => {
+ const { MathService } = await import('../utils/math-service');
+ return new MathService();
+ });
+});
```
```ts [anywhere-else.ts]
diff --git a/docs/content/proxy-service/1.defining-services.md b/docs/content/proxy-service/1.defining-services.md
index a622b16..2ea848d 100644
--- a/docs/content/proxy-service/1.defining-services.md
+++ b/docs/content/proxy-service/1.defining-services.md
@@ -8,9 +8,9 @@ Define a class whose methods are available in other JS contexts:
```ts
import { openDB, IDBPDatabase } from 'idb';
-import { defineProxyService } from '@webext-core/proxy-service';
+import { defineServiceProxy } from '@webext-core/proxy-service';
-class TodosRepo {
+export class TodosRepo {
constructor(private db: Promise) {}
async getAll(): Promise {
@@ -18,16 +18,13 @@ class TodosRepo {
}
}
-export const [registerTodosRepo, getTodosRepo] = defineProxyService(
- 'TodosRepo',
- (idb: Promise) => new TodosRepo(idb),
-);
+export const [registerTodosRepo, getTodosRepo] = defineServiceProxy('TodosRepo');
```
```ts
// Register
const db = openDB('todos');
-registerTodosRepo(db);
+registerTodosRepo((idb: Promise) => new TodosRepo(idb), db);
```
```ts
@@ -42,24 +39,24 @@ Objects can be used as services as well. All functions defined on the object are
```ts
import { openDB, IDBPDatabase } from 'idb';
-import { defineProxyService } from '@webext-core/proxy-service';
+import { defineServiceProxy } from '@webext-core/proxy-service';
+
+export const [registerTodosRepo, getTodosRepo] = defineServiceProxy('TodosRepo');
+```
-export const [registerTodosRepo, getTodosRepo] = defineProxyService(
- 'TodosRepo',
+```ts
+// Register
+const db = openDB('todos');
+await registerTodosRepo(
(db: Promise) => ({
async getAll(): Promise {
return (await this.db).getAll('todos');
},
}),
+ db,
);
```
-```ts
-// Register
-const db = openDB('todos');
-registerTodosRepo(db);
-```
-
```ts
// Get an instance
const todosRepo = getTodosRepo();
@@ -72,21 +69,20 @@ If you only need to define a single function, you can!
```ts
import { openDB, IDBPDatabase } from 'idb';
-import { defineProxyService } from '@webext-core/proxy-service';
+import { defineServiceProxy } from '@webext-core/proxy-service';
-export const [registerGetAllTodos, getGetAllTodos] = defineProxyService(
- 'TodosRepo',
- (db: Promise) =>
- function getAllTodos() {
- return (await this.db).getAll('todos');
- },
-);
+export async function getAllTodos(db: Promise) {
+ return (await db).getAll('todos');
+}
+
+export const [registerGetAllTodos, getGetAllTodos] =
+ defineServiceProxy('TodosRepo');
```
```ts
// Register
const db = openDB('todos');
-registerGetAllTodos(db);
+await registerGetAllTodos((db: Promise) => getAllTodos, db);
```
```ts
@@ -101,9 +97,9 @@ If you need to register "deep" objects containing multiple services, you can do
```ts
import { openDB, IDBPDatabase } from 'idb';
-import { defineProxyService } from '@webext-core/proxy-service';
+import { defineServiceProxy } from '@webext-core/proxy-service';
-class TodosRepo {
+export class TodosRepo {
constructor(private db: Promise) {}
async getAll(): Promise {
@@ -111,26 +107,24 @@ class TodosRepo {
}
}
-const createAuthorsRepo = (db: Promise) => ({
+export const createAuthorsRepo = (db: Promise) => ({
async getOne(id: string): Promise {
return (await this.db).getAll('authors', id);
},
});
-function createApi(db: Promise) {
- return {
- todos: new TodosRepo(db),
- authors: createAuthorsRepo(db),
- };
-}
-
-export const [registerApi, getApi] = defineProxyService('Api', createApi);
+export const [registerApi, getApi] = defineServiceProxy('Api');
```
```ts
// Register
const db = openDB('todos');
-registerApi(db);
+await registerApi((db: Promise) => {
+ return {
+ todos: new TodosRepo(db),
+ authors: reateAuthorsRepo(db),
+ };
+}, db);
```
```ts
diff --git a/docs/content/proxy-service/api.md b/docs/content/proxy-service/api.md
index f14a6d1..0e9f883 100644
--- a/docs/content/proxy-service/api.md
+++ b/docs/content/proxy-service/api.md
@@ -24,6 +24,10 @@ A recursive type that deeply converts all methods in `TService` to be async.
## `defineProxyService`
+:::danger Deprecated
+Use {@link defineServiceProxy } instead.
+:::
+
```ts
function defineProxyService(
name: string,
@@ -54,6 +58,37 @@ of the JS context the they are called from.
- `registerService`: Used to register your service in the background
- `getService`: Used to get an instance of the service anywhere in the extension.
+## `defineServiceProxy`
+
+```ts
+function defineServiceProxy(
+ name: string,
+ config?: ProxyServiceConfig,
+): [
+ registerService: (
+ init: (...args: any[]) => TService | Promise,
+ ...args: any[]
+ ) => Promise,
+ getService: () => ProxyService,
+] {
+ // ...
+}
+```
+
+Utility for creating a service whose functions are executed in the background script regardless
+of the JS context they are called from.
+
+### Parameters
+
+- ***`name: string`***
A unique name for the service. Used to identify which service is being executed.
+
+- ***`config?: ProxyServiceConfig`***
An object that allows configuration of the underlying messaging service
+
+### Returns
+
+- `registerService`: Used to register your service in the background. Requires an `init()` callback used to create the actual service object.
+- `getService`: Used to get an instance of the service anywhere in the extension.
+
## `flattenPromise`
```ts
diff --git a/packages/proxy-service-demo/src/entrypoints/background.ts b/packages/proxy-service-demo/src/entrypoints/background.ts
index c8b65c1..016135e 100644
--- a/packages/proxy-service-demo/src/entrypoints/background.ts
+++ b/packages/proxy-service-demo/src/entrypoints/background.ts
@@ -1,3 +1,6 @@
export default defineBackground(() => {
- registerMathService();
+ registerMathService(async () => {
+ const { MathService } = await import('../utils/math-service');
+ return new MathService();
+ });
});
diff --git a/packages/proxy-service-demo/src/entrypoints/popup/main.ts b/packages/proxy-service-demo/src/entrypoints/popup/main.ts
index d1414f7..648ddaa 100644
--- a/packages/proxy-service-demo/src/entrypoints/popup/main.ts
+++ b/packages/proxy-service-demo/src/entrypoints/popup/main.ts
@@ -1,3 +1,5 @@
+import { getMathService } from '../../utils/math-service';
+
function getMathServiceElements(id: string) {
const parent = document.getElementById(id)!;
return [
diff --git a/packages/proxy-service-demo/src/utils/math-service.ts b/packages/proxy-service-demo/src/utils/math-service.ts
index 2cde22b..3aae2bd 100644
--- a/packages/proxy-service-demo/src/utils/math-service.ts
+++ b/packages/proxy-service-demo/src/utils/math-service.ts
@@ -1,6 +1,6 @@
-import { defineProxyService } from '@webext-core/proxy-service';
+import { defineServiceProxy } from '@webext-core/proxy-service';
-class MathService {
+export class MathService {
add(x: number, y: number): number {
console.log(`MathService.add(${x}, ${y})`);
return x + y;
@@ -25,7 +25,4 @@ class MathService {
}
}
-export const [registerMathService, getMathService] = defineProxyService(
- 'MathService',
- () => new MathService(),
-);
+export const [registerMathService, getMathService] = defineServiceProxy('MathService');
diff --git a/packages/proxy-service/src/defineProxyService.ts b/packages/proxy-service/src/defineProxyService.ts
index 6794fbf..ba5134e 100644
--- a/packages/proxy-service/src/defineProxyService.ts
+++ b/packages/proxy-service/src/defineProxyService.ts
@@ -14,6 +14,9 @@ import get from 'get-value';
* @returns
* - `registerService`: Used to register your service in the background
* - `getService`: Used to get an instance of the service anywhere in the extension.
+ *
+ * @deprecated
+ * Use {@link defineServiceProxy} instead.
*/
export function defineProxyService(
name: string,
diff --git a/packages/proxy-service/src/defineServiceProxy.test.ts b/packages/proxy-service/src/defineServiceProxy.test.ts
new file mode 100644
index 0000000..af037c5
--- /dev/null
+++ b/packages/proxy-service/src/defineServiceProxy.test.ts
@@ -0,0 +1,110 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { fakeBrowser } from '@webext-core/fake-browser';
+import { defineServiceProxy } from './defineServiceProxy';
+import { isBackground } from './isBackground';
+
+vi.mock('webextension-polyfill');
+
+vi.mock('./isBackground', () => ({
+ isBackground: vi.fn(),
+}));
+const isBackgroundMock = vi.mocked(isBackground);
+
+class ShakableTestService {
+ private version: number;
+
+ constructor(version: number) {
+ this.version = version;
+ }
+
+ getVersion() {
+ return this.version;
+ }
+
+ getNextVersion() {
+ return Promise.resolve(this.version + 1);
+ }
+}
+
+const defineShakableTestService = () =>
+ defineServiceProxy('ShakableTestService');
+
+describe('defineServiceProxy', () => {
+ beforeEach(() => {
+ vi.resetAllMocks();
+ fakeBrowser.reset();
+ });
+
+ it("getService should fail to get the service in the background if one hasn't been registered", () => {
+ const [_, getShakableTestService] = defineShakableTestService();
+ isBackgroundMock.mockReturnValue(true);
+
+ expect(getShakableTestService).toThrowError();
+ });
+
+ it('getService should return a proxy in other contexts', () => {
+ const [registerShakableTestService, getShakableTestService] = defineShakableTestService();
+ registerShakableTestService(() => new ShakableTestService(1));
+ isBackgroundMock.mockReturnValue(false);
+
+ // @ts-expect-error: __proxy is not apart of the type, but it's there
+ expect(getShakableTestService().__proxy).toEqual(true);
+ });
+
+ it('should defer execution of the proxy service methods to the real service methods', async () => {
+ const version = 10;
+ const [registerShakableTestService, getShakableTestService] = defineShakableTestService();
+ await registerShakableTestService((ver: number) => new ShakableTestService(ver), version);
+
+ isBackgroundMock.mockReturnValue(true);
+ const real = getShakableTestService();
+ isBackgroundMock.mockReturnValue(false);
+ const proxy = getShakableTestService();
+ const realGetVersionSpy = vi.spyOn(real, 'getVersion');
+
+ const actual = await proxy.getVersion();
+
+ expect(actual).toEqual(version);
+ expect(realGetVersionSpy).toBeCalledTimes(1);
+ });
+
+ it('should support executing functions directly', async () => {
+ const expected = 5;
+ const fn: () => Promise = vi.fn().mockResolvedValue(expected);
+ const [registerFn, getFn] = defineServiceProxy('fn');
+ registerFn(() => fn);
+
+ isBackgroundMock.mockReturnValue(false);
+ const proxyFn = getFn();
+
+ const actual = await proxyFn();
+
+ expect(actual).toBe(expected);
+ expect(fn).toBeCalledTimes(1);
+ });
+
+ it('should support executing deeply nested functions at multiple depths', async () => {
+ const expected1 = 5;
+ const expected2 = 6;
+ const fn1 = vi.fn<() => Promise>().mockResolvedValue(expected1);
+ const fn2 = vi.fn<() => Promise>().mockResolvedValue(expected2);
+ const deepObj = {
+ fn1,
+ path: {
+ to: {
+ fn2,
+ },
+ },
+ };
+ const [registerDeepObject, getDeepObject] = defineServiceProxy('DeepObject');
+ registerDeepObject(() => deepObj);
+
+ isBackgroundMock.mockReturnValue(false);
+ const deepObject = getDeepObject();
+
+ await expect(deepObject.fn1()).resolves.toBe(expected1);
+ expect(fn1).toBeCalledTimes(1);
+ await expect(deepObject.path.to.fn2()).resolves.toBe(expected2);
+ expect(fn2).toBeCalledTimes(1);
+ });
+});
diff --git a/packages/proxy-service/src/defineServiceProxy.ts b/packages/proxy-service/src/defineServiceProxy.ts
new file mode 100644
index 0000000..a1b04c1
--- /dev/null
+++ b/packages/proxy-service/src/defineServiceProxy.ts
@@ -0,0 +1,89 @@
+import { isBackground } from './isBackground';
+import { ProxyService, ProxyServiceConfig, Service } from './types';
+import { defineExtensionMessaging, ProtocolWithReturn } from '@webext-core/messaging';
+import get from 'get-value';
+
+/**
+ * Utility for creating a service whose functions are executed in the background script regardless
+ * of the JS context they are called from.
+ *
+ * @param name A unique name for the service. Used to identify which service is being executed.
+ * @param config An object that allows configuration of the underlying messaging service
+ *
+ * @returns
+ * - `registerService`: Used to register your service in the background. Requires an `init()` callback used to create the actual service object.
+ * - `getService`: Used to get an instance of the service anywhere in the extension.
+ */
+export function defineServiceProxy(
+ name: string,
+ config?: ProxyServiceConfig,
+): [
+ registerService: (
+ init: (...args: any[]) => TService | Promise,
+ ...args: any[]
+ ) => Promise,
+ getService: () => ProxyService,
+] {
+ let service: TService | undefined;
+
+ const messageKey = `proxy-service.${name}`;
+ const { onMessage, sendMessage } = defineExtensionMessaging<{
+ [key: string]: ProtocolWithReturn<{ path?: string; args: any[] }, any>;
+ }>(config);
+
+ /**
+ * Create and returns a "deep" proxy. Every property that is accessed returns another proxy, and
+ * when a function is called at any depth (0 to infinity), a message is sent to the background.
+ */
+ function createProxy(path?: string): ProxyService {
+ const wrapped = (() => {}) as ProxyService;
+ const proxy = new Proxy(wrapped, {
+ // Executed when the object is called as a function
+ async apply(_target, _thisArg, args) {
+ const res = await sendMessage(messageKey, {
+ path,
+ args: args,
+ });
+ return res;
+ },
+
+ // Executed when accessing a property on an object
+ get(target, propertyName, receiver) {
+ if (propertyName === '__proxy' || typeof propertyName === 'symbol') {
+ return Reflect.get(target, propertyName, receiver);
+ }
+ return createProxy(path == null ? propertyName : `${path}.${propertyName}`);
+ },
+ });
+ // @ts-expect-error: Adding a hidden property
+ proxy.__proxy = true;
+ return proxy;
+ }
+
+ return [
+ async function registerService(
+ init: (...args: TArgs) => TService | Promise,
+ ...args: TArgs
+ ): Promise {
+ service = await init(...args);
+ onMessage(messageKey, ({ data }) => {
+ const method = data.path == null ? service : get(service ?? {}, data.path);
+ if (method) return Promise.resolve(method.bind(service)(...data.args));
+ });
+ return service;
+ },
+
+ function getService() {
+ // Create proxy for non-background
+ if (!isBackground()) return createProxy();
+
+ // Register the service if it hasn't been registered yet
+ if (service == null) {
+ throw Error(
+ `Failed to get an instance of ${name}: in background, but registerService has not been called. Did you forget to call registerService?`,
+ );
+ }
+ return service as ProxyService;
+ },
+ ];
+}
diff --git a/packages/proxy-service/src/index.ts b/packages/proxy-service/src/index.ts
index a506956..5b83e01 100644
--- a/packages/proxy-service/src/index.ts
+++ b/packages/proxy-service/src/index.ts
@@ -1,3 +1,4 @@
export { defineProxyService } from './defineProxyService';
+export { defineServiceProxy } from './defineServiceProxy';
export { flattenPromise } from './flattenPromise';
export type { ProxyServiceConfig, ProxyService, DeepAsync } from './types';