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 +65 -12
- package/README.zh-CN.md +65 -12
- package/dist/background.d.ts +17 -5
- package/dist/background.js +135 -44
- package/dist/client.js +2 -1
- package/dist/id.js +3 -2
- package/dist/tool.d.ts +1 -0
- package/dist/tool.js +1 -0
- package/package.json +1 -1
- package/src/background.ts +152 -44
- package/src/client.ts +2 -1
- package/src/id.ts +7 -7
- package/src/tool.ts +1 -0
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,
|
|
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
|
|
231
|
-
const
|
|
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
|
-
//
|
|
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
|
|
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()
|
|
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
|
-
- **`
|
|
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
|
-
###
|
|
232
|
+
### RemoteSubjectManager 和 RemoteSubject(背景脚本)
|
|
233
|
+
|
|
234
|
+
`RemoteSubjectManager` 作为集中式消息中心处理所有订阅管理和消息路由,而 `RemoteSubject` 专注于纯状态管理。
|
|
233
235
|
|
|
234
236
|
```typescript
|
|
235
237
|
// background.ts
|
|
236
|
-
import { BackgroundRPC,
|
|
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
|
-
//
|
|
247
|
-
const
|
|
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
|
-
//
|
|
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
|
|
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
|
-
- **`
|
|
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
|
- 在服务实现中验证输入参数
|
package/dist/background.d.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|
}
|
package/dist/background.js
CHANGED
|
@@ -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
|
|
74
|
+
get finalKey() {
|
|
74
75
|
return `${this.identifier.key}-${this._key}`;
|
|
75
76
|
}
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
110
|
-
|
|
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(
|
|
143
|
+
chrome.tabs.onRemoved.addListener(handleTabRemove);
|
|
113
144
|
this.disposeWithMe(() => {
|
|
114
|
-
chrome.tabs.onRemoved.removeListener(
|
|
145
|
+
chrome.tabs.onRemoved.removeListener(handleTabRemove);
|
|
115
146
|
});
|
|
116
147
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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 =
|
|
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
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
124
|
-
|
|
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
|
-
|
|
161
|
+
|
|
162
|
+
chrome.tabs.onRemoved.addListener(handleTabRemove);
|
|
127
163
|
this.disposeWithMe(() => {
|
|
128
|
-
chrome.tabs.onRemoved.removeListener(
|
|
164
|
+
chrome.tabs.onRemoved.removeListener(handleTabRemove);
|
|
129
165
|
});
|
|
130
166
|
}
|
|
131
167
|
|
|
132
|
-
private
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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 =
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
/**
|
|
10
|
+
* 创建一个 Identifier。
|
|
11
|
+
* @param key 唯一字符串标识
|
|
12
|
+
*/
|
|
15
13
|
export function createIdentifier<T>(key: string): Identifier<T> {
|
|
16
|
-
return {
|
|
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);
|