crx-rpc 2.1.1 → 2.2.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/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/background.d.ts +2 -3
- package/dist/background.js +46 -98
- package/dist/client.d.ts +2 -1
- package/dist/client.js +41 -4
- package/dist/const.d.ts +2 -0
- package/dist/const.js +2 -0
- package/dist/content.js +11 -2
- package/dist/hooks/useRPCService.d.ts +47 -0
- package/dist/hooks/useRPCService.js +97 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +13 -0
- package/package.json +8 -2
- package/src/background.ts +55 -111
- package/src/client.ts +48 -3
- package/src/const.ts +2 -0
- package/src/content.ts +12 -1
- package/src/hooks/useRPCService.ts +146 -0
- package/src/index.ts +1 -0
- package/src/types.ts +13 -0
- package/dist/adapter/background.d.ts +0 -34
- package/dist/adapter/background.js +0 -323
- package/dist/adapter/content.d.ts +0 -11
- package/dist/adapter/content.js +0 -28
- package/dist/webext-core-messaging.d.ts +0 -12
- package/dist/webext-core-messaging.js +0 -168
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ import { RuntimeRPCClient } from 'crx-rpc'
|
|
|
116
116
|
import { IMathService } from './api'
|
|
117
117
|
|
|
118
118
|
const client = new RuntimeRPCClient()
|
|
119
|
-
const mathService = client.createRPCService(IMathService)
|
|
119
|
+
const mathService = await client.createRPCService(IMathService)
|
|
120
120
|
|
|
121
121
|
await mathService.add(1, 2)
|
|
122
122
|
```
|
|
@@ -128,7 +128,7 @@ import { WebRPCClient } from 'crx-rpc'
|
|
|
128
128
|
import { IMathService } from './api'
|
|
129
129
|
|
|
130
130
|
const client = new WebRPCClient()
|
|
131
|
-
const mathService = client.createRPCService(IMathService)
|
|
131
|
+
const mathService = await client.createRPCService(IMathService)
|
|
132
132
|
|
|
133
133
|
await mathService.add(1, 2)
|
|
134
134
|
```
|
|
@@ -149,7 +149,7 @@ import { IPageService } from './api'
|
|
|
149
149
|
|
|
150
150
|
const tabId = 123 // Target Tab ID
|
|
151
151
|
const client = new TabRPCClient(tabId)
|
|
152
|
-
const pageService = client.createRPCService(IPageService)
|
|
152
|
+
const pageService = await client.createRPCService(IPageService)
|
|
153
153
|
|
|
154
154
|
await pageService.doSomething()
|
|
155
155
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -116,7 +116,7 @@ import { RuntimeRPCClient } from 'crx-rpc'
|
|
|
116
116
|
import { IMathService } from './api'
|
|
117
117
|
|
|
118
118
|
const client = new RuntimeRPCClient()
|
|
119
|
-
const mathService = client.createRPCService(IMathService)
|
|
119
|
+
const mathService = await client.createRPCService(IMathService)
|
|
120
120
|
|
|
121
121
|
await mathService.add(1, 2)
|
|
122
122
|
```
|
|
@@ -128,7 +128,7 @@ import { WebRPCClient } from 'crx-rpc'
|
|
|
128
128
|
import { IMathService } from './api'
|
|
129
129
|
|
|
130
130
|
const client = new WebRPCClient()
|
|
131
|
-
const mathService = client.createRPCService(IMathService)
|
|
131
|
+
const mathService = await client.createRPCService(IMathService)
|
|
132
132
|
|
|
133
133
|
await mathService.add(1, 2)
|
|
134
134
|
```
|
|
@@ -149,7 +149,7 @@ import { IPageService } from './api'
|
|
|
149
149
|
|
|
150
150
|
const tabId = 123 // 目标 Tab ID
|
|
151
151
|
const client = new TabRPCClient(tabId)
|
|
152
|
-
const pageService = client.createRPCService(IPageService)
|
|
152
|
+
const pageService = await client.createRPCService(IPageService)
|
|
153
153
|
|
|
154
154
|
await pageService.doSomething()
|
|
155
155
|
```
|
package/dist/background.d.ts
CHANGED
|
@@ -13,8 +13,10 @@ export declare class RemoteSubject<T> extends Disposable implements SubjectLike<
|
|
|
13
13
|
private initialValue;
|
|
14
14
|
private manager;
|
|
15
15
|
private completed;
|
|
16
|
+
private _value;
|
|
16
17
|
get finalKey(): string;
|
|
17
18
|
constructor(identifier: Identifier<T>, _key: string, initialValue: T, manager: RemoteSubjectManager);
|
|
19
|
+
get value(): T;
|
|
18
20
|
next(value: T): void;
|
|
19
21
|
complete(): void;
|
|
20
22
|
subscribe(): () => void;
|
|
@@ -22,11 +24,8 @@ export declare class RemoteSubject<T> extends Disposable implements SubjectLike<
|
|
|
22
24
|
}
|
|
23
25
|
export declare class RemoteSubjectManager extends Disposable {
|
|
24
26
|
private subjects;
|
|
25
|
-
private pendingSubscriptions;
|
|
26
|
-
private activeSenders;
|
|
27
27
|
constructor();
|
|
28
28
|
private handleSubscription;
|
|
29
|
-
private handleUnsubscription;
|
|
30
29
|
sendMessage(message: RpcObservableUpdateMessage<any>): void;
|
|
31
30
|
createSubject<T>(id: Identifier<T>, key: string, initialValue: T): RemoteSubject<T>;
|
|
32
31
|
getSubject<T>(key: string): RemoteSubject<T> | undefined;
|
package/dist/background.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OBSERVABLE_EVENT, RPC_EVENT_NAME, RPC_RESPONSE_EVENT_NAME, SUBSCRIBABLE_OBSERVABLE,
|
|
1
|
+
import { OBSERVABLE_EVENT, RPC_EVENT_NAME, RPC_PING, RPC_PONG, RPC_RESPONSE_EVENT_NAME, SUBSCRIBABLE_OBSERVABLE, } from './const';
|
|
2
2
|
import { Disposable } from './disposable';
|
|
3
3
|
export class BackgroundRPCHost extends Disposable {
|
|
4
4
|
log;
|
|
@@ -7,10 +7,23 @@ export class BackgroundRPCHost extends Disposable {
|
|
|
7
7
|
super();
|
|
8
8
|
this.log = log;
|
|
9
9
|
const handler = (msg, sender, sendResponseCallback) => {
|
|
10
|
-
if (msg.type !== RPC_EVENT_NAME)
|
|
10
|
+
if (msg.type !== RPC_EVENT_NAME && msg.type !== RPC_PING)
|
|
11
11
|
return false;
|
|
12
12
|
const tabId = sender.tab?.id;
|
|
13
13
|
const isFromRuntime = !tabId; // sidepanel/popup 没有 tab id
|
|
14
|
+
if (msg.type === RPC_PING) {
|
|
15
|
+
const pong = {
|
|
16
|
+
type: RPC_PONG,
|
|
17
|
+
from: 'background',
|
|
18
|
+
};
|
|
19
|
+
if (isFromRuntime) {
|
|
20
|
+
chrome.runtime.sendMessage(pong).catch(() => { });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
chrome.tabs.sendMessage(tabId, pong);
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
14
27
|
// 根据来源选择不同的响应方式
|
|
15
28
|
const sendResponse = (response) => {
|
|
16
29
|
const fullResponse = {
|
|
@@ -93,8 +106,14 @@ export class BackgroundRPCHost extends Disposable {
|
|
|
93
106
|
sendResponse(resp);
|
|
94
107
|
return true;
|
|
95
108
|
}
|
|
109
|
+
// 构建 RPC 上下文,自动注入到 service 方法的最后一个参数
|
|
110
|
+
const rpcContext = {
|
|
111
|
+
tabId,
|
|
112
|
+
sender,
|
|
113
|
+
isFromRuntime,
|
|
114
|
+
};
|
|
96
115
|
Promise.resolve()
|
|
97
|
-
.then(() => serviceInstance[method](...args))
|
|
116
|
+
.then(() => serviceInstance[method](...args, rpcContext))
|
|
98
117
|
.then(result => {
|
|
99
118
|
if (this.log) {
|
|
100
119
|
console.log(`%c RPC %c Success: %c ${service} %c.%c ${method} %c [%c ${id} %c]`, 'background: #6b46c1; color: white; font-weight: bold; padding: 2px 4px; border-radius: 3px;', // [RPC] 紫色背景
|
|
@@ -163,6 +182,7 @@ export class RemoteSubject extends Disposable {
|
|
|
163
182
|
initialValue;
|
|
164
183
|
manager;
|
|
165
184
|
completed = false;
|
|
185
|
+
_value;
|
|
166
186
|
get finalKey() {
|
|
167
187
|
return `${this.identifier.key}-${this._key}`;
|
|
168
188
|
}
|
|
@@ -172,10 +192,15 @@ export class RemoteSubject extends Disposable {
|
|
|
172
192
|
this._key = _key;
|
|
173
193
|
this.initialValue = initialValue;
|
|
174
194
|
this.manager = manager;
|
|
195
|
+
this._value = initialValue;
|
|
196
|
+
}
|
|
197
|
+
get value() {
|
|
198
|
+
return this._value;
|
|
175
199
|
}
|
|
176
200
|
next(value) {
|
|
177
201
|
if (this.completed)
|
|
178
202
|
return;
|
|
203
|
+
this._value = value;
|
|
179
204
|
this.manager.sendMessage({
|
|
180
205
|
operation: 'next',
|
|
181
206
|
key: this.finalKey,
|
|
@@ -202,122 +227,48 @@ export class RemoteSubject extends Disposable {
|
|
|
202
227
|
}
|
|
203
228
|
export class RemoteSubjectManager extends Disposable {
|
|
204
229
|
subjects = new Map();
|
|
205
|
-
pendingSubscriptions = new Map(); // key -> senderIds
|
|
206
|
-
activeSenders = new Map(); // key -> senderIds
|
|
207
230
|
constructor() {
|
|
208
231
|
super();
|
|
209
232
|
const handleMessage = (msg, sender) => {
|
|
210
233
|
if (msg.type === SUBSCRIBABLE_OBSERVABLE) {
|
|
211
|
-
const senderId = sender.tab?.id;
|
|
212
|
-
if (!senderId) {
|
|
213
|
-
console.warn('Received RPC request from unknown sender, ignoring.', msg);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
234
|
const { key } = msg;
|
|
217
|
-
this.handleSubscription(key
|
|
218
|
-
}
|
|
219
|
-
if (msg.type === UNSUBSCRIBE_OBSERVABLE) {
|
|
220
|
-
const senderId = sender.tab?.id;
|
|
221
|
-
if (!senderId) {
|
|
222
|
-
console.warn('Received RPC request from unknown sender, ignoring.', msg);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
const { key } = msg;
|
|
226
|
-
this.handleUnsubscription(key, senderId);
|
|
235
|
+
this.handleSubscription(key);
|
|
227
236
|
}
|
|
228
237
|
};
|
|
229
238
|
chrome.runtime.onMessage.addListener(handleMessage);
|
|
230
239
|
this.disposeWithMe(() => {
|
|
231
240
|
chrome.runtime.onMessage.removeListener(handleMessage);
|
|
232
241
|
});
|
|
233
|
-
const handleTabRemove = (tabId) => {
|
|
234
|
-
// 清理该 tab 的所有订阅
|
|
235
|
-
this.activeSenders.forEach(senders => {
|
|
236
|
-
senders.delete(tabId);
|
|
237
|
-
});
|
|
238
|
-
this.pendingSubscriptions.forEach(senders => {
|
|
239
|
-
senders.delete(tabId);
|
|
240
|
-
});
|
|
241
|
-
};
|
|
242
|
-
chrome.tabs.onRemoved.addListener(handleTabRemove);
|
|
243
|
-
this.disposeWithMe(() => {
|
|
244
|
-
chrome.tabs.onRemoved.removeListener(handleTabRemove);
|
|
245
|
-
});
|
|
246
242
|
}
|
|
247
|
-
handleSubscription(key
|
|
243
|
+
handleSubscription(key) {
|
|
248
244
|
const subject = this.subjects.get(key);
|
|
249
245
|
if (subject) {
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
this.activeSenders.get(key).add(senderId);
|
|
255
|
-
// 发送初始值
|
|
256
|
-
chrome.tabs.sendMessage(senderId, {
|
|
246
|
+
// 发送初始值 - 使用广播方式,这样订阅者的 onMessage 才能收到
|
|
247
|
+
chrome.runtime
|
|
248
|
+
.sendMessage({
|
|
257
249
|
operation: 'next',
|
|
258
250
|
key,
|
|
259
251
|
value: subject.getInitialValue(),
|
|
252
|
+
type: OBSERVABLE_EVENT,
|
|
253
|
+
})
|
|
254
|
+
.catch(() => {
|
|
255
|
+
// 忽略错误,可能没有监听者
|
|
260
256
|
});
|
|
261
257
|
}
|
|
262
|
-
else {
|
|
263
|
-
// Subject 尚未创建,缓存到待处理队列
|
|
264
|
-
if (!this.pendingSubscriptions.has(key)) {
|
|
265
|
-
this.pendingSubscriptions.set(key, new Set());
|
|
266
|
-
}
|
|
267
|
-
this.pendingSubscriptions.get(key).add(senderId);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
handleUnsubscription(key, senderId) {
|
|
271
|
-
// 从活跃订阅中移除
|
|
272
|
-
const activeSenders = this.activeSenders.get(key);
|
|
273
|
-
if (activeSenders) {
|
|
274
|
-
activeSenders.delete(senderId);
|
|
275
|
-
if (activeSenders.size === 0) {
|
|
276
|
-
this.activeSenders.delete(key);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
// 从待处理队列中移除
|
|
280
|
-
const pendingSenders = this.pendingSubscriptions.get(key);
|
|
281
|
-
if (pendingSenders) {
|
|
282
|
-
pendingSenders.delete(senderId);
|
|
283
|
-
if (pendingSenders.size === 0) {
|
|
284
|
-
this.pendingSubscriptions.delete(key);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
258
|
}
|
|
288
259
|
sendMessage(message) {
|
|
289
|
-
|
|
290
|
-
// 发送到所有订阅的 tabs
|
|
291
|
-
const senders = this.activeSenders.get(key);
|
|
292
|
-
if (senders) {
|
|
293
|
-
senders.forEach(senderId => {
|
|
294
|
-
chrome.tabs.sendMessage(senderId, message);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
260
|
+
chrome.runtime.sendMessage(message);
|
|
297
261
|
}
|
|
298
262
|
createSubject(id, key, initialValue) {
|
|
299
263
|
const subject = new RemoteSubject(id, key, initialValue, this);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
// 将待处理的订阅转移到活跃订阅
|
|
309
|
-
pendingSenders.forEach(senderId => {
|
|
310
|
-
activeSenders.add(senderId);
|
|
311
|
-
// 发送初始值
|
|
312
|
-
chrome.tabs.sendMessage(senderId, {
|
|
313
|
-
operation: 'next',
|
|
314
|
-
key,
|
|
315
|
-
value: initialValue,
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
// 清空待处理队列
|
|
319
|
-
this.pendingSubscriptions.delete(key);
|
|
320
|
-
}
|
|
264
|
+
// 使用 finalKey 作为存储 key,与客户端订阅时发送的 key 保持一致
|
|
265
|
+
this.subjects.set(subject.finalKey, subject);
|
|
266
|
+
chrome.runtime.sendMessage({
|
|
267
|
+
operation: 'next',
|
|
268
|
+
key: subject.finalKey,
|
|
269
|
+
value: initialValue,
|
|
270
|
+
type: OBSERVABLE_EVENT,
|
|
271
|
+
});
|
|
321
272
|
return subject;
|
|
322
273
|
}
|
|
323
274
|
getSubject(key) {
|
|
@@ -328,9 +279,6 @@ export class RemoteSubjectManager extends Disposable {
|
|
|
328
279
|
if (subject) {
|
|
329
280
|
subject.dispose();
|
|
330
281
|
this.subjects.delete(key);
|
|
331
|
-
// 清理相关的订阅信息
|
|
332
|
-
this.activeSenders.delete(key);
|
|
333
|
-
this.pendingSubscriptions.delete(key);
|
|
334
282
|
}
|
|
335
283
|
}
|
|
336
284
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -12,7 +12,8 @@ export declare class RPCClient extends Disposable {
|
|
|
12
12
|
private pending;
|
|
13
13
|
constructor(messageAdapter: IMessageAdapter, from: RpcFrom);
|
|
14
14
|
call<T = any>(service: string, method: string, to: RpcTo, args: any[]): Promise<T>;
|
|
15
|
-
|
|
15
|
+
private waitReady;
|
|
16
|
+
createRPCService<T>(serviceIdentifier: Identifier<T>): Promise<ServiceProxy<T>>;
|
|
16
17
|
}
|
|
17
18
|
export declare class BaseObservable<T> extends Disposable {
|
|
18
19
|
private identifier;
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OBSERVABLE_EVENT, RPC_EVENT_NAME, RPC_RESPONSE_EVENT_NAME, UNSUBSCRIBE_OBSERVABLE, } from './const';
|
|
1
|
+
import { OBSERVABLE_EVENT, RPC_EVENT_NAME, RPC_PING, RPC_PONG, RPC_RESPONSE_EVENT_NAME, SUBSCRIBABLE_OBSERVABLE, UNSUBSCRIBE_OBSERVABLE, } from './const';
|
|
2
2
|
import { Disposable } from './disposable';
|
|
3
3
|
import { randomId } from './tool';
|
|
4
4
|
export class RPCClient extends Disposable {
|
|
@@ -41,11 +41,47 @@ export class RPCClient extends Disposable {
|
|
|
41
41
|
this.messageAdapter.sendMessage(RPC_EVENT_NAME, requestParam);
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
async waitReady(timeout = 10000) {
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
const check = async () => {
|
|
47
|
+
return new Promise(resolve => {
|
|
48
|
+
let resolved = false;
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
if (!resolved) {
|
|
51
|
+
resolved = true;
|
|
52
|
+
resolve(false);
|
|
53
|
+
}
|
|
54
|
+
}, 1000);
|
|
55
|
+
const handler = (msg) => {
|
|
56
|
+
if (msg.type === RPC_PONG) {
|
|
57
|
+
if (!resolved) {
|
|
58
|
+
resolved = true;
|
|
59
|
+
resolve(true);
|
|
60
|
+
}
|
|
61
|
+
dispose();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const dispose = this.messageAdapter.onMessage(RPC_PONG, handler);
|
|
65
|
+
this.messageAdapter.sendMessage(RPC_PING, { type: RPC_PING });
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
while (Date.now() - startTime < timeout) {
|
|
69
|
+
const ready = await check();
|
|
70
|
+
if (ready)
|
|
71
|
+
return;
|
|
72
|
+
await new Promise(r => setTimeout(r, 500));
|
|
73
|
+
}
|
|
74
|
+
throw new Error('RPC service not ready (timeout)');
|
|
75
|
+
}
|
|
76
|
+
async createRPCService(serviceIdentifier) {
|
|
45
77
|
const serviceKey = serviceIdentifier.key;
|
|
78
|
+
await this.waitReady();
|
|
46
79
|
// 创建代理对象,拦截方法调用
|
|
47
80
|
return new Proxy({}, {
|
|
48
81
|
get: (target, prop) => {
|
|
82
|
+
if (prop === 'then') {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
49
85
|
if (typeof prop === 'string') {
|
|
50
86
|
// 返回一个代理函数
|
|
51
87
|
return (...args) => {
|
|
@@ -74,7 +110,8 @@ export class BaseObservable extends Disposable {
|
|
|
74
110
|
this._callback = _callback;
|
|
75
111
|
this._adapter = _adapter;
|
|
76
112
|
this.disposeWithMe(this._adapter.onMessage(OBSERVABLE_EVENT, (event) => {
|
|
77
|
-
|
|
113
|
+
// 支持两种格式:直接消息对象(runtime adapter)或 CustomEvent detail(web adapter)
|
|
114
|
+
const msg = (event.detail ?? event);
|
|
78
115
|
if (msg.key !== this._finalKey)
|
|
79
116
|
return;
|
|
80
117
|
if (msg.operation === 'next' && !this.completed && msg.value) {
|
|
@@ -85,7 +122,7 @@ export class BaseObservable extends Disposable {
|
|
|
85
122
|
this.listeners.clear();
|
|
86
123
|
}
|
|
87
124
|
}));
|
|
88
|
-
this._adapter.sendMessage(
|
|
125
|
+
this._adapter.sendMessage(SUBSCRIBABLE_OBSERVABLE, { key: this._finalKey });
|
|
89
126
|
}
|
|
90
127
|
unsubscribe() {
|
|
91
128
|
this._adapter.sendMessage(UNSUBSCRIBE_OBSERVABLE, { key: this._finalKey });
|
package/dist/const.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const RPC_EVENT_NAME = "__RPC_CALL_CLIPSHEET_AWESOME__";
|
|
2
|
+
export declare const RPC_PING = "__RPC_PING__";
|
|
3
|
+
export declare const RPC_PONG = "__RPC_PONG__";
|
|
2
4
|
export declare const RPC_RESPONSE_EVENT_NAME = "__RPC_RESPONSE_CLIPSHEET_AWESOME__";
|
|
3
5
|
export declare const SUBSCRIBABLE_OBSERVABLE = "__SUBSCRIBABLE_OBSERVABLE__";
|
|
4
6
|
export declare const UNSUBSCRIBE_OBSERVABLE = "__UNSUBSCRIBE_OBSERVABLE__";
|
package/dist/const.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const RPC_EVENT_NAME = '__RPC_CALL_CLIPSHEET_AWESOME__';
|
|
2
|
+
export const RPC_PING = '__RPC_PING__';
|
|
3
|
+
export const RPC_PONG = '__RPC_PONG__';
|
|
2
4
|
export const RPC_RESPONSE_EVENT_NAME = '__RPC_RESPONSE_CLIPSHEET_AWESOME__';
|
|
3
5
|
export const SUBSCRIBABLE_OBSERVABLE = '__SUBSCRIBABLE_OBSERVABLE__';
|
|
4
6
|
export const UNSUBSCRIBE_OBSERVABLE = '__UNSUBSCRIBE_OBSERVABLE__';
|
package/dist/content.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { OBSERVABLE_EVENT, RPC_EVENT_NAME, RPC_RESPONSE_EVENT_NAME, SUBSCRIBABLE_OBSERVABLE, UNSUBSCRIBE_OBSERVABLE, } from './const';
|
|
1
|
+
import { OBSERVABLE_EVENT, RPC_EVENT_NAME, RPC_PING, RPC_PONG, RPC_RESPONSE_EVENT_NAME, SUBSCRIBABLE_OBSERVABLE, UNSUBSCRIBE_OBSERVABLE, } from './const';
|
|
2
2
|
import { Disposable } from './disposable';
|
|
3
3
|
import { runtimeChannel } from './adapter';
|
|
4
4
|
const WEB_TO_BACKGROUND = [RPC_EVENT_NAME, SUBSCRIBABLE_OBSERVABLE, UNSUBSCRIBE_OBSERVABLE];
|
|
5
|
-
const BACKGROUND_TO_WEB = [RPC_RESPONSE_EVENT_NAME, OBSERVABLE_EVENT];
|
|
5
|
+
const BACKGROUND_TO_WEB = [RPC_RESPONSE_EVENT_NAME, OBSERVABLE_EVENT, RPC_PONG];
|
|
6
6
|
function getRuntimeId() {
|
|
7
7
|
const browserNs = globalThis?.browser;
|
|
8
8
|
if (browserNs?.runtime?.id) {
|
|
@@ -50,6 +50,15 @@ export class ContentRPCHost extends Disposable {
|
|
|
50
50
|
super();
|
|
51
51
|
this.log = log;
|
|
52
52
|
const handler = (msg, sender) => {
|
|
53
|
+
if (msg.type === RPC_PING) {
|
|
54
|
+
runtimeChannel
|
|
55
|
+
.sendMessage({
|
|
56
|
+
type: RPC_PONG,
|
|
57
|
+
from: 'content',
|
|
58
|
+
})
|
|
59
|
+
.catch(() => { });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
53
62
|
if (msg.type !== RPC_EVENT_NAME)
|
|
54
63
|
return;
|
|
55
64
|
if (this.runtimeId && sender.id && sender.id !== this.runtimeId)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Identifier } from '../id';
|
|
2
|
+
type FunctionArgs<T> = T extends (...args: infer A) => any ? A : never;
|
|
3
|
+
type FunctionReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
|
4
|
+
type ServiceProxy<T> = {
|
|
5
|
+
[K in keyof T]: T[K] extends (...args: any[]) => any ? (...args: FunctionArgs<T[K]>) => Promise<Awaited<FunctionReturnType<T[K]>>> : never;
|
|
6
|
+
};
|
|
7
|
+
interface UseRPCServiceOptions {
|
|
8
|
+
/** 是否在 tabId 变化时自动创建新的服务实例 */
|
|
9
|
+
autoRecreate?: boolean;
|
|
10
|
+
tabId?: number;
|
|
11
|
+
}
|
|
12
|
+
interface UseRPCServiceResult<T> {
|
|
13
|
+
/** RPC 服务代理实例,可以直接调用服务方法 */
|
|
14
|
+
service: ServiceProxy<T> | null;
|
|
15
|
+
/** 当前活动 tab 的 ID */
|
|
16
|
+
tabId: number | null;
|
|
17
|
+
/** 是否正在加载/初始化 */
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
/** 错误信息 */
|
|
20
|
+
error: Error | null;
|
|
21
|
+
/** 手动刷新服务实例 */
|
|
22
|
+
refresh: () => Promise<void>;
|
|
23
|
+
/** 销毁服务实例 */
|
|
24
|
+
dispose: () => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 用于创建和管理 RPC 服务实例的 React Hook
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* // 基础用法 - 使用统一的 TableService
|
|
32
|
+
* const { service, isLoading, error } = useRPCService(ITableService)
|
|
33
|
+
*
|
|
34
|
+
* const handleDetect = async () => {
|
|
35
|
+
* if (!service) return
|
|
36
|
+
* const result = await service.detectTableLikeElements()
|
|
37
|
+
* console.log(result)
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // TableService 包含所有表格相关功能
|
|
41
|
+
* const handleHighlight = useCallback(async (selector: string, itemSelector: string) => {
|
|
42
|
+
* await service?.highlightTable(selector, itemSelector)
|
|
43
|
+
* }, [service])
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function useRPCService<T>(serviceIdentifier: Identifier<T>, options?: UseRPCServiceOptions): UseRPCServiceResult<T>;
|
|
47
|
+
export default useRPCService;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { TabRPCClient } from '../adapter/tab';
|
|
3
|
+
/**
|
|
4
|
+
* 用于创建和管理 RPC 服务实例的 React Hook
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* // 基础用法 - 使用统一的 TableService
|
|
9
|
+
* const { service, isLoading, error } = useRPCService(ITableService)
|
|
10
|
+
*
|
|
11
|
+
* const handleDetect = async () => {
|
|
12
|
+
* if (!service) return
|
|
13
|
+
* const result = await service.detectTableLikeElements()
|
|
14
|
+
* console.log(result)
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* // TableService 包含所有表格相关功能
|
|
18
|
+
* const handleHighlight = useCallback(async (selector: string, itemSelector: string) => {
|
|
19
|
+
* await service?.highlightTable(selector, itemSelector)
|
|
20
|
+
* }, [service])
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useRPCService(serviceIdentifier, options = {}) {
|
|
24
|
+
const { autoRecreate = true, tabId: providedTabId } = options;
|
|
25
|
+
const [service, setService] = useState(null);
|
|
26
|
+
const [tabId, setTabId] = useState(providedTabId ?? null);
|
|
27
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
28
|
+
const [error, setError] = useState(null);
|
|
29
|
+
const clientRef = useRef(null);
|
|
30
|
+
const currentTabIdRef = useRef(null);
|
|
31
|
+
const createService = useCallback(async () => {
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
setError(null);
|
|
34
|
+
try {
|
|
35
|
+
// 清理旧的 client
|
|
36
|
+
if (clientRef.current) {
|
|
37
|
+
clientRef.current.dispose();
|
|
38
|
+
clientRef.current = null;
|
|
39
|
+
}
|
|
40
|
+
// 获取当前活动 tab
|
|
41
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
42
|
+
if (!tab.id) {
|
|
43
|
+
throw new Error('No active tab found');
|
|
44
|
+
}
|
|
45
|
+
setTabId(tab.id);
|
|
46
|
+
currentTabIdRef.current = tab.id;
|
|
47
|
+
// 创建 RPC client 和服务
|
|
48
|
+
const tabClient = new TabRPCClient(tab.id);
|
|
49
|
+
clientRef.current = tabClient;
|
|
50
|
+
const rpcService = await tabClient.createRPCService(serviceIdentifier);
|
|
51
|
+
setService(rpcService);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error('[useRPCService] Failed to create service:', err);
|
|
55
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
56
|
+
setService(null);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}, [serviceIdentifier]);
|
|
62
|
+
const dispose = useCallback(() => {
|
|
63
|
+
if (clientRef.current) {
|
|
64
|
+
clientRef.current.dispose();
|
|
65
|
+
clientRef.current = null;
|
|
66
|
+
}
|
|
67
|
+
setService(null);
|
|
68
|
+
setTabId(null);
|
|
69
|
+
}, []);
|
|
70
|
+
// 初始化创建服务
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
createService();
|
|
73
|
+
if (providedTabId) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// 监听 tab 变化
|
|
77
|
+
const handleTabActivated = (activeInfo) => {
|
|
78
|
+
if (autoRecreate && activeInfo.tabId !== currentTabIdRef.current) {
|
|
79
|
+
createService();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
chrome.tabs.onActivated.addListener(handleTabActivated);
|
|
83
|
+
return () => {
|
|
84
|
+
chrome.tabs.onActivated.removeListener(handleTabActivated);
|
|
85
|
+
dispose();
|
|
86
|
+
};
|
|
87
|
+
}, [createService, autoRecreate, dispose, providedTabId]);
|
|
88
|
+
return {
|
|
89
|
+
service,
|
|
90
|
+
tabId,
|
|
91
|
+
isLoading,
|
|
92
|
+
error,
|
|
93
|
+
refresh: createService,
|
|
94
|
+
dispose,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export default useRPCService;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="chrome" />
|
|
1
2
|
export type RpcTo = 'content' | 'background';
|
|
2
3
|
export type RpcFrom = 'runtime' | 'web' | 'wxt-page';
|
|
3
4
|
export interface RpcRequest {
|
|
@@ -46,3 +47,15 @@ export interface IMessageAdapter {
|
|
|
46
47
|
export interface IDisposable {
|
|
47
48
|
dispose(): void;
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* RPC 调用上下文,包含调用者信息
|
|
52
|
+
* Service 方法可以通过最后一个参数获取此上下文
|
|
53
|
+
*/
|
|
54
|
+
export interface RpcContext {
|
|
55
|
+
/** 调用来源的 tab ID,如果来自 sidepanel/popup 则为 undefined */
|
|
56
|
+
tabId?: number;
|
|
57
|
+
/** 完整的 sender 信息 */
|
|
58
|
+
sender: chrome.runtime.MessageSender;
|
|
59
|
+
/** 是否来自 runtime(sidepanel/popup),而非 content script */
|
|
60
|
+
isFromRuntime: boolean;
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crx-rpc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A lightweight RPC framework for Chrome Extension (background <-> content <-> web)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,11 +21,17 @@
|
|
|
21
21
|
"build": "tsc",
|
|
22
22
|
"watch": "tsc -w",
|
|
23
23
|
"clean": "rm -rf dist",
|
|
24
|
-
"postinstall": "tsc"
|
|
24
|
+
"postinstall": "tsc",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@types/chrome": "^0.0.270",
|
|
28
29
|
"@types/node": "^24.3.0",
|
|
30
|
+
"@types/react": "^18.2.0",
|
|
31
|
+
"react": "^18.2.0",
|
|
29
32
|
"typescript": "^5.2.2"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=16.8.0"
|
|
30
36
|
}
|
|
31
37
|
}
|