crx-rpc 1.0.2 → 1.0.4

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
@@ -211,13 +211,15 @@ interface RpcErrorDetails {
211
211
 
212
212
  ## Observable Support
213
213
 
214
- The framework includes built-in support for reactive data streams using `RemoteSubject` and `Observable` patterns.
214
+ The framework includes built-in support for reactive data streams using `RemoteSubject` and `Observable` patterns with a centralized message management system.
215
215
 
216
- ### Remote Subject (Background Script)
216
+ ### Remote Subject Manager & Remote Subject (Background Script)
217
+
218
+ The `RemoteSubjectManager` acts as a centralized message hub that handles all subscription management and message routing, while `RemoteSubject` focuses purely on state management.
217
219
 
218
220
  ```typescript
219
221
  // background.ts
220
- import { BackgroundRPC, RemoteSubject, createIdentifier } from 'crx-rpc';
222
+ import { BackgroundRPC, RemoteSubjectManager, createIdentifier } from 'crx-rpc';
221
223
 
222
224
  interface ICounterObservable {
223
225
  value: number;
@@ -227,8 +229,15 @@ const ICounterObservable = createIdentifier<ICounterObservable>('Counter');
227
229
 
228
230
  const rpc = new BackgroundRPC();
229
231
 
230
- // Create a remote subject that can broadcast to multiple subscribers
231
- const counterSubject = new RemoteSubject(ICounterObservable, 'main', { value: 0 });
232
+ // Create a centralized subject manager
233
+ const subjectManager = new RemoteSubjectManager();
234
+
235
+ // Create a remote subject through the manager
236
+ const counterSubject = subjectManager.createSubject(
237
+ ICounterObservable,
238
+ 'main',
239
+ { value: 0 }
240
+ );
232
241
 
233
242
  // Update value and broadcast to all subscribers
234
243
  setInterval(() => {
@@ -236,8 +245,43 @@ setInterval(() => {
236
245
  counterSubject.next(newValue);
237
246
  }, 1000);
238
247
 
248
+ // The manager handles:
249
+ // - Message routing and subscription management
250
+ // - Queuing subscriptions that arrive before subjects are created
251
+ // - Automatic cleanup when tabs are closed
252
+ // - Broadcasting to multiple subscribers
253
+
239
254
  // Cleanup
240
- // counterSubject.dispose();
255
+ // subjectManager.dispose(); // This will dispose all subjects
256
+ ```
257
+
258
+ ### Key Features of RemoteSubjectManager
259
+
260
+ - **Centralized Message Hub**: All observable-related messages are handled by the manager
261
+ - **Queue Management**: Subscriptions received before subject creation are queued and processed later
262
+ - **Resource Management**: Automatic cleanup of subscriptions when tabs are closed
263
+ - **Type Safety**: Full TypeScript support with proper typing throughout
264
+
265
+ ### Architecture
266
+
267
+ ```
268
+ ┌─────────────────┐ ┌──────────────────────────────────────┐ ┌─────────────────┐
269
+ │ Web Page │ │ Background Script │ │ Content Script │
270
+ ├─────────────────┤ ├──────────────────────────────────────┤ ├─────────────────┤
271
+ │ WebObservable │ │ RemoteSubjectManager │ │ContentObservable│
272
+ │ │ │ ┌─────────────────────────────────┐ │ │ │
273
+ │ subscribe() ────┼───▶│ │ Message Routing & Queue Mgmt │ │◄───┤ subscribe() │
274
+ │ │◄───│ │ │ │ │ │
275
+ └─────────────────┘ │ └─────────────────────────────────┘ │ └─────────────────┘
276
+ │ │ │
277
+ │ ┌─────────────▼─────────────────┐ │
278
+ │ │ RemoteSubject │ │
279
+ │ │ (Pure State Management) │ │
280
+ │ │ │ │
281
+ │ │ next() ─────────────────────▶ │ │
282
+ │ │ complete() ─────────────────▶ │ │
283
+ │ └───────────────────────────────┘ │
284
+ └──────────────────────────────────────┘
241
285
  ```
242
286
 
243
287
  ### Subscribing from Web Page
@@ -294,21 +338,27 @@ const observable = new ContentObservable(
294
338
 
295
339
  ### Observable Communication Patterns
296
340
 
297
- The Observable system supports multiple communication patterns:
341
+ The Observable system supports multiple communication patterns with centralized management:
298
342
 
299
343
  ```typescript
300
344
  // Pattern 1: Background → Web Page (via Content Script bridge)
301
- // Background: RemoteSubject.next()
345
+ // Background: RemoteSubjectManager creates and manages RemoteSubject
346
+ // Background: RemoteSubject.next() → Manager routes to subscribers
302
347
  // Web Page: WebObservable.subscribe()
303
348
 
304
349
  // Pattern 2: Background → Content Script (direct)
305
- // Background: RemoteSubject.next()
350
+ // Background: RemoteSubject.next() → Manager routes directly
306
351
  // Content Script: ContentObservable.subscribe()
307
352
 
308
353
  // Pattern 3: Background → Both Web Page and Content Script
309
- // Background: RemoteSubject.next() (broadcasts to all subscribers)
354
+ // Background: RemoteSubject.next() → Manager broadcasts to all subscribers
310
355
  // Web Page: WebObservable.subscribe()
311
356
  // Content Script: ContentObservable.subscribe()
357
+
358
+ // Pattern 4: Subscription before Subject Creation (Queue Management)
359
+ // Subscriber: WebObservable.subscribe() → Manager queues subscription
360
+ // Background: Later creates RemoteSubject → Manager processes queued subscriptions
361
+ // Result: No missed initial values, proper subscription ordering
312
362
  ```
313
363
 
314
364
  ## Advanced Usage
@@ -364,10 +414,12 @@ if (!client.isDisposed()) {
364
414
  - **`ContentRPC`**: Message bridge between web pages and background scripts
365
415
  - **`WebRPCClient`**: RPC client for web pages
366
416
  - **`ContentRPCClient`**: Direct RPC client for content scripts
417
+ - **`RemoteSubjectManager`**: Centralized observable message management system
367
418
 
368
419
  ### Observable Classes
369
420
 
370
- - **`RemoteSubject<T>`**: Observable subject that can broadcast to multiple subscribers
421
+ - **`RemoteSubjectManager`**: Centralized message hub that manages subscriptions and message routing for all observables
422
+ - **`RemoteSubject<T>`**: Pure state management observable that works with the manager to broadcast updates
371
423
  - **`WebObservable<T>`**: Observable subscriber for web pages
372
424
  - **`ContentObservable<T>`**: Observable subscriber for content scripts
373
425
 
@@ -404,8 +456,9 @@ if (!client.isDisposed()) {
404
456
  4. **Performance Optimization**
405
457
  - Avoid frequent small data transfers
406
458
  - Consider batching operations when possible
407
- - Use Observable pattern for real-time data updates
459
+ - Use Observable pattern for real-time data updates with `RemoteSubjectManager` for efficient message routing
408
460
  - Implement caching strategies where appropriate
461
+ - The manager automatically handles subscription queuing to prevent race conditions
409
462
 
410
463
  5. **Security Considerations**
411
464
  - Validate input parameters in service implementations
package/README.zh-CN.md CHANGED
@@ -227,13 +227,15 @@ interface RpcErrorDetails {
227
227
 
228
228
  ## Observable支持
229
229
 
230
- 框架包含使用 `RemoteSubject` 和 `Observable` 模式的内置响应式数据流支持。
230
+ 框架包含使用 `RemoteSubject` 和 `Observable` 模式的内置响应式数据流支持,采用集中式消息管理系统。
231
231
 
232
- ### 远程Subject(背景脚本)
232
+ ### RemoteSubjectManager 和 RemoteSubject(背景脚本)
233
+
234
+ `RemoteSubjectManager` 作为集中式消息中心处理所有订阅管理和消息路由,而 `RemoteSubject` 专注于纯状态管理。
233
235
 
234
236
  ```typescript
235
237
  // background.ts
236
- import { BackgroundRPC, RemoteSubject, createIdentifier } from 'crx-rpc';
238
+ import { BackgroundRPC, RemoteSubjectManager, createIdentifier } from 'crx-rpc';
237
239
 
238
240
  interface ICounterObservable {
239
241
  value: number;
@@ -243,8 +245,15 @@ const ICounterObservable = createIdentifier<ICounterObservable>('Counter');
243
245
 
244
246
  const rpc = new BackgroundRPC();
245
247
 
246
- // 创建可以向多个订阅者广播的远程subject
247
- const counterSubject = new RemoteSubject(ICounterObservable, 'main', { value: 0 });
248
+ // 创建集中式subject管理器
249
+ const subjectManager = new RemoteSubjectManager();
250
+
251
+ // 通过管理器创建远程subject
252
+ const counterSubject = subjectManager.createSubject(
253
+ ICounterObservable,
254
+ 'main',
255
+ { value: 0 }
256
+ );
248
257
 
249
258
  // 更新值并广播给所有订阅者
250
259
  setInterval(() => {
@@ -252,8 +261,43 @@ setInterval(() => {
252
261
  counterSubject.next(newValue);
253
262
  }, 1000);
254
263
 
264
+ // 管理器处理:
265
+ // - 消息路由和订阅管理
266
+ // - 在subject创建前到达的订阅排队
267
+ // - tab关闭时自动清理
268
+ // - 向多个订阅者广播
269
+
255
270
  // 清理
256
- // counterSubject.dispose();
271
+ // subjectManager.dispose(); // 这将处理所有subject
272
+ ```
273
+
274
+ ### RemoteSubjectManager 的核心特性
275
+
276
+ - **集中式消息中心**: 所有observable相关的消息都由管理器处理
277
+ - **队列管理**: 在subject创建前收到的订阅会被排队并稍后处理
278
+ - **资源管理**: tab关闭时自动清理订阅
279
+ - **类型安全**: 完整的TypeScript支持和恰当的类型检查
280
+
281
+ ### 架构
282
+
283
+ ```
284
+ ┌─────────────────┐ ┌─────────────────────────────────────┐ ┌─────────────────┐
285
+ │ 网页 │ │ 背景脚本 │ │ 内容脚本 │
286
+ ├─────────────────┤ ├─────────────────────────────────────┤ ├─────────────────┤
287
+ │ WebObservable │ │ RemoteSubjectManager │ │ContentObservable│
288
+ │ │ │ ┌─────────────────────────────────┐│ │ │
289
+ │ subscribe() ────┼───▶│ │ 消息路由和队列管理 │ │◄──┤ subscribe() │
290
+ │ │◄───│ │ │ │ │ │
291
+ └─────────────────┘ │ └─────────────────────────────────┘ │ └─────────────────┘
292
+ │ │ │
293
+ │ ┌─────────────▼─────────────────┐ │
294
+ │ │ RemoteSubject │ │
295
+ │ │ (纯状态管理) │ │
296
+ │ │ │ │
297
+ │ │ next() ─────────────────────▶ │ │
298
+ │ │ complete() ─────────────────▶ │ │
299
+ │ └───────────────────────────────┘ │
300
+ └──────────────────────────────────────┘
257
301
  ```
258
302
 
259
303
  ### 从网页订阅
@@ -310,21 +354,27 @@ const observable = new ContentObservable(
310
354
 
311
355
  ### Observable通信模式
312
356
 
313
- Observable系统支持多种通信模式:
357
+ Observable系统支持多种具有集中式管理的通信模式:
314
358
 
315
359
  ```typescript
316
360
  // 模式1: 背景脚本 → 网页 (通过内容脚本桥接器)
317
- // 背景脚本: RemoteSubject.next()
361
+ // 背景脚本: RemoteSubjectManager创建和管理RemoteSubject
362
+ // 背景脚本: RemoteSubject.next() → Manager路由到订阅者
318
363
  // 网页: WebObservable.subscribe()
319
364
 
320
365
  // 模式2: 背景脚本 → 内容脚本 (直接)
321
- // 背景脚本: RemoteSubject.next()
366
+ // 背景脚本: RemoteSubject.next() → Manager直接路由
322
367
  // 内容脚本: ContentObservable.subscribe()
323
368
 
324
369
  // 模式3: 背景脚本 → 网页和内容脚本同时
325
- // 背景脚本: RemoteSubject.next() (广播给所有订阅者)
370
+ // 背景脚本: RemoteSubject.next() → Manager广播给所有订阅者
326
371
  // 网页: WebObservable.subscribe()
327
372
  // 内容脚本: ContentObservable.subscribe()
373
+
374
+ // 模式4: Subject创建前的订阅 (队列管理)
375
+ // 订阅者: WebObservable.subscribe() → Manager将订阅排队
376
+ // 背景脚本: 稍后创建RemoteSubject → Manager处理排队的订阅
377
+ // 结果: 不会错过初始值,保证订阅顺序
328
378
  ```
329
379
 
330
380
  ## 高级用法
@@ -480,10 +530,12 @@ const [sum, user, file] = await Promise.all([
480
530
  - **`ContentRPC`**: 网页和背景脚本间的消息桥接器
481
531
  - **`WebRPCClient`**: 网页的RPC客户端
482
532
  - **`ContentRPCClient`**: 内容脚本的直接RPC客户端
533
+ - **`RemoteSubjectManager`**: 集中式observable消息管理系统
483
534
 
484
535
  ### Observable类
485
536
 
486
- - **`RemoteSubject<T>`**: 可以向多个订阅者广播的Observable subject
537
+ - **`RemoteSubjectManager`**: 管理订阅和消息路由的集中式消息中心
538
+ - **`RemoteSubject<T>`**: 与管理器配合进行纯状态管理的Observable subject
487
539
  - **`WebObservable<T>`**: 网页的Observable订阅者
488
540
  - **`ContentObservable<T>`**: 内容脚本的Observable订阅者
489
541
 
@@ -520,8 +572,9 @@ const [sum, user, file] = await Promise.all([
520
572
  4. **性能优化**
521
573
  - 避免频繁的小数据传输
522
574
  - 可能时考虑批处理操作
523
- - 对实时数据更新使用Observable模式
575
+ - 对实时数据更新使用Observable模式,通过 `RemoteSubjectManager` 进行高效消息路由
524
576
  - 在适当的地方实现缓存策略
577
+ - 管理器自动处理订阅排队以防止竞态条件
525
578
 
526
579
  5. **安全考虑**
527
580
  - 在服务实现中验证输入参数
@@ -1,5 +1,5 @@
1
1
  import type { Identifier } from './id';
2
- import type { SubjectLike } from './types';
2
+ import type { SubjectLike, RpcObservableUpdateMessage } from './types';
3
3
  import { Disposable } from './disposable';
4
4
  export declare class BackgroundRPC extends Disposable {
5
5
  private services;
@@ -10,12 +10,24 @@ export declare class RemoteSubject<T> extends Disposable implements SubjectLike<
10
10
  private identifier;
11
11
  private _key;
12
12
  private initialValue;
13
+ private manager;
13
14
  private completed;
14
- private get _finalKey();
15
- private senders;
16
- constructor(identifier: Identifier<T>, _key: string, initialValue: T);
17
- private _sendMessage;
15
+ get finalKey(): string;
16
+ constructor(identifier: Identifier<T>, _key: string, initialValue: T, manager: RemoteSubjectManager);
18
17
  next(value: T): void;
19
18
  complete(): void;
20
19
  subscribe(): () => void;
20
+ getInitialValue(): T;
21
+ }
22
+ export declare class RemoteSubjectManager extends Disposable {
23
+ private subjects;
24
+ private pendingSubscriptions;
25
+ private activeSenders;
26
+ constructor();
27
+ private handleSubscription;
28
+ private handleUnsubscription;
29
+ sendMessage(message: RpcObservableUpdateMessage<any>): void;
30
+ createSubject<T>(id: Identifier<T>, key: string, initialValue: T): RemoteSubject<T>;
31
+ getSubject<T>(key: string): RemoteSubject<T> | undefined;
32
+ removeSubject(key: string): void;
21
33
  }
@@ -69,78 +69,169 @@ export class RemoteSubject extends Disposable {
69
69
  identifier;
70
70
  _key;
71
71
  initialValue;
72
+ manager;
72
73
  completed = false;
73
- get _finalKey() {
74
+ get finalKey() {
74
75
  return `${this.identifier.key}-${this._key}`;
75
76
  }
76
- senders = new Set();
77
- constructor(identifier, _key, initialValue) {
77
+ constructor(identifier, _key, initialValue, manager) {
78
78
  super();
79
79
  this.identifier = identifier;
80
80
  this._key = _key;
81
81
  this.initialValue = initialValue;
82
- // 初始化时立即广播一次
82
+ this.manager = manager;
83
+ }
84
+ next(value) {
85
+ if (this.completed)
86
+ return;
87
+ this.manager.sendMessage({
88
+ operation: 'next',
89
+ key: this.finalKey,
90
+ value,
91
+ type: OBSERVABLE_EVENT
92
+ });
93
+ }
94
+ complete() {
95
+ if (this.completed)
96
+ return;
97
+ this.completed = true;
98
+ this.manager.sendMessage({
99
+ operation: 'complete',
100
+ key: this.finalKey,
101
+ type: OBSERVABLE_EVENT
102
+ });
103
+ }
104
+ subscribe() {
105
+ throw new Error('RemoteSubject should not be subscribed locally.');
106
+ }
107
+ getInitialValue() {
108
+ return this.initialValue;
109
+ }
110
+ }
111
+ export class RemoteSubjectManager extends Disposable {
112
+ subjects = new Map();
113
+ pendingSubscriptions = new Map(); // key -> senderIds
114
+ activeSenders = new Map(); // key -> senderIds
115
+ constructor() {
116
+ super();
83
117
  const handleMessage = (msg, sender) => {
84
118
  const senderId = sender.tab.id;
85
119
  if (!senderId)
86
120
  return;
87
121
  if (msg.type === SUBSCRIBABLE_OBSERVABLE) {
88
122
  const { key } = msg;
89
- if (key === this._finalKey) {
90
- this.senders.add(senderId);
91
- chrome.tabs.sendMessage(senderId, {
92
- operation: 'next',
93
- key: this._finalKey,
94
- value: this.initialValue,
95
- });
96
- }
123
+ this.handleSubscription(key, senderId);
97
124
  }
98
125
  if (msg.type === UNSUBSCRIBE_OBSERVABLE) {
99
126
  const { key } = msg;
100
- if (key === this._finalKey) {
101
- this.senders.delete(senderId);
102
- }
127
+ this.handleUnsubscription(key, senderId);
103
128
  }
104
129
  };
105
130
  chrome.runtime.onMessage.addListener(handleMessage);
106
131
  this.disposeWithMe(() => {
107
132
  chrome.runtime.onMessage.removeListener(handleMessage);
108
133
  });
109
- const handleRemove = (tabId) => {
110
- this.senders.delete(tabId);
134
+ const handleTabRemove = (tabId) => {
135
+ // 清理该 tab 的所有订阅
136
+ this.activeSenders.forEach((senders) => {
137
+ senders.delete(tabId);
138
+ });
139
+ this.pendingSubscriptions.forEach((senders) => {
140
+ senders.delete(tabId);
141
+ });
111
142
  };
112
- chrome.tabs.onRemoved.addListener(handleRemove);
143
+ chrome.tabs.onRemoved.addListener(handleTabRemove);
113
144
  this.disposeWithMe(() => {
114
- chrome.tabs.onRemoved.removeListener(handleRemove);
145
+ chrome.tabs.onRemoved.removeListener(handleTabRemove);
115
146
  });
116
147
  }
117
- _sendMessage(message) {
118
- chrome.runtime.sendMessage(message);
119
- this.senders.forEach(senderId => {
120
- chrome.tabs.sendMessage(senderId, message);
121
- });
148
+ handleSubscription(key, senderId) {
149
+ const subject = this.subjects.get(key);
150
+ if (subject) {
151
+ // Subject 已存在,直接处理订阅
152
+ if (!this.activeSenders.has(key)) {
153
+ this.activeSenders.set(key, new Set());
154
+ }
155
+ this.activeSenders.get(key).add(senderId);
156
+ // 发送初始值
157
+ chrome.tabs.sendMessage(senderId, {
158
+ operation: 'next',
159
+ key,
160
+ value: subject.getInitialValue(),
161
+ });
162
+ }
163
+ else {
164
+ // Subject 尚未创建,缓存到待处理队列
165
+ if (!this.pendingSubscriptions.has(key)) {
166
+ this.pendingSubscriptions.set(key, new Set());
167
+ }
168
+ this.pendingSubscriptions.get(key).add(senderId);
169
+ }
122
170
  }
123
- next(value) {
124
- if (this.completed)
125
- return;
126
- this._sendMessage({
127
- operation: 'next',
128
- key: this._finalKey,
129
- value,
130
- type: OBSERVABLE_EVENT
131
- });
171
+ handleUnsubscription(key, senderId) {
172
+ // 从活跃订阅中移除
173
+ const activeSenders = this.activeSenders.get(key);
174
+ if (activeSenders) {
175
+ activeSenders.delete(senderId);
176
+ if (activeSenders.size === 0) {
177
+ this.activeSenders.delete(key);
178
+ }
179
+ }
180
+ // 从待处理队列中移除
181
+ const pendingSenders = this.pendingSubscriptions.get(key);
182
+ if (pendingSenders) {
183
+ pendingSenders.delete(senderId);
184
+ if (pendingSenders.size === 0) {
185
+ this.pendingSubscriptions.delete(key);
186
+ }
187
+ }
132
188
  }
133
- complete() {
134
- if (this.completed)
135
- return;
136
- this.completed = true;
137
- this._sendMessage({
138
- operation: 'complete',
139
- key: this._finalKey,
140
- type: OBSERVABLE_EVENT
141
- });
189
+ sendMessage(message) {
190
+ const { key } = message;
191
+ // 发送到所有订阅的 tabs
192
+ const senders = this.activeSenders.get(key);
193
+ if (senders) {
194
+ senders.forEach(senderId => {
195
+ chrome.tabs.sendMessage(senderId, message);
196
+ });
197
+ }
142
198
  }
143
- subscribe() {
144
- throw new Error('RemoteSubject should not be subscribed locally.');
199
+ createSubject(id, key, initialValue) {
200
+ const subject = new RemoteSubject(id, key, initialValue, this);
201
+ this.subjects.set(key, subject);
202
+ // 处理待处理的订阅
203
+ const pendingSenders = this.pendingSubscriptions.get(key);
204
+ if (pendingSenders && pendingSenders.size > 0) {
205
+ if (!this.activeSenders.has(key)) {
206
+ this.activeSenders.set(key, new Set());
207
+ }
208
+ const activeSenders = this.activeSenders.get(key);
209
+ // 将待处理的订阅转移到活跃订阅
210
+ pendingSenders.forEach(senderId => {
211
+ activeSenders.add(senderId);
212
+ // 发送初始值
213
+ chrome.tabs.sendMessage(senderId, {
214
+ operation: 'next',
215
+ key,
216
+ value: initialValue,
217
+ });
218
+ });
219
+ // 清空待处理队列
220
+ this.pendingSubscriptions.delete(key);
221
+ }
222
+ return subject;
223
+ }
224
+ getSubject(key) {
225
+ return this.subjects.get(key);
226
+ }
227
+ removeSubject(key) {
228
+ const subject = this.subjects.get(key);
229
+ if (subject) {
230
+ subject.dispose();
231
+ this.subjects.delete(key);
232
+ // 清理相关的订阅信息
233
+ this.activeSenders.delete(key);
234
+ this.pendingSubscriptions.delete(key);
235
+ }
145
236
  }
146
237
  }
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { OBSERVABLE_EVENT, RPC_EVENT_NAME, RPC_RESPONSE_EVENT_NAME, UNSUBSCRIBE_OBSERVABLE } from './const';
2
2
  import { Disposable } from './disposable';
3
+ import { randomId } from './tool';
3
4
  export class RPCClient extends Disposable {
4
5
  messageAdapter;
5
6
  pending = new Map();
@@ -24,7 +25,7 @@ export class RPCClient extends Disposable {
24
25
  }));
25
26
  }
26
27
  call(service, method, args) {
27
- const id = crypto.randomUUID();
28
+ const id = randomId();
28
29
  return new Promise((resolve, reject) => {
29
30
  this.pending.set(id, { resolve, reject });
30
31
  const requestParam = {
package/dist/id.js CHANGED
@@ -1,8 +1,9 @@
1
- // shared/utils/identifier.ts
2
1
  /**
3
2
  * 创建一个 Identifier。
4
3
  * @param key 唯一字符串标识
5
4
  */
6
5
  export function createIdentifier(key) {
7
- return { key };
6
+ return {
7
+ key,
8
+ };
8
9
  }
package/dist/tool.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const randomId: () => string;
package/dist/tool.js ADDED
@@ -0,0 +1 @@
1
+ export const randomId = () => Math.random().toString(36).slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crx-rpc",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "A lightweight RPC framework for Chrome Extension (background <-> content <-> web)",
5
5
  "repository": {
6
6
  "type": "git",
package/src/background.ts CHANGED
@@ -79,84 +79,192 @@ export class BackgroundRPC extends Disposable {
79
79
  export class RemoteSubject<T> extends Disposable implements SubjectLike<T> {
80
80
  private completed = false;
81
81
 
82
- private get _finalKey() {
82
+ get finalKey() {
83
83
  return `${this.identifier.key}-${this._key}`;
84
84
  }
85
85
 
86
- private senders = new Set<number>();
87
-
88
86
  constructor(
89
87
  private identifier: Identifier<T>,
90
88
  private _key: string,
91
89
  private initialValue: T,
90
+ private manager: RemoteSubjectManager,
92
91
  ) {
93
92
  super();
94
- // 初始化时立即广播一次
93
+ }
94
+
95
+ next(value: T): void {
96
+ if (this.completed) return;
97
+ this.manager.sendMessage({
98
+ operation: 'next',
99
+ key: this.finalKey,
100
+ value,
101
+ type: OBSERVABLE_EVENT
102
+ });
103
+ }
104
+
105
+ complete(): void {
106
+ if (this.completed) return;
107
+ this.completed = true;
108
+ this.manager.sendMessage({
109
+ operation: 'complete',
110
+ key: this.finalKey,
111
+ type: OBSERVABLE_EVENT
112
+ });
113
+ }
114
+
115
+ subscribe(): () => void {
116
+ throw new Error('RemoteSubject should not be subscribed locally.');
117
+ }
118
+
119
+ getInitialValue(): T {
120
+ return this.initialValue;
121
+ }
122
+ }
123
+
124
+ export class RemoteSubjectManager extends Disposable {
125
+ private subjects = new Map<string, RemoteSubject<any>>();
126
+ private pendingSubscriptions = new Map<string, Set<number>>(); // key -> senderIds
127
+ private activeSenders = new Map<string, Set<number>>(); // key -> senderIds
128
+
129
+ constructor() {
130
+ super();
131
+
95
132
  const handleMessage = (msg: RpcObservableSubscribeMessage, sender: chrome.runtime.MessageSender) => {
96
133
  const senderId = sender.tab!.id!;
97
134
  if (!senderId) return;
98
135
 
99
136
  if (msg.type === SUBSCRIBABLE_OBSERVABLE) {
100
137
  const { key } = msg;
101
- if (key === this._finalKey) {
102
- this.senders.add(senderId);
103
- chrome.tabs.sendMessage(senderId, {
104
- operation: 'next',
105
- key: this._finalKey,
106
- value: this.initialValue,
107
- });
108
- }
138
+ this.handleSubscription(key, senderId);
109
139
  }
110
140
 
111
141
  if (msg.type === UNSUBSCRIBE_OBSERVABLE) {
112
142
  const { key } = msg;
113
- if (key === this._finalKey) {
114
- this.senders.delete(senderId);
115
- }
143
+ this.handleUnsubscription(key, senderId);
116
144
  }
117
- }
145
+ };
146
+
118
147
  chrome.runtime.onMessage.addListener(handleMessage);
119
148
  this.disposeWithMe(() => {
120
149
  chrome.runtime.onMessage.removeListener(handleMessage);
121
150
  });
122
151
 
123
- const handleRemove = (tabId: number) => {
124
- this.senders.delete(tabId);
152
+ const handleTabRemove = (tabId: number) => {
153
+ // 清理该 tab 的所有订阅
154
+ this.activeSenders.forEach((senders) => {
155
+ senders.delete(tabId);
156
+ });
157
+ this.pendingSubscriptions.forEach((senders) => {
158
+ senders.delete(tabId);
159
+ });
125
160
  };
126
- chrome.tabs.onRemoved.addListener(handleRemove);
161
+
162
+ chrome.tabs.onRemoved.addListener(handleTabRemove);
127
163
  this.disposeWithMe(() => {
128
- chrome.tabs.onRemoved.removeListener(handleRemove);
164
+ chrome.tabs.onRemoved.removeListener(handleTabRemove);
129
165
  });
130
166
  }
131
167
 
132
- private _sendMessage(message: RpcObservableUpdateMessage<any>) {
133
- chrome.runtime.sendMessage(message);
134
- this.senders.forEach(senderId => {
135
- chrome.tabs.sendMessage(senderId, message);
136
- });
168
+ private handleSubscription(key: string, senderId: number) {
169
+ const subject = this.subjects.get(key);
170
+
171
+ if (subject) {
172
+ // Subject 已存在,直接处理订阅
173
+ if (!this.activeSenders.has(key)) {
174
+ this.activeSenders.set(key, new Set());
175
+ }
176
+ this.activeSenders.get(key)!.add(senderId);
177
+
178
+ // 发送初始值
179
+ chrome.tabs.sendMessage(senderId, {
180
+ operation: 'next',
181
+ key,
182
+ value: subject.getInitialValue(),
183
+ });
184
+ } else {
185
+ // Subject 尚未创建,缓存到待处理队列
186
+ if (!this.pendingSubscriptions.has(key)) {
187
+ this.pendingSubscriptions.set(key, new Set());
188
+ }
189
+ this.pendingSubscriptions.get(key)!.add(senderId);
190
+ }
137
191
  }
138
192
 
139
- next(value: T): void {
140
- if (this.completed) return;
141
- this._sendMessage({
142
- operation: 'next',
143
- key: this._finalKey,
144
- value,
145
- type: OBSERVABLE_EVENT
146
- });
193
+ private handleUnsubscription(key: string, senderId: number) {
194
+ // 从活跃订阅中移除
195
+ const activeSenders = this.activeSenders.get(key);
196
+ if (activeSenders) {
197
+ activeSenders.delete(senderId);
198
+ if (activeSenders.size === 0) {
199
+ this.activeSenders.delete(key);
200
+ }
201
+ }
202
+
203
+ // 从待处理队列中移除
204
+ const pendingSenders = this.pendingSubscriptions.get(key);
205
+ if (pendingSenders) {
206
+ pendingSenders.delete(senderId);
207
+ if (pendingSenders.size === 0) {
208
+ this.pendingSubscriptions.delete(key);
209
+ }
210
+ }
147
211
  }
148
212
 
149
- complete(): void {
150
- if (this.completed) return;
151
- this.completed = true;
152
- this._sendMessage({
153
- operation: 'complete',
154
- key: this._finalKey,
155
- type: OBSERVABLE_EVENT
156
- });
213
+ sendMessage(message: RpcObservableUpdateMessage<any>) {
214
+ const { key } = message;
215
+
216
+ // 发送到所有订阅的 tabs
217
+ const senders = this.activeSenders.get(key);
218
+ if (senders) {
219
+ senders.forEach(senderId => {
220
+ chrome.tabs.sendMessage(senderId, message);
221
+ });
222
+ }
157
223
  }
158
224
 
159
- subscribe(): () => void {
160
- throw new Error('RemoteSubject should not be subscribed locally.');
225
+ createSubject<T>(id: Identifier<T>, key: string, initialValue: T): RemoteSubject<T> {
226
+ const subject = new RemoteSubject<T>(id, key, initialValue, this);
227
+ this.subjects.set(key, subject);
228
+
229
+ // 处理待处理的订阅
230
+ const pendingSenders = this.pendingSubscriptions.get(key);
231
+ if (pendingSenders && pendingSenders.size > 0) {
232
+ if (!this.activeSenders.has(key)) {
233
+ this.activeSenders.set(key, new Set());
234
+ }
235
+ const activeSenders = this.activeSenders.get(key)!;
236
+
237
+ // 将待处理的订阅转移到活跃订阅
238
+ pendingSenders.forEach(senderId => {
239
+ activeSenders.add(senderId);
240
+ // 发送初始值
241
+ chrome.tabs.sendMessage(senderId, {
242
+ operation: 'next',
243
+ key,
244
+ value: initialValue,
245
+ });
246
+ });
247
+
248
+ // 清空待处理队列
249
+ this.pendingSubscriptions.delete(key);
250
+ }
251
+
252
+ return subject;
161
253
  }
162
- }
254
+
255
+ getSubject<T>(key: string): RemoteSubject<T> | undefined {
256
+ return this.subjects.get(key) as RemoteSubject<T> | undefined;
257
+ }
258
+
259
+ removeSubject(key: string): void {
260
+ const subject = this.subjects.get(key);
261
+ if (subject) {
262
+ subject.dispose();
263
+ this.subjects.delete(key);
264
+
265
+ // 清理相关的订阅信息
266
+ this.activeSenders.delete(key);
267
+ this.pendingSubscriptions.delete(key);
268
+ }
269
+ }
270
+ }
package/src/client.ts CHANGED
@@ -2,6 +2,7 @@ import { OBSERVABLE_EVENT, RPC_EVENT_NAME, RPC_RESPONSE_EVENT_NAME, UNSUBSCRIBE_
2
2
  import type { RpcRequest, RpcResponse, RpcObservableUpdateMessage, IMessageAdapter } from './types';
3
3
  import type { Identifier } from './id';
4
4
  import { Disposable } from './disposable';
5
+ import { randomId } from './tool';
5
6
 
6
7
  // 类型工具:提取函数类型的参数和返回值类型
7
8
  type FunctionArgs<T> = T extends (...args: infer A) => any ? A : never;
@@ -40,7 +41,7 @@ export class RPCClient extends Disposable {
40
41
  }
41
42
 
42
43
  call<T = any>(service: string, method: string, args: any[]): Promise<T> {
43
- const id = crypto.randomUUID();
44
+ const id = randomId();
44
45
  return new Promise<T>((resolve, reject) => {
45
46
  this.pending.set(id, { resolve, reject });
46
47
  const requestParam: RpcRequest = {
package/src/id.ts CHANGED
@@ -1,5 +1,3 @@
1
- // shared/utils/identifier.ts
2
-
3
1
  /**
4
2
  * Identifier 类型,既携带类型信息,又在运行时能唯一标识。
5
3
  */
@@ -8,10 +6,12 @@ export interface Identifier<T> {
8
6
  __type?: T; // 用于 TS 类型推导,不会出现在运行时
9
7
  }
10
8
 
11
- /**
12
- * 创建一个 Identifier。
13
- * @param key 唯一字符串标识
14
- */
9
+ /**
10
+ * 创建一个 Identifier。
11
+ * @param key 唯一字符串标识
12
+ */
15
13
  export function createIdentifier<T>(key: string): Identifier<T> {
16
- return { key } as Identifier<T>;
14
+ return {
15
+ key,
16
+ } as Identifier<T>;
17
17
  }
package/src/tool.ts ADDED
@@ -0,0 +1 @@
1
+ export const randomId = () => Math.random().toString(36).slice(2);