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 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
  ```
@@ -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;
@@ -1,4 +1,4 @@
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, } 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, senderId);
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, senderId) {
243
+ handleSubscription(key) {
248
244
  const subject = this.subjects.get(key);
249
245
  if (subject) {
250
- // Subject 已存在,直接处理订阅
251
- if (!this.activeSenders.has(key)) {
252
- this.activeSenders.set(key, new Set());
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
- const { key } = message;
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
- this.subjects.set(key, subject);
301
- // 处理待处理的订阅
302
- const pendingSenders = this.pendingSubscriptions.get(key);
303
- if (pendingSenders && pendingSenders.size > 0) {
304
- if (!this.activeSenders.has(key)) {
305
- this.activeSenders.set(key, new Set());
306
- }
307
- const activeSenders = this.activeSenders.get(key);
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
- createRPCService<T>(serviceIdentifier: Identifier<T>): ServiceProxy<T>;
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
- createRPCService(serviceIdentifier) {
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
- const msg = event.detail;
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(OBSERVABLE_EVENT, { key: this._finalKey });
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
@@ -3,3 +3,4 @@ export * from './id';
3
3
  export * from './adapter';
4
4
  export * from './content';
5
5
  export * from './background';
6
+ export * from './hooks/useRPCService';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export * from './id';
3
3
  export * from './adapter';
4
4
  export * from './content';
5
5
  export * from './background';
6
+ export * from './hooks/useRPCService';
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.1.1",
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
  }