@aspectly/core 0.1.0 → 2.0.8

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.
@@ -1,9 +1,11 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { BridgeCore } from './BridgeCore';
3
+ import type { Transport } from '@aspectly/transports';
3
4
 
4
5
  describe('BridgeCore', () => {
5
6
  beforeEach(() => {
6
7
  vi.clearAllMocks();
8
+ BridgeCore.resetTransport();
7
9
  });
8
10
 
9
11
  describe('wrapBridgeEvent', () => {
@@ -103,152 +105,89 @@ describe('BridgeCore', () => {
103
105
  });
104
106
  });
105
107
 
106
- describe('browserListener', () => {
107
- it('should extract data from MessageEvent', () => {
108
- const listener = vi.fn();
109
- const browserListener = BridgeCore.browserListener(listener);
110
- const event = { method: 'test' };
111
- const messageEvent = {
112
- data: JSON.stringify({ type: 'BridgeEvent', event }),
113
- } as MessageEvent;
114
-
115
- browserListener(messageEvent);
116
-
117
- expect(listener).toHaveBeenCalledWith(event);
118
- });
119
-
120
- it('should ignore events without data', () => {
121
- const listener = vi.fn();
122
- const browserListener = BridgeCore.browserListener(listener);
123
-
124
- browserListener({ data: undefined } as MessageEvent);
125
- browserListener({} as MessageEvent);
126
-
127
- expect(listener).not.toHaveBeenCalled();
128
- });
129
- });
130
-
131
- describe('webViewListener', () => {
132
- it('should extract data from WebView nativeEvent', () => {
133
- const listener = vi.fn();
134
- const webViewListener = BridgeCore.webViewListener(listener);
135
- const event = { method: 'test' };
136
- const webViewMessage = {
137
- nativeEvent: {
138
- data: JSON.stringify({ type: 'BridgeEvent', event }),
139
- },
140
- };
141
-
142
- webViewListener(webViewMessage);
143
-
144
- expect(listener).toHaveBeenCalledWith(event);
145
- });
146
-
147
- it('should ignore messages without nativeEvent', () => {
148
- const listener = vi.fn();
149
- const webViewListener = BridgeCore.webViewListener(listener);
150
-
151
- webViewListener({});
152
- webViewListener({ nativeEvent: {} });
153
- webViewListener({ nativeEvent: { data: undefined } });
154
-
155
- expect(listener).not.toHaveBeenCalled();
156
- });
157
- });
158
-
159
108
  describe('sendEvent', () => {
160
- let originalParent: Window;
161
- let originalRNW: typeof window.ReactNativeWebView;
162
-
163
- beforeEach(() => {
164
- originalParent = window.parent;
165
- originalRNW = window.ReactNativeWebView;
166
- });
167
-
168
- afterEach(() => {
169
- Object.defineProperty(window, 'parent', {
170
- value: originalParent,
171
- writable: true,
172
- configurable: true,
173
- });
174
- (window as any).ReactNativeWebView = originalRNW;
175
- });
176
-
177
- it('should send to iframe parent when available', () => {
178
- (window as any).ReactNativeWebView = undefined;
179
- const mockParent = {
180
- postMessage: vi.fn(),
109
+ it('should send via the configured transport', () => {
110
+ const mockTransport: Transport = {
111
+ name: 'mock',
112
+ isAvailable: () => true,
113
+ send: vi.fn(),
114
+ subscribe: vi.fn(),
181
115
  };
182
- Object.defineProperty(window, 'parent', {
183
- value: mockParent,
184
- writable: true,
185
- configurable: true,
186
- });
116
+ BridgeCore.setTransport(mockTransport);
187
117
 
188
118
  const event = { method: 'test' };
189
119
  BridgeCore.sendEvent(event);
190
120
 
191
- expect(mockParent.postMessage).toHaveBeenCalledWith(
192
- expect.any(String),
193
- '*'
194
- );
121
+ expect(mockTransport.send).toHaveBeenCalledWith(expect.any(String));
195
122
  });
196
123
 
197
- it('should send to ReactNativeWebView when available', () => {
198
- const mockRNW = {
199
- postMessage: vi.fn(),
200
- };
201
- (window as any).ReactNativeWebView = mockRNW;
202
-
124
+ it('should not throw with null transport (fallback)', () => {
203
125
  const event = { method: 'test' };
204
- BridgeCore.sendEvent(event);
205
-
206
- expect(mockRNW.postMessage).toHaveBeenCalledWith(expect.any(String));
207
- });
208
-
209
- it('should not send when window.parent === window (top level)', () => {
210
- (window as any).ReactNativeWebView = undefined;
211
- Object.defineProperty(window, 'parent', {
212
- value: window,
213
- writable: true,
214
- configurable: true,
215
- });
216
-
217
- const event = { method: 'test' };
218
- // This should not throw
219
126
  expect(() => BridgeCore.sendEvent(event)).not.toThrow();
220
127
  });
221
128
  });
222
129
 
223
130
  describe('subscribe', () => {
224
- it('should subscribe to message events', () => {
225
- const listener = vi.fn();
226
- const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
131
+ it('should subscribe via the configured transport', () => {
132
+ const mockUnsubscribe = vi.fn();
133
+ const mockTransport: Transport = {
134
+ name: 'mock',
135
+ isAvailable: () => true,
136
+ send: vi.fn(),
137
+ subscribe: vi.fn().mockReturnValue(mockUnsubscribe),
138
+ };
139
+ BridgeCore.setTransport(mockTransport);
227
140
 
141
+ const listener = vi.fn();
228
142
  const unsubscribe = BridgeCore.subscribe(listener);
229
143
 
230
- expect(addEventListenerSpy).toHaveBeenCalledWith(
231
- 'message',
232
- expect.any(Function)
233
- );
144
+ expect(mockTransport.subscribe).toHaveBeenCalledWith(expect.any(Function));
234
145
  expect(typeof unsubscribe).toBe('function');
235
-
236
- addEventListenerSpy.mockRestore();
237
146
  });
238
147
 
239
- it('should return cleanup function that removes listener', () => {
240
- const listener = vi.fn();
241
- const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
148
+ it('should return cleanup function from transport', () => {
149
+ const mockUnsubscribe = vi.fn();
150
+ const mockTransport: Transport = {
151
+ name: 'mock',
152
+ isAvailable: () => true,
153
+ send: vi.fn(),
154
+ subscribe: vi.fn().mockReturnValue(mockUnsubscribe),
155
+ };
156
+ BridgeCore.setTransport(mockTransport);
242
157
 
158
+ const listener = vi.fn();
243
159
  const unsubscribe = BridgeCore.subscribe(listener);
244
160
  unsubscribe();
245
161
 
246
- expect(removeEventListenerSpy).toHaveBeenCalledWith(
247
- 'message',
248
- expect.any(Function)
249
- );
162
+ expect(mockUnsubscribe).toHaveBeenCalled();
163
+ });
164
+ });
165
+
166
+ describe('transport management', () => {
167
+ it('should allow setting a custom transport', () => {
168
+ const mockTransport: Transport = {
169
+ name: 'custom',
170
+ isAvailable: () => true,
171
+ send: vi.fn(),
172
+ subscribe: vi.fn(),
173
+ };
174
+ BridgeCore.setTransport(mockTransport);
175
+
176
+ expect(BridgeCore.getTransportName()).toBe('custom');
177
+ });
178
+
179
+ it('should reset transport to auto-detect', () => {
180
+ const mockTransport: Transport = {
181
+ name: 'custom',
182
+ isAvailable: () => true,
183
+ send: vi.fn(),
184
+ subscribe: vi.fn(),
185
+ };
186
+ BridgeCore.setTransport(mockTransport);
187
+ BridgeCore.resetTransport();
250
188
 
251
- removeEventListenerSpy.mockRestore();
189
+ // After reset, auto-detection will pick a default transport
190
+ expect(BridgeCore.getTransportName()).not.toBe('custom');
252
191
  });
253
192
  });
254
193
  });
package/src/BridgeCore.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Low-level bridge core handling platform-specific message passing
3
+ * Now delegates to @aspectly/transports for platform detection
3
4
  */
4
5
 
6
+ import { detectTransport, type Transport } from '@aspectly/transports';
7
+
5
8
  export type Event = unknown;
6
9
 
7
10
  interface BridgeCoreEvent {
@@ -9,33 +12,57 @@ interface BridgeCoreEvent {
9
12
  event: Event;
10
13
  }
11
14
 
12
- interface WebViewMessage {
13
- nativeEvent?: {
14
- data?: string;
15
- };
16
- }
17
-
18
15
  export type BridgeCoreListener = (event: Event) => void;
19
16
 
20
- declare global {
21
- interface Window {
22
- ReactNativeWebView?: {
23
- postMessage: (message: string) => void;
24
- };
25
- }
26
- }
27
-
28
17
  /**
29
18
  * BridgeCore handles the low-level platform detection and message serialization.
30
19
  * It provides static methods for wrapping events, creating listeners, and sending messages.
20
+ *
21
+ * Platform detection is now handled by @aspectly/transports which supports:
22
+ * - CefSharp (Chromium Embedded Framework for .NET)
23
+ * - React Native WebView
24
+ * - Iframe (window.parent.postMessage)
25
+ * - Custom transports via TransportRegistry
31
26
  */
32
27
  export class BridgeCore {
33
28
  private static BRIDGE_EVENT_TYPE = 'BridgeEvent';
29
+ private static transport: Transport | null = null;
34
30
 
35
31
  private static isJSONObject = (str: string): boolean => {
36
32
  return str.startsWith('{') && str.endsWith('}');
37
33
  };
38
34
 
35
+ /**
36
+ * Get the current transport (lazy initialization)
37
+ */
38
+ private static getTransport(): Transport {
39
+ if (!BridgeCore.transport) {
40
+ BridgeCore.transport = detectTransport();
41
+ }
42
+ return BridgeCore.transport;
43
+ }
44
+
45
+ /**
46
+ * Set a custom transport (useful for testing or manual configuration)
47
+ */
48
+ public static setTransport(transport: Transport): void {
49
+ BridgeCore.transport = transport;
50
+ }
51
+
52
+ /**
53
+ * Reset transport to auto-detect on next use
54
+ */
55
+ public static resetTransport(): void {
56
+ BridgeCore.transport = null;
57
+ }
58
+
59
+ /**
60
+ * Get the name of the current transport
61
+ */
62
+ public static getTransportName(): string {
63
+ return BridgeCore.getTransport().name;
64
+ }
65
+
39
66
  /**
40
67
  * Wraps an event in the bridge protocol format
41
68
  */
@@ -78,61 +105,21 @@ export class BridgeCore {
78
105
  };
79
106
 
80
107
  /**
81
- * Creates a browser-specific message event listener
82
- */
83
- static browserListener = (listener: BridgeCoreListener) => {
84
- const triggerEvent = BridgeCore.wrapListener(listener);
85
- return (originalEvent: MessageEvent): void => {
86
- if (!originalEvent?.data) {
87
- return;
88
- }
89
- triggerEvent(originalEvent.data);
90
- };
91
- };
92
-
93
- /**
94
- * Creates a React Native WebView message listener
95
- */
96
- static webViewListener = (listener: BridgeCoreListener) => {
97
- const triggerEvent = BridgeCore.wrapListener(listener);
98
- return (originalEvent: WebViewMessage): void => {
99
- if (!originalEvent?.nativeEvent?.data) {
100
- return;
101
- }
102
- triggerEvent(originalEvent.nativeEvent.data);
103
- };
104
- };
105
-
106
- /**
107
- * Sends an event to the parent context (WebView or iframe parent)
108
+ * Sends an event to the parent context using the detected transport
108
109
  */
109
110
  static sendEvent = (event: Event): void => {
110
111
  const bridgeEvent = BridgeCore.wrapBridgeEvent(event);
111
- if (typeof window === 'undefined') {
112
- console.warn('Window is undefined');
113
- return;
114
- }
115
- const RNW = window.ReactNativeWebView;
116
- if (typeof RNW?.postMessage === 'function') {
117
- RNW.postMessage(`'${bridgeEvent}'`);
118
- return;
119
- }
120
- if (window.parent === window) {
121
- return;
122
- }
123
- window.parent.postMessage(bridgeEvent, '*');
112
+ const transport = BridgeCore.getTransport();
113
+ transport.send(bridgeEvent);
124
114
  };
125
115
 
126
116
  /**
127
- * Subscribes to window message events
117
+ * Subscribes to incoming messages via the detected transport
128
118
  * @returns Cleanup function to unsubscribe
129
119
  */
130
120
  static subscribe = (listener: BridgeCoreListener): VoidFunction => {
131
- const browserListener = BridgeCore.browserListener(listener);
132
- if (typeof window === 'undefined' || !window.addEventListener) {
133
- return () => {};
134
- }
135
- window.addEventListener('message', browserListener);
136
- return () => window.removeEventListener('message', browserListener);
121
+ const transport = BridgeCore.getTransport();
122
+ const wrappedListener = BridgeCore.wrapListener(listener);
123
+ return transport.subscribe(wrappedListener);
137
124
  };
138
125
  }
@@ -318,6 +318,69 @@ describe('BridgeInternal', () => {
318
318
  });
319
319
  });
320
320
 
321
+ describe('registerHandler/unregisterHandler', () => {
322
+ it('should register a handler that can be called via Request', async () => {
323
+ const handler = vi.fn().mockResolvedValue({ ok: true });
324
+ bridge.registerHandler('myMethod', handler);
325
+
326
+ bridge.handleCoreEvent({
327
+ type: BridgeEventType.Request,
328
+ data: { method: 'myMethod', params: { x: 1 }, request_id: '1' },
329
+ });
330
+
331
+ await new Promise((resolve) => setTimeout(resolve, 50));
332
+
333
+ expect(handler).toHaveBeenCalledWith({ x: 1 });
334
+ expect(sendEvent).toHaveBeenCalledWith({
335
+ type: BridgeEventType.Result,
336
+ data: {
337
+ type: BridgeResultType.Success,
338
+ data: { ok: true },
339
+ method: 'myMethod',
340
+ request_id: '1',
341
+ },
342
+ });
343
+ });
344
+
345
+ it('should unregister a handler', async () => {
346
+ bridge.registerHandler('myMethod', vi.fn().mockResolvedValue({}));
347
+ bridge.unregisterHandler('myMethod');
348
+
349
+ bridge.handleCoreEvent({
350
+ type: BridgeEventType.Request,
351
+ data: { method: 'myMethod', params: {}, request_id: '1' },
352
+ });
353
+
354
+ await new Promise((resolve) => setTimeout(resolve, 50));
355
+
356
+ expect(sendEvent).toHaveBeenCalledWith({
357
+ type: BridgeEventType.Result,
358
+ data: expect.objectContaining({
359
+ type: BridgeResultType.Error,
360
+ data: expect.objectContaining({
361
+ error_type: BridgeErrorType.UNSUPPORTED_METHOD,
362
+ }),
363
+ }),
364
+ });
365
+ });
366
+
367
+ it('should include registerHandler methods in init', () => {
368
+ bridge.registerHandler('pre1', vi.fn().mockResolvedValue({}));
369
+ bridge.registerHandler('pre2', vi.fn().mockResolvedValue({}));
370
+
371
+ bridge.init({
372
+ pre1: vi.fn().mockResolvedValue({}),
373
+ pre2: vi.fn().mockResolvedValue({}),
374
+ extra: vi.fn().mockResolvedValue({}),
375
+ });
376
+
377
+ expect(sendEvent).toHaveBeenCalledWith({
378
+ type: BridgeEventType.Init,
379
+ data: { methods: ['pre1', 'pre2', 'extra'] },
380
+ });
381
+ });
382
+ });
383
+
321
384
  describe('supports', () => {
322
385
  it('should return false before initialization', () => {
323
386
  expect(bridge.supports('test')).toBe(false);
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  BridgeData,
3
3
  BridgeEvent,
4
+ BridgeHandler,
4
5
  BridgeHandlers,
5
6
  BridgeInitEvent,
6
7
  BridgeInitResultEvent,
@@ -67,6 +68,32 @@ export class BridgeInternal {
67
68
  this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
68
69
  }
69
70
 
71
+ /**
72
+ * Reset bridge state for a new connection context.
73
+ * Call this when the remote side has changed (e.g., new popup window).
74
+ */
75
+ public reset = (): void => {
76
+ this.handlers = {};
77
+ this.available = false;
78
+ this.supportedMethods = [];
79
+ this.initPromise = undefined;
80
+ };
81
+
82
+ /**
83
+ * Register a single handler for a method.
84
+ * Can be called before or after init().
85
+ */
86
+ public registerHandler = (method: string, handler: BridgeHandler): void => {
87
+ this.handlers[method] = handler;
88
+ };
89
+
90
+ /**
91
+ * Remove a previously registered handler.
92
+ */
93
+ public unregisterHandler = (method: string): void => {
94
+ delete this.handlers[method];
95
+ };
96
+
70
97
  /**
71
98
  * Subscribe to all result events
72
99
  */
package/src/index.ts CHANGED
@@ -4,6 +4,24 @@ export { BridgeBase } from './BridgeBase';
4
4
  export { BridgeInternal } from './BridgeInternal';
5
5
  export { BridgeCore } from './BridgeCore';
6
6
 
7
+ // Re-export transports for convenience
8
+ export {
9
+ detectTransport,
10
+ registerTransport,
11
+ TransportRegistry,
12
+ CefSharpTransport,
13
+ ReactNativeTransport,
14
+ IframeTransport,
15
+ NullTransport,
16
+ } from '@aspectly/transports';
17
+
18
+ export type {
19
+ Transport,
20
+ TransportDetector,
21
+ TransportListener,
22
+ TransportUnsubscribe,
23
+ } from '@aspectly/transports';
24
+
7
25
  // Type exports
8
26
  export type {
9
27
  BridgeEvent,
package/LICENSE DELETED
@@ -1,20 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2022 Zhan Isaakian
4
- Permission is hereby granted, free of charge, to any person obtaining a copy
5
- of this software and associated documentation files (the "Software"), to deal
6
- in the Software without restriction, including without limitation the rights
7
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- copies of the Software, and to permit persons to whom the Software is
9
- furnished to do so, subject to the following conditions:
10
-
11
- The above copyright notice and this permission notice shall be included in all
12
- copies or substantial portions of the Software.
13
-
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
- SOFTWARE.