@ermis-network/ermis-chat-sdk 1.0.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/init-call.js +9 -0
- package/dist/index.browser.cjs +778 -1628
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.full-bundle.min.js +16 -18
- package/dist/index.browser.full-bundle.min.js.map +1 -1
- package/dist/index.browser.mjs +780 -1630
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +778 -1628
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +173 -40
- package/dist/index.d.ts +173 -40
- package/dist/index.mjs +780 -1630
- package/dist/index.mjs.map +1 -1
- package/dist/wasm_worker.worker.mjs +1596 -0
- package/dist/wasm_worker.worker.mjs.map +1 -0
- package/package.json +2 -2
- package/public/ermis_call_node_wasm_bg.wasm +0 -0
- package/src/channel.ts +117 -44
- package/src/channel_state.ts +6 -1
- package/src/client.ts +198 -56
- package/src/ermis_call_node.ts +123 -55
- package/src/index.ts +2 -1
- package/src/media_stream_receiver.ts +103 -35
- package/src/media_stream_sender.ts +72 -7
- package/src/signal_message.ts +48 -23
- package/src/system_message.ts +169 -27
- package/src/types.ts +13 -0
- package/src/utils.ts +22 -3
- package/src/wasm/ermis_call_node_wasm.d.ts +80 -78
- package/src/wasm/ermis_call_node_wasm.js +1427 -1357
- package/src/wasm_worker.ts +219 -0
- package/src/wasm_worker_proxy.ts +244 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM Worker — chạy ErmisCall WASM instance trên Worker thread.
|
|
3
|
+
*
|
|
4
|
+
* Mọi WASM sync calls (sendFrame, sendAudioFrame, sendControlFrame) chạy ở đây,
|
|
5
|
+
* KHÔNG BAO GIỜ block Main Thread. Khi P2P connection congested (peer mạng yếu),
|
|
6
|
+
* Worker thread bị block nhưng UI vẫn responsive.
|
|
7
|
+
*
|
|
8
|
+
* Communication: postMessage với Transferable buffers (zero-copy).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// @ts-ignore — WASM module import sẽ được resolve bởi bundler
|
|
12
|
+
import { initSync, ErmisCall } from './wasm/ermis_call_node_wasm';
|
|
13
|
+
|
|
14
|
+
let ermisCall: ErmisCall | null = null;
|
|
15
|
+
let isRecvActive = false;
|
|
16
|
+
|
|
17
|
+
/** Request types từ Main Thread */
|
|
18
|
+
type WorkerRequest =
|
|
19
|
+
| { id: number; type: 'init'; wasmBytes: ArrayBuffer }
|
|
20
|
+
| { id: number; type: 'spawn'; relayUrls: string[] }
|
|
21
|
+
| { id: number; type: 'getLocalEndpointAddr' }
|
|
22
|
+
| { id: number; type: 'connect'; address: string }
|
|
23
|
+
| { id: number; type: 'acceptConnection' }
|
|
24
|
+
| { id: number; type: 'sendFrame'; data: Uint8Array }
|
|
25
|
+
| { id: number; type: 'beginWithGop'; data: Uint8Array }
|
|
26
|
+
| { id: number; type: 'sendAudioFrame'; data: Uint8Array }
|
|
27
|
+
| { id: number; type: 'sendControlFrame'; data: Uint8Array }
|
|
28
|
+
| { id: number; type: 'closeEndpoint' }
|
|
29
|
+
| { id: number; type: 'closeConnection' }
|
|
30
|
+
| { id: number; type: 'networkChange' }
|
|
31
|
+
| { id: number; type: 'startRecvLoop' }
|
|
32
|
+
| { id: number; type: 'stopRecvLoop' }
|
|
33
|
+
| { id: number; type: 'getStats' }
|
|
34
|
+
| { id: number; type: 'terminate' };
|
|
35
|
+
|
|
36
|
+
/** Helper: gửi success response */
|
|
37
|
+
function sendResult(id: number, data?: any) {
|
|
38
|
+
self.postMessage({ id, type: 'result', data });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Helper: gửi error response */
|
|
42
|
+
function sendError(id: number, error: string) {
|
|
43
|
+
self.postMessage({ id, type: 'error', error });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Helper: sleep */
|
|
47
|
+
function sleep(ms: number): Promise<void> {
|
|
48
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Receive loop — chạy liên tục trong Worker.
|
|
53
|
+
* asyncRecv() block Worker thread khi chờ data — KHÔNG ảnh hưởng Main Thread.
|
|
54
|
+
* Data nhận được gửi về Main Thread qua postMessage với Transferable.
|
|
55
|
+
*/
|
|
56
|
+
async function recvLoop() {
|
|
57
|
+
while (isRecvActive && ermisCall) {
|
|
58
|
+
try {
|
|
59
|
+
const data = await ermisCall.asyncRecv();
|
|
60
|
+
// Transfer buffer ownership → zero-copy
|
|
61
|
+
(self as unknown as { postMessage: (msg: any, transfer?: Transferable[]) => void }).postMessage(
|
|
62
|
+
{ type: 'recv_data', data },
|
|
63
|
+
[data.buffer],
|
|
64
|
+
);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!isRecvActive) break; // Đã dừng — không cần log
|
|
67
|
+
(self as unknown as { postMessage: (msg: any, transfer?: Transferable[]) => void }).postMessage({
|
|
68
|
+
type: 'recv_error',
|
|
69
|
+
error: String(error),
|
|
70
|
+
});
|
|
71
|
+
await sleep(200); // Backoff trước khi thử lại
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Main message handler */
|
|
77
|
+
self.onmessage = async (e: MessageEvent<WorkerRequest>) => {
|
|
78
|
+
const msg = e.data;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
switch (msg.type) {
|
|
82
|
+
// === LIFECYCLE ===
|
|
83
|
+
case 'init': {
|
|
84
|
+
// Nhận WASM bytes từ Main Thread → compile thành Module → initSync
|
|
85
|
+
const wasmModule = new WebAssembly.Module(msg.wasmBytes);
|
|
86
|
+
initSync(wasmModule);
|
|
87
|
+
ermisCall = new ErmisCall();
|
|
88
|
+
sendResult(msg.id);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case 'spawn': {
|
|
93
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
94
|
+
await ermisCall.spawn(msg.relayUrls);
|
|
95
|
+
sendResult(msg.id);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case 'getLocalEndpointAddr': {
|
|
100
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
101
|
+
const addr = await ermisCall.getLocalEndpointAddr();
|
|
102
|
+
sendResult(msg.id, addr);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'connect': {
|
|
107
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
108
|
+
await ermisCall.connect(msg.address);
|
|
109
|
+
sendResult(msg.id);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'acceptConnection': {
|
|
114
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
115
|
+
await ermisCall.acceptConnection();
|
|
116
|
+
sendResult(msg.id);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// === DATA SEND (blocking calls — now safe in Worker) ===
|
|
121
|
+
case 'sendFrame': {
|
|
122
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
123
|
+
ermisCall.sendFrame(msg.data);
|
|
124
|
+
sendResult(msg.id);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'beginWithGop': {
|
|
129
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
130
|
+
ermisCall.beginWithGop(msg.data);
|
|
131
|
+
sendResult(msg.id);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'sendAudioFrame': {
|
|
136
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
137
|
+
ermisCall.sendAudioFrame(msg.data);
|
|
138
|
+
sendResult(msg.id);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'sendControlFrame': {
|
|
143
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
144
|
+
ermisCall.sendControlFrame(msg.data);
|
|
145
|
+
sendResult(msg.id);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// === RECEIVE LOOP ===
|
|
150
|
+
case 'startRecvLoop': {
|
|
151
|
+
isRecvActive = true;
|
|
152
|
+
sendResult(msg.id);
|
|
153
|
+
// Start loop (fire-and-forget — runs until stopRecvLoop)
|
|
154
|
+
recvLoop();
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case 'stopRecvLoop': {
|
|
159
|
+
isRecvActive = false;
|
|
160
|
+
sendResult(msg.id);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// === CONTROL ===
|
|
165
|
+
case 'closeEndpoint': {
|
|
166
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
167
|
+
await ermisCall.closeEndpoint();
|
|
168
|
+
sendResult(msg.id);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case 'closeConnection': {
|
|
173
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
174
|
+
ermisCall.closeConnection();
|
|
175
|
+
sendResult(msg.id);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case 'networkChange': {
|
|
180
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
181
|
+
ermisCall.networkChange();
|
|
182
|
+
sendResult(msg.id);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case 'getStats': {
|
|
187
|
+
if (!ermisCall) throw new Error('WASM not initialized');
|
|
188
|
+
const stats = ermisCall.getStats();
|
|
189
|
+
sendResult(msg.id, stats);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'terminate': {
|
|
194
|
+
isRecvActive = false;
|
|
195
|
+
if (ermisCall) {
|
|
196
|
+
try {
|
|
197
|
+
ermisCall.closeConnection();
|
|
198
|
+
} catch {
|
|
199
|
+
/* ignore */
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
ermisCall.free();
|
|
203
|
+
} catch {
|
|
204
|
+
/* ignore */
|
|
205
|
+
}
|
|
206
|
+
ermisCall = null;
|
|
207
|
+
}
|
|
208
|
+
sendResult(msg.id);
|
|
209
|
+
// Worker sẽ bị terminate bởi Main Thread sau khi nhận result
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
sendError((msg as any).id, `Unknown message type: ${(msg as any).type}`);
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
sendError(msg.id, String(error));
|
|
218
|
+
}
|
|
219
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WasmWorkerProxy — Proxy class chạy trên Main Thread.
|
|
3
|
+
*
|
|
4
|
+
* Implement interface INodeCall, nhưng mọi method call đều được forward
|
|
5
|
+
* tới Web Worker qua postMessage. Main Thread KHÔNG BAO GIỜ gọi WASM trực tiếp.
|
|
6
|
+
*
|
|
7
|
+
* Data transfer dùng Transferable buffers (zero-copy).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { INodeCall } from './types';
|
|
11
|
+
|
|
12
|
+
/** Response types từ Worker */
|
|
13
|
+
type WorkerResponse =
|
|
14
|
+
| { id: number; type: 'result'; data?: any }
|
|
15
|
+
| { id: number; type: 'error'; error: string }
|
|
16
|
+
| { type: 'recv_data'; data: Uint8Array }
|
|
17
|
+
| { type: 'recv_error'; error: string };
|
|
18
|
+
|
|
19
|
+
export class WasmWorkerProxy implements INodeCall {
|
|
20
|
+
private worker: Worker;
|
|
21
|
+
private nextId = 0;
|
|
22
|
+
private pendingCalls = new Map<number, { resolve: (data?: any) => void; reject: (error: Error) => void }>();
|
|
23
|
+
|
|
24
|
+
/** Queue cho asyncRecv — Worker gửi data về, Main Thread consume từng cái */
|
|
25
|
+
private recvResolveQueue: Array<(data: Uint8Array) => void> = [];
|
|
26
|
+
private recvDataQueue: Uint8Array[] = [];
|
|
27
|
+
private recvErrorQueue: Array<(error: Error) => void> = [];
|
|
28
|
+
|
|
29
|
+
/** Static cache — persist across Worker instances */
|
|
30
|
+
private static cachedBlobUrl: string | null = null;
|
|
31
|
+
private static cachedWasmBytes: ArrayBuffer | null = null;
|
|
32
|
+
|
|
33
|
+
constructor(workerUrl: string | URL) {
|
|
34
|
+
// Cache Blob URL — chỉ fetch worker script 1 lần
|
|
35
|
+
if (!WasmWorkerProxy.cachedBlobUrl) {
|
|
36
|
+
const url = workerUrl.toString();
|
|
37
|
+
const xhr = new XMLHttpRequest();
|
|
38
|
+
xhr.open('GET', url, false); // synchronous
|
|
39
|
+
xhr.send();
|
|
40
|
+
|
|
41
|
+
if (xhr.status === 200) {
|
|
42
|
+
const blob = new Blob([xhr.responseText], { type: 'application/javascript' });
|
|
43
|
+
WasmWorkerProxy.cachedBlobUrl = URL.createObjectURL(blob);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (WasmWorkerProxy.cachedBlobUrl) {
|
|
48
|
+
this.worker = new Worker(WasmWorkerProxy.cachedBlobUrl, { type: 'module' });
|
|
49
|
+
} else {
|
|
50
|
+
// Fallback: try direct URL (works when server has correct MIME config)
|
|
51
|
+
this.worker = new Worker(workerUrl, { type: 'module' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => this.handleMessage(e.data);
|
|
55
|
+
this.worker.onerror = (e) => {
|
|
56
|
+
console.error('🔴 WASM Worker error:', e.message);
|
|
57
|
+
// Reject all pending calls
|
|
58
|
+
this.pendingCalls.forEach(({ reject }) => reject(new Error(`Worker error: ${e.message}`)));
|
|
59
|
+
this.pendingCalls.clear();
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// === LIFECYCLE ===
|
|
64
|
+
|
|
65
|
+
/** Initialize WASM trong Worker — fetch bytes 1 lần, gửi cached bytes cho Worker */
|
|
66
|
+
async init(wasmPath?: string): Promise<void> {
|
|
67
|
+
// Fetch WASM bytes 1 lần duy nhất trên Main Thread
|
|
68
|
+
if (!WasmWorkerProxy.cachedWasmBytes) {
|
|
69
|
+
const absoluteWasmPath = new URL(
|
|
70
|
+
wasmPath || '/ermis_call_node_wasm_bg.wasm',
|
|
71
|
+
window.location.origin,
|
|
72
|
+
).href;
|
|
73
|
+
const response = await fetch(absoluteWasmPath);
|
|
74
|
+
WasmWorkerProxy.cachedWasmBytes = await response.arrayBuffer();
|
|
75
|
+
}
|
|
76
|
+
// Gửi copy bytes cho Worker (transfer → zero-copy, original stays cached)
|
|
77
|
+
const bytesCopy = WasmWorkerProxy.cachedWasmBytes.slice(0);
|
|
78
|
+
await this.call('init', { wasmBytes: bytesCopy }, [bytesCopy]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Spawn WASM node */
|
|
82
|
+
async spawn(relayUrls: string[]): Promise<void> {
|
|
83
|
+
await this.call('spawn', { relayUrls });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Lấy local endpoint address */
|
|
87
|
+
async getLocalEndpointAddr(): Promise<string> {
|
|
88
|
+
return await this.call('getLocalEndpointAddr');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// === INodeCall IMPLEMENTATION ===
|
|
92
|
+
|
|
93
|
+
async connect(address: string): Promise<void> {
|
|
94
|
+
await this.call('connect', { address });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async acceptConnection(): Promise<void> {
|
|
98
|
+
await this.call('acceptConnection');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async sendFrame(data: Uint8Array): Promise<void> {
|
|
102
|
+
// Transfer buffer ownership → zero-copy Main→Worker
|
|
103
|
+
await this.call('sendFrame', { data }, [data.buffer]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async beginWithGop(data: Uint8Array): Promise<void> {
|
|
107
|
+
await this.call('beginWithGop', { data }, [data.buffer]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async sendAudioFrame(data: Uint8Array): Promise<void> {
|
|
111
|
+
await this.call('sendAudioFrame', { data }, [data.buffer]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async sendControlFrame(data: Uint8Array): Promise<void> {
|
|
115
|
+
// Control frames nhỏ — clone OK, không cần transfer
|
|
116
|
+
await this.call('sendControlFrame', { data });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* asyncRecv — nhận data từ Worker recv loop.
|
|
121
|
+
*
|
|
122
|
+
* Worker chạy recv loop nội bộ, gửi data về qua postMessage.
|
|
123
|
+
* Method này trả về Promise resolve khi có data mới.
|
|
124
|
+
* KHÔNG BAO GIỜ block Main Thread — chỉ chờ postMessage event.
|
|
125
|
+
*/
|
|
126
|
+
async asyncRecv(): Promise<Uint8Array> {
|
|
127
|
+
// Nếu đã có data trong queue (Worker gửi trước khi Main gọi asyncRecv)
|
|
128
|
+
if (this.recvDataQueue.length > 0) {
|
|
129
|
+
return this.recvDataQueue.shift()!;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Chờ data từ Worker
|
|
133
|
+
return new Promise<Uint8Array>((resolve, reject) => {
|
|
134
|
+
this.recvResolveQueue.push(resolve);
|
|
135
|
+
this.recvErrorQueue.push(reject);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// === CONTROL ===
|
|
140
|
+
|
|
141
|
+
/** Bắt đầu recv loop trong Worker */
|
|
142
|
+
async startRecvLoop(): Promise<void> {
|
|
143
|
+
await this.call('startRecvLoop');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Dừng recv loop trong Worker */
|
|
147
|
+
async stopRecvLoop(): Promise<void> {
|
|
148
|
+
await this.call('stopRecvLoop');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async closeEndpoint(): Promise<void> {
|
|
152
|
+
await this.call('closeEndpoint');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
closeConnection(): void {
|
|
156
|
+
// Fire-and-forget — không cần chờ response
|
|
157
|
+
const id = this.nextId++;
|
|
158
|
+
this.worker.postMessage({ id, type: 'closeConnection' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
networkChange(): void {
|
|
162
|
+
const id = this.nextId++;
|
|
163
|
+
this.worker.postMessage({ id, type: 'networkChange' });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getStats(): Promise<any> {
|
|
167
|
+
return await this.call('getStats');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Terminate Worker — gọi khi call kết thúc */
|
|
171
|
+
async terminate(): Promise<void> {
|
|
172
|
+
try {
|
|
173
|
+
await this.call('terminate');
|
|
174
|
+
} catch {
|
|
175
|
+
/* ignore — worker may already be dead */
|
|
176
|
+
}
|
|
177
|
+
this.worker.terminate();
|
|
178
|
+
|
|
179
|
+
// KHÔNG revoke Blob URL và KHÔNG clear cached Module
|
|
180
|
+
// — chúng được reuse cho call tiếp theo
|
|
181
|
+
|
|
182
|
+
// Reject remaining recv waiters
|
|
183
|
+
this.recvResolveQueue = [];
|
|
184
|
+
this.recvErrorQueue.forEach((reject) => reject(new Error('Worker terminated')));
|
|
185
|
+
this.recvErrorQueue = [];
|
|
186
|
+
this.recvDataQueue = [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// === INTERNAL ===
|
|
190
|
+
|
|
191
|
+
private handleMessage(msg: WorkerResponse) {
|
|
192
|
+
// Stream data từ recv loop (không có id)
|
|
193
|
+
if (msg.type === 'recv_data') {
|
|
194
|
+
const data = (msg as any).data as Uint8Array;
|
|
195
|
+
if (this.recvResolveQueue.length > 0) {
|
|
196
|
+
// Có consumer đang chờ → resolve ngay
|
|
197
|
+
const resolve = this.recvResolveQueue.shift()!;
|
|
198
|
+
this.recvErrorQueue.shift(); // Remove paired reject
|
|
199
|
+
resolve(data);
|
|
200
|
+
} else {
|
|
201
|
+
// Chưa có consumer → queue data
|
|
202
|
+
this.recvDataQueue.push(data);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (msg.type === 'recv_error') {
|
|
208
|
+
const error = new Error((msg as any).error);
|
|
209
|
+
if (this.recvErrorQueue.length > 0) {
|
|
210
|
+
const reject = this.recvErrorQueue.shift()!;
|
|
211
|
+
this.recvResolveQueue.shift(); // Remove paired resolve
|
|
212
|
+
reject(error);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// RPC response (có id)
|
|
218
|
+
const { id } = msg;
|
|
219
|
+
const pending = this.pendingCalls.get(id);
|
|
220
|
+
if (!pending) return;
|
|
221
|
+
|
|
222
|
+
this.pendingCalls.delete(id);
|
|
223
|
+
|
|
224
|
+
if (msg.type === 'error') {
|
|
225
|
+
pending.reject(new Error(msg.error));
|
|
226
|
+
} else {
|
|
227
|
+
pending.resolve(msg.data);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Send RPC call to Worker and wait for response */
|
|
232
|
+
private call(type: string, payload?: Record<string, any>, transfer?: Transferable[]): Promise<any> {
|
|
233
|
+
const id = this.nextId++;
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
this.pendingCalls.set(id, { resolve, reject });
|
|
236
|
+
const message = { id, type, ...payload };
|
|
237
|
+
if (transfer && transfer.length > 0) {
|
|
238
|
+
this.worker.postMessage(message, transfer);
|
|
239
|
+
} else {
|
|
240
|
+
this.worker.postMessage(message);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|