@northsea4/proxy-service 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +313 -0
- package/lib/index.d.ts +115 -0
- package/lib/index.js +98 -0
- package/lib/types.d.ts +39 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Aaron
|
|
4
|
+
Copyright (c) 2026 Northsea4
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# @northsea4/proxy-service
|
|
2
|
+
|
|
3
|
+
基于 [@webext-core/proxy-service](https://github.com/aklinker1/webext-core/tree/main/packages/proxy-service) 的二次开发版本,添加了 `sender` 参数支持。
|
|
4
|
+
|
|
5
|
+
类型安全的浏览器扩展消息传递 API 封装,允许你在任何地方调用函数,但在后台脚本中执行。支持所有浏览器(Chrome、Firefox、Safari 等)。
|
|
6
|
+
|
|
7
|
+
## 主要修改
|
|
8
|
+
|
|
9
|
+
相比原版 `@webext-core/proxy-service`,本包的主要修改:
|
|
10
|
+
|
|
11
|
+
**`registerService` 函数现在会将 `sender` 参数传递给服务方法:**
|
|
12
|
+
|
|
13
|
+
当代理服务的方法被调用时,消息的 `sender` 对象(包含 tab、url 等信息)会作为最后一个参数传递给实际的服务方法。这使得服务可以获取调用者的上下文信息。
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// 原版行为
|
|
17
|
+
registerService(key, service);
|
|
18
|
+
// 调用 service.method(arg1, arg2)
|
|
19
|
+
|
|
20
|
+
// 修改后的行为
|
|
21
|
+
registerService(key, service);
|
|
22
|
+
// 调用 service.method(arg1, arg2, sender)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add @northsea4/proxy-service
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 快速开始
|
|
32
|
+
|
|
33
|
+
### 1. 定义服务
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// services/tab-service.ts
|
|
37
|
+
import type { Runtime } from 'webextension-polyfill';
|
|
38
|
+
|
|
39
|
+
export class TabService {
|
|
40
|
+
// sender 参数会自动注入为最后一个参数
|
|
41
|
+
async getCurrentTabInfo(sender: Runtime.MessageSender) {
|
|
42
|
+
return {
|
|
43
|
+
tabId: sender.tab?.id,
|
|
44
|
+
url: sender.tab?.url,
|
|
45
|
+
title: sender.tab?.title,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async closeTab(tabId: number, sender: Runtime.MessageSender) {
|
|
50
|
+
console.log('closeTab called from:', sender.tab?.url);
|
|
51
|
+
await browser.tabs.remove(tabId);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. 创建类型安全的服务键
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// services/keys.ts
|
|
60
|
+
import type { ProxyServiceKey } from '@northsea4/proxy-service';
|
|
61
|
+
import type { TabService } from './tab-service';
|
|
62
|
+
|
|
63
|
+
export const TAB_SERVICE_KEY = 'tab-service' as ProxyServiceKey<TabService>;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. 在后台脚本中注册服务
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// background.ts
|
|
70
|
+
import { registerService } from '@northsea4/proxy-service';
|
|
71
|
+
import { TabService } from './services/tab-service';
|
|
72
|
+
import { TAB_SERVICE_KEY } from './services/keys';
|
|
73
|
+
|
|
74
|
+
// 注册服务
|
|
75
|
+
const tabService = new TabService();
|
|
76
|
+
registerService(TAB_SERVICE_KEY, tabService);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 4. 在其他上下文中使用服务
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// content-script.ts 或 popup.ts
|
|
83
|
+
import { createProxyService } from '@northsea4/proxy-service';
|
|
84
|
+
import { TAB_SERVICE_KEY } from './services/keys';
|
|
85
|
+
|
|
86
|
+
const tabService = createProxyService(TAB_SERVICE_KEY);
|
|
87
|
+
|
|
88
|
+
// 调用服务方法(不需要手动传递 sender,它会自动注入)
|
|
89
|
+
const tabInfo = await tabService.getCurrentTabInfo();
|
|
90
|
+
console.log('Current tab:', tabInfo);
|
|
91
|
+
|
|
92
|
+
// 关闭当前标签页
|
|
93
|
+
await tabService.closeTab(tabInfo.tabId);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 服务定义方式
|
|
97
|
+
|
|
98
|
+
支持多种服务定义方式:
|
|
99
|
+
|
|
100
|
+
### 类
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
export class MathService {
|
|
104
|
+
add(x: number, y: number, sender: Runtime.MessageSender): number {
|
|
105
|
+
console.log('add called from:', sender.tab?.url);
|
|
106
|
+
return x + y;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 对象
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
export function createMathService() {
|
|
115
|
+
return {
|
|
116
|
+
add(x: number, y: number, sender: Runtime.MessageSender): number {
|
|
117
|
+
console.log('add called from:', sender.tab?.url);
|
|
118
|
+
return x + y;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 函数
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
export type SayHello = (name: string, sender: Runtime.MessageSender) => Promise<string>;
|
|
128
|
+
|
|
129
|
+
export const sayHello: SayHello = async (name, sender) => {
|
|
130
|
+
console.log('sayHello called from:', sender.tab?.url);
|
|
131
|
+
return `Hello, ${name}!`;
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 嵌套对象
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
export function createApi() {
|
|
139
|
+
return {
|
|
140
|
+
math: {
|
|
141
|
+
add(x: number, y: number, sender: Runtime.MessageSender): number {
|
|
142
|
+
return x + y;
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
string: {
|
|
146
|
+
uppercase(str: string, sender: Runtime.MessageSender): string {
|
|
147
|
+
return str.toUpperCase();
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 使用
|
|
154
|
+
registerService('api', createApi());
|
|
155
|
+
const api = createProxyService('api');
|
|
156
|
+
await api.math.add(1, 2);
|
|
157
|
+
await api.string.uppercase('hello');
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Sender 参数详解
|
|
161
|
+
|
|
162
|
+
`sender` 参数是一个 `Runtime.MessageSender` 对象,包含以下信息:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
interface MessageSender {
|
|
166
|
+
tab?: Tab; // 发送消息的标签页信息
|
|
167
|
+
frameId?: number; // 发送消息的框架 ID
|
|
168
|
+
id?: string; // 发送消息的扩展 ID
|
|
169
|
+
url?: string; // 发送消息的页面 URL
|
|
170
|
+
tlsChannelId?: string;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 使用场景示例
|
|
175
|
+
|
|
176
|
+
#### 1. 获取调用者的标签页信息
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
class ContextService {
|
|
180
|
+
async getCallerInfo(sender: Runtime.MessageSender) {
|
|
181
|
+
return {
|
|
182
|
+
fromTab: sender.tab?.id,
|
|
183
|
+
fromUrl: sender.tab?.url,
|
|
184
|
+
fromFrame: sender.frameId,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### 2. 根据调用者执行不同逻辑
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
class PermissionService {
|
|
194
|
+
async checkPermission(action: string, sender: Runtime.MessageSender) {
|
|
195
|
+
const url = sender.tab?.url;
|
|
196
|
+
if (url?.startsWith('https://trusted-domain.com')) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### 3. 操作调用者的标签页
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
class TabControlService {
|
|
208
|
+
async closeCallerTab(sender: Runtime.MessageSender) {
|
|
209
|
+
if (sender.tab?.id) {
|
|
210
|
+
await browser.tabs.remove(sender.tab.id);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async reloadCallerTab(sender: Runtime.MessageSender) {
|
|
215
|
+
if (sender.tab?.id) {
|
|
216
|
+
await browser.tabs.reload(sender.tab.id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## TypeScript 类型支持
|
|
223
|
+
|
|
224
|
+
在定义服务类型时,记得在每个方法的最后添加 `sender` 参数:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import type { Runtime } from 'webextension-polyfill';
|
|
228
|
+
|
|
229
|
+
// 正确的类型定义
|
|
230
|
+
interface MyService {
|
|
231
|
+
method1(arg1: string, sender: Runtime.MessageSender): Promise<void>;
|
|
232
|
+
method2(arg1: number, arg2: string, sender: Runtime.MessageSender): Promise<number>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 错误的类型定义(缺少 sender 参数)
|
|
236
|
+
interface MyServiceWrong {
|
|
237
|
+
method1(arg1: string): Promise<void>; // ❌ 运行时会出错
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## 高级功能
|
|
242
|
+
|
|
243
|
+
### flattenPromise
|
|
244
|
+
|
|
245
|
+
简化异步依赖的处理:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { flattenPromise } from '@northsea4/proxy-service';
|
|
249
|
+
|
|
250
|
+
function createService(dbPromise: Promise<IDBDatabase>) {
|
|
251
|
+
const db = flattenPromise(dbPromise);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
async getData(sender: Runtime.MessageSender) {
|
|
255
|
+
// 不需要 await dbPromise,可以直接使用
|
|
256
|
+
return await db.get('store', 'key');
|
|
257
|
+
// 而不是: await (await dbPromise).get('store', 'key');
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### defineProxyService
|
|
264
|
+
|
|
265
|
+
简化服务定义和使用的工具函数:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// service.ts
|
|
269
|
+
export const [registerMathService, getMathService] = defineProxyService(
|
|
270
|
+
'math-service',
|
|
271
|
+
() => new MathService(),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// background.ts
|
|
275
|
+
registerMathService();
|
|
276
|
+
|
|
277
|
+
// content-script.ts
|
|
278
|
+
const mathService = getMathService();
|
|
279
|
+
await mathService.add(1, 2);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## 配置选项
|
|
283
|
+
|
|
284
|
+
支持 `@webext-core/messaging` 的所有配置选项:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { registerService, createProxyService } from '@northsea4/proxy-service';
|
|
288
|
+
|
|
289
|
+
const config = {
|
|
290
|
+
logger: console, // 自定义日志记录器
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
registerService('my-service', myService, config);
|
|
294
|
+
const proxy = createProxyService('my-service', config);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## 开发
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# 构建
|
|
301
|
+
pnpm build
|
|
302
|
+
|
|
303
|
+
# 开发模式(监听文件变化)
|
|
304
|
+
pnpm dev
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
MIT
|
|
310
|
+
|
|
311
|
+
## 致谢
|
|
312
|
+
|
|
313
|
+
本项目基于 [@webext-core/proxy-service](https://github.com/aklinker1/webext-core/tree/main/packages/proxy-service) 开发,感谢原作者的优秀工作。
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ExtensionMessagingConfig, RemoveListenerCallback } from '@webext-core/messaging';
|
|
2
|
+
import { DeepAsync, Service } from './types.js';
|
|
3
|
+
export { ServiceCallContext } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A type that ensures a service has only async methods.
|
|
7
|
+
*
|
|
8
|
+
* - ***If all methods are async***, it returns the original type.
|
|
9
|
+
* - ***If the service has non-async methods***, it returns a `DeepAsync` of the service.
|
|
10
|
+
*/
|
|
11
|
+
type ProxyService<T> = T extends DeepAsync<T> ? T : DeepAsync<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Create a proxy service that uses the message APIs to proxy function calls to
|
|
14
|
+
* the real service registered in the background with `registerService`.
|
|
15
|
+
*
|
|
16
|
+
* @param key The service key to listen for, must be the same string as the one
|
|
17
|
+
* used in `registerService`.
|
|
18
|
+
*/
|
|
19
|
+
declare function createProxyService<T extends Service>(key: ProxyServiceKey<T> | string, config?: ExtensionMessagingConfig): ProxyService<T>;
|
|
20
|
+
/**
|
|
21
|
+
* Sets up message listeners that receive messages from proxies created with
|
|
22
|
+
* `createProxyService`.
|
|
23
|
+
*
|
|
24
|
+
* @param key The service key to listen for, must be the same string as the one
|
|
25
|
+
* used in `createProxyService`.
|
|
26
|
+
* @param realService The real service instance that will handle the requests.
|
|
27
|
+
*
|
|
28
|
+
* **Modified from @webext-core/proxy-service:**
|
|
29
|
+
* The `context` parameter (ServiceCallContext) is now passed as the last argument to all service methods,
|
|
30
|
+
* allowing services to access message sender information, timestamp, requestId, and other metadata.
|
|
31
|
+
*/
|
|
32
|
+
declare function registerService<T extends Service, K extends string = ProxyServiceKey<T> | string>(key: K, realService: T, config?: ExtensionMessagingConfig): RemoveListenerCallback;
|
|
33
|
+
declare function isProxyService<T>(obj: unknown): obj is ProxyService<T>;
|
|
34
|
+
interface ProxyServiceConstraint<_> {
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Used to constrain a service's type between calls to `createProxyService` and
|
|
38
|
+
* `registerService`.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* // utils/proxy-service-keys.ts
|
|
43
|
+
* import type { ProxyServiceKey } from '@northsea4/proxy-service';
|
|
44
|
+
* import type { MathService } from './math-service';
|
|
45
|
+
*
|
|
46
|
+
* export const PROXY_SERVICE_KEY = 'math-service' as ProxyServiceKey<MathService>;
|
|
47
|
+
*
|
|
48
|
+
* // background.ts
|
|
49
|
+
* import { PROXY_SERVICE_KEY } from './utils/proxy-service-keys';
|
|
50
|
+
*
|
|
51
|
+
* registerService(PROXY_SERVICE_KEY, new MathService())
|
|
52
|
+
*
|
|
53
|
+
* // content-script.ts
|
|
54
|
+
* import { PROXY_SERVICE_KEY } from './utils/proxy-service-keys';
|
|
55
|
+
*
|
|
56
|
+
* const mathService = await createProxyService(PROXY_SERVICE_KEY);
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
type ProxyServiceKey<T> = string & ProxyServiceConstraint<T>;
|
|
60
|
+
/**
|
|
61
|
+
* Given a promise of a variable, return a proxy to that awaits the promise internally so you don't
|
|
62
|
+
* have to call `await` twice.
|
|
63
|
+
*
|
|
64
|
+
* > This can be used to simplify handling `Promise<Dependency>` passed in your services.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* function createService(dependencyPromise: Promise<SomeDependency>) {
|
|
69
|
+
* const dependency = flattenPromise(dependencyPromise);
|
|
70
|
+
*
|
|
71
|
+
* return {
|
|
72
|
+
* async doSomething() {
|
|
73
|
+
* await dependency.someAsyncWork();
|
|
74
|
+
* // Instead of `await (await dependencyPromise).someAsyncWork();`
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare function flattenPromise<T>(promise: Promise<T>): DeepAsync<T>;
|
|
81
|
+
/**
|
|
82
|
+
* Utility for creating a service whose functions are executed in the background script regardless
|
|
83
|
+
* of the JS context the they are called from.
|
|
84
|
+
*
|
|
85
|
+
* @param name A unique name for the service. Used to identify which service is being executed.
|
|
86
|
+
* @param init A function that returns your real service implementation. If args are listed,
|
|
87
|
+
* `registerService` will require the same set of arguments.
|
|
88
|
+
* @param config
|
|
89
|
+
* @returns `registerService`: Used to register your service in the background
|
|
90
|
+
* @returns `getService`: Used to get an instance of the service anywhere in the extension.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* // service.ts
|
|
95
|
+
* export const [registerJobScheduler, getJobScheduler] = defineProxyService(
|
|
96
|
+
* 'JobScheduler',
|
|
97
|
+
* () => defineJobScheduler(),
|
|
98
|
+
* );
|
|
99
|
+
*
|
|
100
|
+
* // background.ts
|
|
101
|
+
* const jobs = registerJobScheduler();
|
|
102
|
+
*
|
|
103
|
+
* // content-script.ts
|
|
104
|
+
* const jobs = getJobScheduler();
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
declare function defineProxyService<TService extends Service, TArgs extends any[]>(name: string, init: (...args: TArgs) => TService, config?: ProxyServiceConfig): [registerService: (...args: TArgs) => TService, getService: () => ProxyService<TService>];
|
|
108
|
+
/**
|
|
109
|
+
* Configure a proxy service's behavior. It uses `@webext-core/messaging` internally, so any
|
|
110
|
+
* config from `ExtensionMessagingConfig` can be passed as well.
|
|
111
|
+
*/
|
|
112
|
+
interface ProxyServiceConfig extends ExtensionMessagingConfig {
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { DeepAsync, type ProxyService, type ProxyServiceConfig, type ProxyServiceKey, Service, createProxyService, defineProxyService, flattenPromise, isProxyService, registerService };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
defineExtensionMessaging
|
|
4
|
+
} from "@webext-core/messaging";
|
|
5
|
+
function createProxyService(key, config) {
|
|
6
|
+
return createProxy(defineProxyMessaging(key, config));
|
|
7
|
+
}
|
|
8
|
+
function registerService(key, realService, config) {
|
|
9
|
+
const messenger = defineProxyMessaging(key, config);
|
|
10
|
+
return messenger.onMessage(messenger.messageKey, ({ data, sender }) => {
|
|
11
|
+
const method = data.path == null ? realService : get(realService ?? {}, data.path);
|
|
12
|
+
if (method) {
|
|
13
|
+
const context = {
|
|
14
|
+
sender,
|
|
15
|
+
timestamp: data.timestamp,
|
|
16
|
+
requestId: data.requestId,
|
|
17
|
+
metadata: {}
|
|
18
|
+
};
|
|
19
|
+
return Promise.resolve(method.bind(realService)(...data.args, context));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function isProxyService(obj) {
|
|
24
|
+
return obj?.[ProxyServiceSymbol] === true;
|
|
25
|
+
}
|
|
26
|
+
function defineProxyMessaging(key, config) {
|
|
27
|
+
const messaging = defineExtensionMessaging(config);
|
|
28
|
+
return {
|
|
29
|
+
messageKey: `proxy-service.${key}`,
|
|
30
|
+
...messaging
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createProxy(messenger, path) {
|
|
34
|
+
const wrapped = (() => {
|
|
35
|
+
});
|
|
36
|
+
const proxy = new Proxy(wrapped, {
|
|
37
|
+
async apply(_target, _thisArg, args) {
|
|
38
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
39
|
+
const timestamp = Date.now();
|
|
40
|
+
const res = await messenger.sendMessage(messenger.messageKey, {
|
|
41
|
+
path,
|
|
42
|
+
args,
|
|
43
|
+
timestamp,
|
|
44
|
+
requestId
|
|
45
|
+
});
|
|
46
|
+
return res;
|
|
47
|
+
},
|
|
48
|
+
get(target, propertyName, receiver) {
|
|
49
|
+
if (typeof propertyName === "symbol") {
|
|
50
|
+
return Reflect.get(target, propertyName, receiver);
|
|
51
|
+
}
|
|
52
|
+
return createProxy(messenger, path == null ? [propertyName] : path.concat([propertyName]));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
proxy[ProxyServiceSymbol] = true;
|
|
56
|
+
return proxy;
|
|
57
|
+
}
|
|
58
|
+
var ProxyServiceSymbol = /* @__PURE__ */ Symbol();
|
|
59
|
+
function get(obj, path) {
|
|
60
|
+
if (path.length === 0) return obj;
|
|
61
|
+
return path.reduce((acc, key) => acc?.[key], obj);
|
|
62
|
+
}
|
|
63
|
+
function flattenPromise(promise) {
|
|
64
|
+
return new Proxy({}, {
|
|
65
|
+
get(_target, p) {
|
|
66
|
+
if (typeof p === "symbol") return;
|
|
67
|
+
return async (...args) => {
|
|
68
|
+
const value = await promise;
|
|
69
|
+
return await value[p](...args);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function defineProxyService(name, init, config) {
|
|
75
|
+
const key = name;
|
|
76
|
+
let registeredInstance = null;
|
|
77
|
+
return [
|
|
78
|
+
(...args) => {
|
|
79
|
+
const service = init(...args);
|
|
80
|
+
registeredInstance = service;
|
|
81
|
+
registerService(key, service, config);
|
|
82
|
+
return service;
|
|
83
|
+
},
|
|
84
|
+
() => {
|
|
85
|
+
if (registeredInstance) {
|
|
86
|
+
return registeredInstance;
|
|
87
|
+
}
|
|
88
|
+
return createProxyService(key, config);
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
createProxyService,
|
|
94
|
+
defineProxyService,
|
|
95
|
+
flattenPromise,
|
|
96
|
+
isProxyService,
|
|
97
|
+
registerService
|
|
98
|
+
};
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type Proimsify<T> = T extends Promise<any> ? T : Promise<T>;
|
|
2
|
+
type Service = ((...args: any[]) => Promise<any>) | {
|
|
3
|
+
[key: string]: any | Service;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Service 方法调用时的上下文信息
|
|
7
|
+
* 作为服务方法的最后一个可选参数传递
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* class MyService {
|
|
12
|
+
* async doSomething(arg1: string, context?: ServiceCallContext) {
|
|
13
|
+
* console.log('Called by:', context?.sender?.tab?.title)
|
|
14
|
+
* console.log('Request ID:', context?.requestId)
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
interface ServiceCallContext {
|
|
20
|
+
/** 消息发送者信息 */
|
|
21
|
+
sender: any;
|
|
22
|
+
/** 调用时间戳 */
|
|
23
|
+
timestamp?: number;
|
|
24
|
+
/** 请求 ID,用于追踪和调试 */
|
|
25
|
+
requestId?: string;
|
|
26
|
+
/** 其他元数据 */
|
|
27
|
+
metadata?: Record<string, any>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A recursive type that deeply converts all methods in `TService` to be async.
|
|
31
|
+
*/
|
|
32
|
+
type DeepAsync<TService> = TService extends (...args: any) => any ? ToAsyncFunction<TService> : TService extends {
|
|
33
|
+
[key: string]: any;
|
|
34
|
+
} ? {
|
|
35
|
+
[fn in keyof TService]: DeepAsync<TService[fn]>;
|
|
36
|
+
} : never;
|
|
37
|
+
type ToAsyncFunction<T extends (...args: any) => any> = (...args: Parameters<T>) => Proimsify<ReturnType<T>>;
|
|
38
|
+
|
|
39
|
+
export type { DeepAsync, Proimsify, Service, ServiceCallContext };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@northsea4/proxy-service",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A type-safe wrapper around the web extension messaging APIs that lets you call a function from anywhere, but execute it in the background. Forked from @webext-core/proxy-service with sender parameter support.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"module": "./lib/index.js",
|
|
8
|
+
"types": "./lib/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/index.d.ts",
|
|
12
|
+
"import": "./lib/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./types": {
|
|
15
|
+
"types": "./lib/types.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"dev": "tsup --watch"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"webextension",
|
|
27
|
+
"browser-extension",
|
|
28
|
+
"chrome-extension",
|
|
29
|
+
"proxy",
|
|
30
|
+
"service",
|
|
31
|
+
"messaging"
|
|
32
|
+
],
|
|
33
|
+
"author": "northsea4",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@webext-core/messaging": "^1.4.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"tsup": "^8.0.0",
|
|
40
|
+
"typescript": "^5.0.0",
|
|
41
|
+
"webextension-polyfill": "^0.12.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"webextension-polyfill": "^0.10.0 || ^0.11.0 || ^0.12.0"
|
|
45
|
+
}
|
|
46
|
+
}
|