@aspectly/core 0.1.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/LICENSE +20 -0
- package/README.md +188 -0
- package/dist/AspectlyBridge-hGuDjcI6.d.mts +242 -0
- package/dist/AspectlyBridge-hGuDjcI6.d.ts +242 -0
- package/dist/browser.d.mts +24 -0
- package/dist/browser.d.ts +24 -0
- package/dist/browser.js +398 -0
- package/dist/browser.js.map +1 -0
- package/dist/browser.mjs +396 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +420 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +412 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +71 -0
- package/src/AspectlyBridge.test.ts +78 -0
- package/src/AspectlyBridge.ts +44 -0
- package/src/BridgeBase.test.ts +132 -0
- package/src/BridgeBase.ts +63 -0
- package/src/BridgeCore.test.ts +254 -0
- package/src/BridgeCore.ts +138 -0
- package/src/BridgeInternal.test.ts +403 -0
- package/src/BridgeInternal.ts +305 -0
- package/src/browser.ts +27 -0
- package/src/index.ts +28 -0
- package/src/types.ts +134 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { BridgeInternal } from './BridgeInternal';
|
|
3
|
+
import { BridgeEventType, BridgeResultType, BridgeErrorType } from './types';
|
|
4
|
+
|
|
5
|
+
describe('BridgeInternal', () => {
|
|
6
|
+
let sendEvent: ReturnType<typeof vi.fn>;
|
|
7
|
+
let bridge: BridgeInternal;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
sendEvent = vi.fn();
|
|
11
|
+
bridge = new BridgeInternal(sendEvent);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('init', () => {
|
|
15
|
+
it('should send Init event with methods', () => {
|
|
16
|
+
const handlers = {
|
|
17
|
+
greet: async () => ({ message: 'hello' }),
|
|
18
|
+
getData: async () => ({ data: 'value' }),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
bridge.init(handlers);
|
|
22
|
+
|
|
23
|
+
expect(sendEvent).toHaveBeenCalledWith({
|
|
24
|
+
type: BridgeEventType.Init,
|
|
25
|
+
data: {
|
|
26
|
+
methods: ['greet', 'getData'],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should resolve immediately if called with empty handlers', async () => {
|
|
32
|
+
const result = await bridge.init({});
|
|
33
|
+
expect(result).toBe(true);
|
|
34
|
+
expect(sendEvent).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return promise that resolves on InitResult', async () => {
|
|
38
|
+
const handlers = {
|
|
39
|
+
greet: async () => ({ message: 'hello' }),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const initPromise = bridge.init(handlers);
|
|
43
|
+
|
|
44
|
+
// Simulate receiving InitResult
|
|
45
|
+
bridge.handleCoreEvent({
|
|
46
|
+
type: BridgeEventType.InitResult,
|
|
47
|
+
data: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await expect(initPromise).resolves.toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('handleCoreEvent', () => {
|
|
55
|
+
it('should handle Init event and send InitResult', () => {
|
|
56
|
+
bridge.handleCoreEvent({
|
|
57
|
+
type: BridgeEventType.Init,
|
|
58
|
+
data: { methods: ['test'] },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(sendEvent).toHaveBeenCalledWith({
|
|
62
|
+
type: BridgeEventType.InitResult,
|
|
63
|
+
data: true,
|
|
64
|
+
});
|
|
65
|
+
expect(bridge.isAvailable()).toBe(true);
|
|
66
|
+
expect(bridge.supports('test')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle Request event and call handler', async () => {
|
|
70
|
+
const handler = vi.fn().mockResolvedValue({ success: true });
|
|
71
|
+
|
|
72
|
+
// First initialize the bridge by receiving Init from other side
|
|
73
|
+
bridge.handleCoreEvent({
|
|
74
|
+
type: BridgeEventType.Init,
|
|
75
|
+
data: { methods: [] },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Start init and simulate InitResult response
|
|
79
|
+
const initPromise = bridge.init({ greet: handler });
|
|
80
|
+
bridge.handleCoreEvent({
|
|
81
|
+
type: BridgeEventType.InitResult,
|
|
82
|
+
data: true,
|
|
83
|
+
});
|
|
84
|
+
await initPromise;
|
|
85
|
+
sendEvent.mockClear();
|
|
86
|
+
|
|
87
|
+
bridge.handleCoreEvent({
|
|
88
|
+
type: BridgeEventType.Request,
|
|
89
|
+
data: {
|
|
90
|
+
method: 'greet',
|
|
91
|
+
params: { name: 'John' },
|
|
92
|
+
request_id: '1',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Wait for async handler
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
98
|
+
|
|
99
|
+
expect(handler).toHaveBeenCalledWith({ name: 'John' });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should send success result after handler completes', async () => {
|
|
103
|
+
const handler = vi.fn().mockResolvedValue({ message: 'Hello!' });
|
|
104
|
+
|
|
105
|
+
// Initialize bridge
|
|
106
|
+
bridge.handleCoreEvent({
|
|
107
|
+
type: BridgeEventType.Init,
|
|
108
|
+
data: { methods: [] },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const initPromise = bridge.init({ greet: handler });
|
|
112
|
+
bridge.handleCoreEvent({
|
|
113
|
+
type: BridgeEventType.InitResult,
|
|
114
|
+
data: true,
|
|
115
|
+
});
|
|
116
|
+
await initPromise;
|
|
117
|
+
sendEvent.mockClear();
|
|
118
|
+
|
|
119
|
+
bridge.handleCoreEvent({
|
|
120
|
+
type: BridgeEventType.Request,
|
|
121
|
+
data: {
|
|
122
|
+
method: 'greet',
|
|
123
|
+
params: {},
|
|
124
|
+
request_id: '1',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
129
|
+
|
|
130
|
+
expect(sendEvent).toHaveBeenCalledWith({
|
|
131
|
+
type: BridgeEventType.Result,
|
|
132
|
+
data: {
|
|
133
|
+
type: BridgeResultType.Success,
|
|
134
|
+
data: { message: 'Hello!' },
|
|
135
|
+
method: 'greet',
|
|
136
|
+
request_id: '1',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should send error result for unsupported methods', async () => {
|
|
142
|
+
bridge.handleCoreEvent({
|
|
143
|
+
type: BridgeEventType.Request,
|
|
144
|
+
data: {
|
|
145
|
+
method: 'unknownMethod',
|
|
146
|
+
params: {},
|
|
147
|
+
request_id: '1',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
152
|
+
|
|
153
|
+
expect(sendEvent).toHaveBeenCalledWith({
|
|
154
|
+
type: BridgeEventType.Result,
|
|
155
|
+
data: {
|
|
156
|
+
type: BridgeResultType.Error,
|
|
157
|
+
request_id: '1',
|
|
158
|
+
method: 'unknownMethod',
|
|
159
|
+
data: {
|
|
160
|
+
error_message: expect.any(String),
|
|
161
|
+
error_type: BridgeErrorType.UNSUPPORTED_METHOD,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should send error result when handler throws', async () => {
|
|
168
|
+
const handler = vi.fn().mockRejectedValue(new Error('Handler error'));
|
|
169
|
+
|
|
170
|
+
// Initialize bridge
|
|
171
|
+
bridge.handleCoreEvent({
|
|
172
|
+
type: BridgeEventType.Init,
|
|
173
|
+
data: { methods: [] },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const initPromise = bridge.init({ failing: handler });
|
|
177
|
+
bridge.handleCoreEvent({
|
|
178
|
+
type: BridgeEventType.InitResult,
|
|
179
|
+
data: true,
|
|
180
|
+
});
|
|
181
|
+
await initPromise;
|
|
182
|
+
sendEvent.mockClear();
|
|
183
|
+
|
|
184
|
+
bridge.handleCoreEvent({
|
|
185
|
+
type: BridgeEventType.Request,
|
|
186
|
+
data: {
|
|
187
|
+
method: 'failing',
|
|
188
|
+
params: {},
|
|
189
|
+
request_id: '1',
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
194
|
+
|
|
195
|
+
expect(sendEvent).toHaveBeenCalledWith({
|
|
196
|
+
type: BridgeEventType.Result,
|
|
197
|
+
data: {
|
|
198
|
+
type: BridgeResultType.Error,
|
|
199
|
+
request_id: '1',
|
|
200
|
+
method: 'failing',
|
|
201
|
+
data: {
|
|
202
|
+
error_message: 'Handler error',
|
|
203
|
+
error_type: BridgeErrorType.REJECTED,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('send', () => {
|
|
211
|
+
beforeEach(() => {
|
|
212
|
+
// Initialize bridge
|
|
213
|
+
bridge.handleCoreEvent({
|
|
214
|
+
type: BridgeEventType.Init,
|
|
215
|
+
data: { methods: ['test'] },
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should send request event', () => {
|
|
220
|
+
bridge.send('test', { foo: 'bar' });
|
|
221
|
+
|
|
222
|
+
expect(sendEvent).toHaveBeenCalledWith({
|
|
223
|
+
type: BridgeEventType.Request,
|
|
224
|
+
data: {
|
|
225
|
+
method: 'test',
|
|
226
|
+
params: { foo: 'bar' },
|
|
227
|
+
request_id: expect.any(String),
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should resolve promise on success result', async () => {
|
|
233
|
+
const promise = bridge.send<{ message: string }>('test', {});
|
|
234
|
+
|
|
235
|
+
// Simulate result
|
|
236
|
+
bridge.handleCoreEvent({
|
|
237
|
+
type: BridgeEventType.Result,
|
|
238
|
+
data: {
|
|
239
|
+
type: BridgeResultType.Success,
|
|
240
|
+
data: { message: 'success' },
|
|
241
|
+
request_id: '0',
|
|
242
|
+
method: 'test',
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await expect(promise).resolves.toEqual({ message: 'success' });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should reject promise on error result', async () => {
|
|
250
|
+
const promise = bridge.send('test', {});
|
|
251
|
+
|
|
252
|
+
bridge.handleCoreEvent({
|
|
253
|
+
type: BridgeEventType.Result,
|
|
254
|
+
data: {
|
|
255
|
+
type: BridgeResultType.Error,
|
|
256
|
+
data: {
|
|
257
|
+
error_type: BridgeErrorType.REJECTED,
|
|
258
|
+
error_message: 'Failed',
|
|
259
|
+
},
|
|
260
|
+
request_id: '0',
|
|
261
|
+
method: 'test',
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await expect(promise).rejects.toEqual({
|
|
266
|
+
error_type: BridgeErrorType.REJECTED,
|
|
267
|
+
error_message: 'Failed',
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should reject with BRIDGE_NOT_AVAILABLE when not initialized', async () => {
|
|
272
|
+
const uninitializedBridge = new BridgeInternal(sendEvent);
|
|
273
|
+
const promise = uninitializedBridge.send('test', {});
|
|
274
|
+
|
|
275
|
+
await expect(promise).rejects.toEqual({
|
|
276
|
+
error_type: BridgeErrorType.BRIDGE_NOT_AVAILABLE,
|
|
277
|
+
error_message: 'Bridge is not available',
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('subscribe/unsubscribe', () => {
|
|
283
|
+
it('should notify listeners on result events', () => {
|
|
284
|
+
const listener = vi.fn();
|
|
285
|
+
bridge.subscribe(listener);
|
|
286
|
+
|
|
287
|
+
bridge.handleCoreEvent({
|
|
288
|
+
type: BridgeEventType.Result,
|
|
289
|
+
data: {
|
|
290
|
+
type: BridgeResultType.Success,
|
|
291
|
+
data: { foo: 'bar' },
|
|
292
|
+
method: 'test',
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(listener).toHaveBeenCalledWith({
|
|
297
|
+
type: BridgeResultType.Success,
|
|
298
|
+
data: { foo: 'bar' },
|
|
299
|
+
method: 'test',
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should stop notifying after unsubscribe', () => {
|
|
304
|
+
const listener = vi.fn();
|
|
305
|
+
bridge.subscribe(listener);
|
|
306
|
+
bridge.unsubscribe(listener);
|
|
307
|
+
|
|
308
|
+
bridge.handleCoreEvent({
|
|
309
|
+
type: BridgeEventType.Result,
|
|
310
|
+
data: {
|
|
311
|
+
type: BridgeResultType.Success,
|
|
312
|
+
data: {},
|
|
313
|
+
method: 'test',
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(listener).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('supports', () => {
|
|
322
|
+
it('should return false before initialization', () => {
|
|
323
|
+
expect(bridge.supports('test')).toBe(false);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should return true for supported methods after init', () => {
|
|
327
|
+
bridge.handleCoreEvent({
|
|
328
|
+
type: BridgeEventType.Init,
|
|
329
|
+
data: { methods: ['greet', 'getData'] },
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(bridge.supports('greet')).toBe(true);
|
|
333
|
+
expect(bridge.supports('getData')).toBe(true);
|
|
334
|
+
expect(bridge.supports('unknown')).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('isAvailable', () => {
|
|
339
|
+
it('should return false before initialization', () => {
|
|
340
|
+
expect(bridge.isAvailable()).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should return true after Init event', () => {
|
|
344
|
+
bridge.handleCoreEvent({
|
|
345
|
+
type: BridgeEventType.Init,
|
|
346
|
+
data: { methods: [] },
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(bridge.isAvailable()).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('timeout', () => {
|
|
354
|
+
it('should use custom timeout from options', async () => {
|
|
355
|
+
vi.useFakeTimers();
|
|
356
|
+
|
|
357
|
+
const customBridge = new BridgeInternal(sendEvent, { timeout: 100 });
|
|
358
|
+
|
|
359
|
+
// Initialize bridge first
|
|
360
|
+
customBridge.handleCoreEvent({
|
|
361
|
+
type: BridgeEventType.Init,
|
|
362
|
+
data: { methods: [] },
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const slowHandler = vi.fn().mockImplementation(
|
|
366
|
+
() => new Promise((resolve) => setTimeout(resolve, 200))
|
|
367
|
+
);
|
|
368
|
+
const initPromise = customBridge.init({ slow: slowHandler });
|
|
369
|
+
customBridge.handleCoreEvent({
|
|
370
|
+
type: BridgeEventType.InitResult,
|
|
371
|
+
data: true,
|
|
372
|
+
});
|
|
373
|
+
await initPromise;
|
|
374
|
+
sendEvent.mockClear();
|
|
375
|
+
|
|
376
|
+
customBridge.handleCoreEvent({
|
|
377
|
+
type: BridgeEventType.Request,
|
|
378
|
+
data: {
|
|
379
|
+
method: 'slow',
|
|
380
|
+
params: {},
|
|
381
|
+
request_id: '1',
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await vi.advanceTimersByTimeAsync(150);
|
|
386
|
+
|
|
387
|
+
expect(sendEvent).toHaveBeenCalledWith({
|
|
388
|
+
type: BridgeEventType.Result,
|
|
389
|
+
data: {
|
|
390
|
+
type: BridgeResultType.Error,
|
|
391
|
+
request_id: '1',
|
|
392
|
+
method: 'slow',
|
|
393
|
+
data: {
|
|
394
|
+
error_message: 'Execution timeout exceeded',
|
|
395
|
+
error_type: BridgeErrorType.METHOD_EXECUTION_TIMEOUT,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
vi.useRealTimers();
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BridgeData,
|
|
3
|
+
BridgeEvent,
|
|
4
|
+
BridgeHandlers,
|
|
5
|
+
BridgeInitEvent,
|
|
6
|
+
BridgeInitResultEvent,
|
|
7
|
+
BridgeListener,
|
|
8
|
+
BridgeOptions,
|
|
9
|
+
BridgeRequestEvent,
|
|
10
|
+
BridgeResultData,
|
|
11
|
+
BridgeResultError,
|
|
12
|
+
BridgeResultEvent,
|
|
13
|
+
} from './types';
|
|
14
|
+
import {
|
|
15
|
+
BridgeErrorType,
|
|
16
|
+
BridgeEventType,
|
|
17
|
+
BridgeResultType,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a bridge event with the specified type and data
|
|
22
|
+
*/
|
|
23
|
+
export const internalEvent = (
|
|
24
|
+
type: BridgeEventType,
|
|
25
|
+
data: BridgeData
|
|
26
|
+
): BridgeEvent => ({
|
|
27
|
+
type,
|
|
28
|
+
data,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a result event
|
|
33
|
+
*/
|
|
34
|
+
export const internalResultEvent = (data: BridgeData): BridgeEvent =>
|
|
35
|
+
internalEvent(BridgeEventType.Result, data);
|
|
36
|
+
|
|
37
|
+
interface InternalRequestPromise {
|
|
38
|
+
reject: (error: BridgeResultError) => void;
|
|
39
|
+
resolve: (result: BridgeResultData) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface InitPromise {
|
|
43
|
+
reject: () => void;
|
|
44
|
+
resolve: (success: boolean) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type InternalEventSender = (event: BridgeEvent) => void;
|
|
48
|
+
|
|
49
|
+
const DEFAULT_TIMEOUT = 100000;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* BridgeInternal handles the business logic of the bridge protocol.
|
|
53
|
+
* It manages request/response lifecycle, handler registration, and event routing.
|
|
54
|
+
*/
|
|
55
|
+
export class BridgeInternal {
|
|
56
|
+
private requests: InternalRequestPromise[] = [];
|
|
57
|
+
private handlers: BridgeHandlers = {};
|
|
58
|
+
private available = false;
|
|
59
|
+
private supportedMethods: string[] = [];
|
|
60
|
+
private listeners: BridgeListener[] = [];
|
|
61
|
+
private initPromise?: InitPromise;
|
|
62
|
+
private readonly sendEvent: InternalEventSender;
|
|
63
|
+
private readonly timeout: number;
|
|
64
|
+
|
|
65
|
+
constructor(sendEvent: InternalEventSender, options?: BridgeOptions) {
|
|
66
|
+
this.sendEvent = sendEvent;
|
|
67
|
+
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Subscribe to all result events
|
|
72
|
+
*/
|
|
73
|
+
public subscribe = (listener: BridgeListener): number => {
|
|
74
|
+
return this.listeners.push(listener);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Unsubscribe from result events
|
|
79
|
+
*/
|
|
80
|
+
public unsubscribe = (listener: BridgeListener): void => {
|
|
81
|
+
this.listeners = this.listeners.filter(
|
|
82
|
+
(oldListener) => oldListener !== listener
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
private checkDiff = (a: string[], b: string[]): boolean => {
|
|
87
|
+
return (
|
|
88
|
+
a.filter((x) => !b.includes(x)).length > 0 ||
|
|
89
|
+
b.filter((x) => !a.includes(x)).length > 0
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Initialize the bridge with handlers
|
|
95
|
+
* @param handlers Map of method names to handler functions
|
|
96
|
+
* @returns Promise that resolves when the other side acknowledges
|
|
97
|
+
*/
|
|
98
|
+
public init = (handlers: BridgeHandlers = {}): Promise<boolean> => {
|
|
99
|
+
const oldMethods = Object.keys(this.handlers);
|
|
100
|
+
const newMethods = Object.keys(handlers);
|
|
101
|
+
this.handlers = handlers;
|
|
102
|
+
if (!this.checkDiff(oldMethods, newMethods)) {
|
|
103
|
+
return Promise.resolve(true);
|
|
104
|
+
}
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
this.initPromise = { resolve, reject };
|
|
107
|
+
this.sendEvent(
|
|
108
|
+
internalEvent(BridgeEventType.Init, {
|
|
109
|
+
methods: newMethods,
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Handle incoming bridge events
|
|
117
|
+
*/
|
|
118
|
+
public handleCoreEvent = (event: BridgeEvent): void => {
|
|
119
|
+
const { type, data } = event;
|
|
120
|
+
switch (type) {
|
|
121
|
+
case BridgeEventType.Init:
|
|
122
|
+
this.handleInit(data as BridgeInitEvent);
|
|
123
|
+
break;
|
|
124
|
+
case BridgeEventType.InitResult:
|
|
125
|
+
this.handleInitResult(data as BridgeInitResultEvent);
|
|
126
|
+
break;
|
|
127
|
+
case BridgeEventType.Request:
|
|
128
|
+
this.handleRequest(data as BridgeRequestEvent);
|
|
129
|
+
break;
|
|
130
|
+
case BridgeEventType.Result:
|
|
131
|
+
this.handleResult(data as BridgeResultEvent);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handle incoming requests and execute the appropriate handler
|
|
138
|
+
*/
|
|
139
|
+
public handleRequest = (request: BridgeRequestEvent): void => {
|
|
140
|
+
const { method, params, request_id } = request;
|
|
141
|
+
new Promise<BridgeResultData>((resolve, reject) => {
|
|
142
|
+
let timeout = false;
|
|
143
|
+
if (!Object.prototype.hasOwnProperty.call(this.handlers, method)) {
|
|
144
|
+
reject({
|
|
145
|
+
error_type: BridgeErrorType.UNSUPPORTED_METHOD,
|
|
146
|
+
error: new Error(`Handler for «${method}» is not registered`),
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
timeout = true;
|
|
152
|
+
reject({
|
|
153
|
+
error_type: BridgeErrorType.METHOD_EXECUTION_TIMEOUT,
|
|
154
|
+
error: new Error('Execution timeout exceeded'),
|
|
155
|
+
});
|
|
156
|
+
}, this.timeout);
|
|
157
|
+
const handler = this.handlers[method];
|
|
158
|
+
if (!handler) {
|
|
159
|
+
reject({
|
|
160
|
+
error_type: BridgeErrorType.UNSUPPORTED_METHOD,
|
|
161
|
+
error: new Error(`Handler for «${method}» is undefined`),
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
handler(params)
|
|
166
|
+
.then((result) => {
|
|
167
|
+
if (timeout) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
resolve(result as BridgeResultData);
|
|
172
|
+
})
|
|
173
|
+
.catch((error: Error) => {
|
|
174
|
+
if (timeout) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
reject({
|
|
179
|
+
error_type: BridgeErrorType.REJECTED,
|
|
180
|
+
error: error,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
})
|
|
184
|
+
.then((data: BridgeResultData) => {
|
|
185
|
+
this.sendEvent(
|
|
186
|
+
internalResultEvent({
|
|
187
|
+
type: BridgeResultType.Success,
|
|
188
|
+
data,
|
|
189
|
+
method,
|
|
190
|
+
request_id,
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
})
|
|
194
|
+
.catch(
|
|
195
|
+
({
|
|
196
|
+
error_type,
|
|
197
|
+
error,
|
|
198
|
+
}: {
|
|
199
|
+
error_type: BridgeErrorType;
|
|
200
|
+
error: Error;
|
|
201
|
+
}) => {
|
|
202
|
+
this.sendEvent(
|
|
203
|
+
internalResultEvent({
|
|
204
|
+
type: BridgeResultType.Error,
|
|
205
|
+
request_id,
|
|
206
|
+
method,
|
|
207
|
+
data: {
|
|
208
|
+
error_message: error.message,
|
|
209
|
+
error_type,
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
private handleResult = (result: BridgeResultEvent): void => {
|
|
218
|
+
this.handleRequestResult(result);
|
|
219
|
+
this.listeners.forEach((listener) => listener(result));
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
private handleRequestResult = (result: BridgeResultEvent): void => {
|
|
223
|
+
if (!result || !Object.prototype.hasOwnProperty.call(result, 'request_id')) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!Object.prototype.hasOwnProperty.call(result, 'type')) {
|
|
227
|
+
console.warn('unknown result', result);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const { request_id, data, type } = result;
|
|
231
|
+
const request = this.requests[Number(request_id)];
|
|
232
|
+
if (!request) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (type === BridgeResultType.Success) {
|
|
236
|
+
request.resolve(data);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (type === BridgeResultType.Error) {
|
|
240
|
+
request.reject(data as BridgeResultError);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
private handleInit = (data: BridgeInitEvent): void => {
|
|
245
|
+
this.available = true;
|
|
246
|
+
this.supportedMethods = data.methods;
|
|
247
|
+
this.sendEvent(internalEvent(BridgeEventType.InitResult, true));
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
private handleInitResult = (success: BridgeInitResultEvent): void => {
|
|
251
|
+
if (success) {
|
|
252
|
+
this.initPromise?.resolve(true);
|
|
253
|
+
} else {
|
|
254
|
+
this.initPromise?.reject();
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Send a request to the other side
|
|
260
|
+
* @param method Method name to invoke
|
|
261
|
+
* @param params Parameters to pass to the method
|
|
262
|
+
* @returns Promise that resolves with the result
|
|
263
|
+
*/
|
|
264
|
+
public send = <TResult = unknown>(
|
|
265
|
+
method: string,
|
|
266
|
+
params: object
|
|
267
|
+
): Promise<TResult> =>
|
|
268
|
+
new Promise((resolve, reject) => {
|
|
269
|
+
const request_id = (
|
|
270
|
+
this.requests.push({ resolve: resolve as (result: BridgeResultData) => void, reject }) - 1
|
|
271
|
+
).toString();
|
|
272
|
+
if (!this.isAvailable()) {
|
|
273
|
+
this.handleCoreEvent(
|
|
274
|
+
internalResultEvent({
|
|
275
|
+
type: BridgeResultType.Error,
|
|
276
|
+
request_id,
|
|
277
|
+
method,
|
|
278
|
+
data: {
|
|
279
|
+
error_message: 'Bridge is not available',
|
|
280
|
+
error_type: BridgeErrorType.BRIDGE_NOT_AVAILABLE,
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
this.sendEvent(
|
|
287
|
+
internalEvent(BridgeEventType.Request, {
|
|
288
|
+
method,
|
|
289
|
+
params,
|
|
290
|
+
request_id,
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if a method is supported by the other side
|
|
297
|
+
*/
|
|
298
|
+
public supports = (method: string): boolean =>
|
|
299
|
+
this.supportedMethods.includes(method);
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if the bridge is available (initialized)
|
|
303
|
+
*/
|
|
304
|
+
public isAvailable = (): boolean => this.available;
|
|
305
|
+
}
|