@kookapp/web-bridge 0.0.1
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 +144 -0
- package/dist/child/ChildBridge.d.ts +40 -0
- package/dist/child/ChildBridge.d.ts.map +1 -0
- package/dist/child/ChildBridge.js +124 -0
- package/dist/child/index.d.ts +4 -0
- package/dist/child/index.d.ts.map +1 -0
- package/dist/child/index.js +2 -0
- package/dist/child/utils.d.ts +7 -0
- package/dist/child/utils.d.ts.map +1 -0
- package/dist/child/utils.js +22 -0
- package/dist/constants.d.ts +29 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +28 -0
- package/dist/errors/BridgeError.d.ts +21 -0
- package/dist/errors/BridgeError.d.ts.map +1 -0
- package/dist/errors/BridgeError.js +35 -0
- package/dist/errors/errorFactory.d.ts +14 -0
- package/dist/errors/errorFactory.d.ts.map +1 -0
- package/dist/errors/errorFactory.js +13 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/parent/IframeChannel.d.ts +44 -0
- package/dist/parent/IframeChannel.d.ts.map +1 -0
- package/dist/parent/IframeChannel.js +96 -0
- package/dist/parent/ParentBridge.d.ts +41 -0
- package/dist/parent/ParentBridge.d.ts.map +1 -0
- package/dist/parent/ParentBridge.js +101 -0
- package/dist/parent/index.d.ts +6 -0
- package/dist/parent/index.d.ts.map +1 -0
- package/dist/parent/index.js +3 -0
- package/dist/parent/utils.d.ts +6 -0
- package/dist/parent/utils.d.ts.map +1 -0
- package/dist/parent/utils.js +23 -0
- package/dist/shared/EventEmitter.d.ts +13 -0
- package/dist/shared/EventEmitter.d.ts.map +1 -0
- package/dist/shared/EventEmitter.js +47 -0
- package/dist/shared/IdGenerator.d.ts +11 -0
- package/dist/shared/IdGenerator.d.ts.map +1 -0
- package/dist/shared/IdGenerator.js +16 -0
- package/dist/shared/MessageHandler.d.ts +75 -0
- package/dist/shared/MessageHandler.d.ts.map +1 -0
- package/dist/shared/MessageHandler.js +197 -0
- package/dist/shared/OriginValidator.d.ts +14 -0
- package/dist/shared/OriginValidator.d.ts.map +1 -0
- package/dist/shared/OriginValidator.js +53 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +31 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IframeChannel for managing communication with a specific iframe
|
|
3
|
+
*/
|
|
4
|
+
import { MessageHandler } from '@/shared/MessageHandler';
|
|
5
|
+
export class IframeChannel extends MessageHandler {
|
|
6
|
+
constructor(iframe, allowedOrigins, timeout = 5000, debug = false) {
|
|
7
|
+
super(allowedOrigins, timeout, debug);
|
|
8
|
+
this.connected = false;
|
|
9
|
+
this.iframe = iframe;
|
|
10
|
+
this.iframeOrigin = this.extractOrigin(iframe.src);
|
|
11
|
+
// Setup message listener for this channel
|
|
12
|
+
this.setupMessageListener((event) => {
|
|
13
|
+
// Only accept messages from the specific iframe
|
|
14
|
+
if (event.source === this.iframe.contentWindow) {
|
|
15
|
+
this.handleMessage(event.data, event.origin);
|
|
16
|
+
if (!this.connected) {
|
|
17
|
+
this.connected = true;
|
|
18
|
+
this.eventEmitter.emit('ready');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract origin from URL
|
|
25
|
+
*/
|
|
26
|
+
extractOrigin(url) {
|
|
27
|
+
try {
|
|
28
|
+
return new URL(url).origin;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// If iframe.src is not a valid URL, return current location origin
|
|
32
|
+
return window.location.origin;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Call a handler in the iframe
|
|
37
|
+
*/
|
|
38
|
+
async callHandler(handlerName, data, callback) {
|
|
39
|
+
if (!this.iframe.contentWindow) {
|
|
40
|
+
throw new Error('iframe contentWindow is not available');
|
|
41
|
+
}
|
|
42
|
+
const promise = this.callHandlerWithTimeout(this.iframeOrigin, handlerName, data);
|
|
43
|
+
if (callback) {
|
|
44
|
+
promise.then(callback).catch(error => console.error(error));
|
|
45
|
+
}
|
|
46
|
+
return promise;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Send message to iframe
|
|
50
|
+
*/
|
|
51
|
+
sendMessage(targetOrigin, message) {
|
|
52
|
+
if (!this.iframe.contentWindow) {
|
|
53
|
+
if (this.debug) {
|
|
54
|
+
console.warn('[WebBridge] iframe contentWindow is not available');
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Add version info
|
|
59
|
+
message._version = '1.0.0';
|
|
60
|
+
this.iframe.contentWindow.postMessage(message, targetOrigin);
|
|
61
|
+
if (this.debug) {
|
|
62
|
+
console.log(`[WebBridge] Sent message to iframe:`, message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get target origin (for this channel, it's the iframe origin)
|
|
67
|
+
*/
|
|
68
|
+
getTargetOrigin() {
|
|
69
|
+
return this.iframeOrigin;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get the iframe element
|
|
73
|
+
*/
|
|
74
|
+
getIframe() {
|
|
75
|
+
return this.iframe;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check if connected
|
|
79
|
+
*/
|
|
80
|
+
isConnected() {
|
|
81
|
+
return this.connected && !!this.iframe.contentWindow;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Set connected status
|
|
85
|
+
*/
|
|
86
|
+
setConnected(connected) {
|
|
87
|
+
this.connected = connected;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Destroy the channel
|
|
91
|
+
*/
|
|
92
|
+
destroy() {
|
|
93
|
+
super.destroy();
|
|
94
|
+
this.connected = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParentBridge for managing iframe communication from parent
|
|
3
|
+
*/
|
|
4
|
+
import { ParentBridgeConfig, BridgeMessage } from '@/types';
|
|
5
|
+
import { MessageHandler } from '@/shared/MessageHandler';
|
|
6
|
+
import { IframeChannel } from '@/parent/IframeChannel';
|
|
7
|
+
export declare class ParentBridge extends MessageHandler {
|
|
8
|
+
private channels;
|
|
9
|
+
constructor(config: ParentBridgeConfig);
|
|
10
|
+
/**
|
|
11
|
+
* Create a channel for an iframe
|
|
12
|
+
*/
|
|
13
|
+
createChannel(iframe: HTMLIFrameElement, options?: {
|
|
14
|
+
origin?: string;
|
|
15
|
+
}): IframeChannel;
|
|
16
|
+
/**
|
|
17
|
+
* Get channel for an iframe
|
|
18
|
+
*/
|
|
19
|
+
getChannel(iframe: HTMLIFrameElement): IframeChannel | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Remove channel for an iframe
|
|
22
|
+
*/
|
|
23
|
+
removeChannel(iframe: HTMLIFrameElement): void;
|
|
24
|
+
/**
|
|
25
|
+
* Send message (overrides parent method)
|
|
26
|
+
*/
|
|
27
|
+
protected sendMessage(targetOrigin: string, message: BridgeMessage): void;
|
|
28
|
+
/**
|
|
29
|
+
* Get target origin
|
|
30
|
+
*/
|
|
31
|
+
protected getTargetOrigin(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Check if any channel is connected
|
|
34
|
+
*/
|
|
35
|
+
isConnected(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Destroy the bridge and all channels
|
|
38
|
+
*/
|
|
39
|
+
destroy(): void;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=ParentBridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ParentBridge.d.ts","sourceRoot":"","sources":["../../src/parent/ParentBridge.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAGtD,qBAAa,YAAa,SAAQ,cAAc;IAC9C,OAAO,CAAC,QAAQ,CAAmD;gBAEvD,MAAM,EAAE,kBAAkB;IAqBtC;;OAEG;IACH,aAAa,CACX,MAAM,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5B,aAAa;IAuBhB;;OAEG;IACH,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,aAAa,GAAG,SAAS;IAIhE;;OAEG;IACH,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAY9C;;OAEG;IACH,SAAS,CAAC,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI;IAezE;;OAEG;IACH,SAAS,CAAC,eAAe,IAAI,MAAM;IAKnC;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,OAAO,IAAI,IAAI;CAWhB"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParentBridge for managing iframe communication from parent
|
|
3
|
+
*/
|
|
4
|
+
import { MessageHandler } from '@/shared/MessageHandler';
|
|
5
|
+
import { IframeChannel } from '@/parent/IframeChannel';
|
|
6
|
+
import { DEFAULT_TIMEOUT } from '@/constants';
|
|
7
|
+
export class ParentBridge extends MessageHandler {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
const timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
10
|
+
super(config.allowedOrigins, timeout, config.debug);
|
|
11
|
+
this.channels = new Map();
|
|
12
|
+
// Setup global message listener for parent
|
|
13
|
+
this.setupMessageListener((event) => {
|
|
14
|
+
// Only handle messages from iframes that are registered
|
|
15
|
+
const channel = Array.from(this.channels.values()).find(ch => ch.getIframe().contentWindow === event.source);
|
|
16
|
+
if (channel) {
|
|
17
|
+
this.handleMessage(event.data, event.origin);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
if (this.debug) {
|
|
21
|
+
console.log('[WebBridge] ParentBridge initialized');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a channel for an iframe
|
|
26
|
+
*/
|
|
27
|
+
createChannel(iframe, options) {
|
|
28
|
+
// If channel already exists, return it
|
|
29
|
+
if (this.channels.has(iframe)) {
|
|
30
|
+
return this.channels.get(iframe);
|
|
31
|
+
}
|
|
32
|
+
// Create new channel
|
|
33
|
+
const channel = new IframeChannel(iframe, this.originValidator.getAllowedOrigins(), this.timeout, this.debug);
|
|
34
|
+
this.channels.set(iframe, channel);
|
|
35
|
+
if (this.debug) {
|
|
36
|
+
console.log(`[WebBridge] Channel created for iframe`);
|
|
37
|
+
}
|
|
38
|
+
return channel;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get channel for an iframe
|
|
42
|
+
*/
|
|
43
|
+
getChannel(iframe) {
|
|
44
|
+
return this.channels.get(iframe);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Remove channel for an iframe
|
|
48
|
+
*/
|
|
49
|
+
removeChannel(iframe) {
|
|
50
|
+
const channel = this.channels.get(iframe);
|
|
51
|
+
if (channel) {
|
|
52
|
+
channel.destroy();
|
|
53
|
+
this.channels.delete(iframe);
|
|
54
|
+
if (this.debug) {
|
|
55
|
+
console.log('[WebBridge] Channel removed');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Send message (overrides parent method)
|
|
61
|
+
*/
|
|
62
|
+
sendMessage(targetOrigin, message) {
|
|
63
|
+
// Find the channel with matching origin
|
|
64
|
+
for (const channel of this.channels.values()) {
|
|
65
|
+
if (channel.getIframe().contentWindow) {
|
|
66
|
+
// Add version info
|
|
67
|
+
message._version = '1.0.0';
|
|
68
|
+
channel.getIframe().contentWindow.postMessage(message, targetOrigin);
|
|
69
|
+
if (this.debug) {
|
|
70
|
+
console.log('[WebBridge] Sent message from parent:', message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get target origin
|
|
77
|
+
*/
|
|
78
|
+
getTargetOrigin() {
|
|
79
|
+
// For parent bridge, return current window origin
|
|
80
|
+
return window.location.origin;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if any channel is connected
|
|
84
|
+
*/
|
|
85
|
+
isConnected() {
|
|
86
|
+
return Array.from(this.channels.values()).some(ch => ch.isConnected());
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Destroy the bridge and all channels
|
|
90
|
+
*/
|
|
91
|
+
destroy() {
|
|
92
|
+
for (const channel of this.channels.values()) {
|
|
93
|
+
channel.destroy();
|
|
94
|
+
}
|
|
95
|
+
this.channels.clear();
|
|
96
|
+
super.destroy();
|
|
97
|
+
if (this.debug) {
|
|
98
|
+
console.log('[WebBridge] ParentBridge destroyed');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ParentBridge } from '@/parent/ParentBridge';
|
|
2
|
+
export { IframeChannel } from '@/parent/IframeChannel';
|
|
3
|
+
export type { IframeChannelInterface } from '@/types';
|
|
4
|
+
export type { ParentBridgeConfig } from '@/types';
|
|
5
|
+
export * from '@/parent/utils';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parent/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,YAAY,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;AACrD,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AACjD,cAAc,gBAAgB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/parent/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAKjE;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CASjE"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for parent bridge
|
|
3
|
+
*/
|
|
4
|
+
export function validateIframe(iframe) {
|
|
5
|
+
if (!iframe)
|
|
6
|
+
return false;
|
|
7
|
+
if (!(iframe instanceof HTMLIFrameElement))
|
|
8
|
+
return false;
|
|
9
|
+
if (!iframe.contentWindow)
|
|
10
|
+
return false;
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
export function getIframeOrigin(iframe) {
|
|
14
|
+
try {
|
|
15
|
+
if (!iframe.src) {
|
|
16
|
+
return window.location.origin;
|
|
17
|
+
}
|
|
18
|
+
return new URL(iframe.src).origin;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return window.location.origin;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple EventEmitter for bridge events
|
|
3
|
+
*/
|
|
4
|
+
import { BridgeEventType } from '@/types';
|
|
5
|
+
export declare class EventEmitter {
|
|
6
|
+
private listeners;
|
|
7
|
+
on(type: BridgeEventType, callback: (data?: any) => void): void;
|
|
8
|
+
off(type: BridgeEventType, callback: (data?: any) => void): void;
|
|
9
|
+
emit(type: BridgeEventType, data?: any): void;
|
|
10
|
+
removeAllListeners(type?: BridgeEventType): void;
|
|
11
|
+
hasListener(type: BridgeEventType): boolean;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=EventEmitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventEmitter.d.ts","sourceRoot":"","sources":["../../src/shared/EventEmitter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAEzC,qBAAa,YAAY;IACvB,OAAO,CAAC,SAAS,CAA+D;IAEhF,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAO/D,GAAG,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAUhE,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI;IAa7C,kBAAkB,CAAC,IAAI,CAAC,EAAE,eAAe,GAAG,IAAI;IAQhD,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO;CAG5C"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple EventEmitter for bridge events
|
|
3
|
+
*/
|
|
4
|
+
export class EventEmitter {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.listeners = new Map();
|
|
7
|
+
}
|
|
8
|
+
on(type, callback) {
|
|
9
|
+
if (!this.listeners.has(type)) {
|
|
10
|
+
this.listeners.set(type, []);
|
|
11
|
+
}
|
|
12
|
+
this.listeners.get(type).push(callback);
|
|
13
|
+
}
|
|
14
|
+
off(type, callback) {
|
|
15
|
+
if (!this.listeners.has(type))
|
|
16
|
+
return;
|
|
17
|
+
const callbacks = this.listeners.get(type);
|
|
18
|
+
const index = callbacks.indexOf(callback);
|
|
19
|
+
if (index >= 0) {
|
|
20
|
+
callbacks.splice(index, 1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
emit(type, data) {
|
|
24
|
+
if (!this.listeners.has(type))
|
|
25
|
+
return;
|
|
26
|
+
const callbacks = this.listeners.get(type) || [];
|
|
27
|
+
callbacks.forEach(callback => {
|
|
28
|
+
try {
|
|
29
|
+
callback(data);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error(`EventEmitter error in ${type} listener:`, error);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
removeAllListeners(type) {
|
|
37
|
+
if (type) {
|
|
38
|
+
this.listeners.delete(type);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
this.listeners.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
hasListener(type) {
|
|
45
|
+
return this.listeners.has(type) && (this.listeners.get(type)?.length ?? 0) > 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IdGenerator.d.ts","sourceRoot":"","sources":["../../src/shared/IdGenerator.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAY;IAC3B,OAAO,CAAC,MAAM,CAAQ;gBAEV,MAAM,GAAE,MAAc;IAIlC,QAAQ,IAAI,MAAM;IAKlB,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ID generator for unique message IDs
|
|
3
|
+
*/
|
|
4
|
+
export class IdGenerator {
|
|
5
|
+
constructor(prefix = 'msg') {
|
|
6
|
+
this.counter = 0;
|
|
7
|
+
this.prefix = prefix;
|
|
8
|
+
}
|
|
9
|
+
generate() {
|
|
10
|
+
this.counter++;
|
|
11
|
+
return `${this.prefix}_${Date.now()}_${this.counter}_${Math.random().toString(36).substr(2, 9)}`;
|
|
12
|
+
}
|
|
13
|
+
reset() {
|
|
14
|
+
this.counter = 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for handling postMessage communication
|
|
3
|
+
*/
|
|
4
|
+
import { BridgeMessage, HandlerRegistry, PendingRequest } from '@/types';
|
|
5
|
+
import { EventEmitter } from '@/shared/EventEmitter';
|
|
6
|
+
import { OriginValidator } from '@/shared/OriginValidator';
|
|
7
|
+
import { IdGenerator } from '@/shared/IdGenerator';
|
|
8
|
+
export declare abstract class MessageHandler {
|
|
9
|
+
protected handlers: HandlerRegistry;
|
|
10
|
+
protected pendingRequests: Map<string, PendingRequest>;
|
|
11
|
+
protected eventEmitter: EventEmitter;
|
|
12
|
+
protected originValidator: OriginValidator;
|
|
13
|
+
protected idGenerator: IdGenerator;
|
|
14
|
+
protected timeout: number;
|
|
15
|
+
protected debug: boolean;
|
|
16
|
+
protected messageListener: ((event: MessageEvent) => void) | null;
|
|
17
|
+
constructor(allowedOrigins: string[], timeout?: number, debug?: boolean);
|
|
18
|
+
/**
|
|
19
|
+
* Register a handler
|
|
20
|
+
*/
|
|
21
|
+
registerHandler(handlerName: string, handler: (data?: any, callback?: Function) => void): void;
|
|
22
|
+
/**
|
|
23
|
+
* Unregister a handler
|
|
24
|
+
*/
|
|
25
|
+
unregisterHandler(handlerName: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Call a handler on the other side with timeout
|
|
28
|
+
*/
|
|
29
|
+
protected callHandlerWithTimeout(targetOrigin: string, handlerName: string, data?: any): Promise<any>;
|
|
30
|
+
/**
|
|
31
|
+
* Send a message to the other side (must be implemented by subclass)
|
|
32
|
+
*/
|
|
33
|
+
protected abstract sendMessage(targetOrigin: string, message: BridgeMessage): void;
|
|
34
|
+
/**
|
|
35
|
+
* Get the target origin for sending messages (must be implemented by subclass)
|
|
36
|
+
*/
|
|
37
|
+
protected abstract getTargetOrigin(): string;
|
|
38
|
+
/**
|
|
39
|
+
* Handle incoming message
|
|
40
|
+
*/
|
|
41
|
+
protected handleMessage(message: BridgeMessage, origin: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Handle incoming call message
|
|
44
|
+
*/
|
|
45
|
+
private handleCall;
|
|
46
|
+
/**
|
|
47
|
+
* Handle incoming response message
|
|
48
|
+
*/
|
|
49
|
+
private handleResponse;
|
|
50
|
+
/**
|
|
51
|
+
* Setup message listener
|
|
52
|
+
*/
|
|
53
|
+
protected setupMessageListener(handler: (event: MessageEvent) => void): void;
|
|
54
|
+
/**
|
|
55
|
+
* Remove message listener
|
|
56
|
+
*/
|
|
57
|
+
protected removeMessageListener(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Check if connected
|
|
60
|
+
*/
|
|
61
|
+
abstract isConnected(): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Add event listener
|
|
64
|
+
*/
|
|
65
|
+
on(type: 'ready' | 'disconnect' | 'error', callback: (data?: any) => void): void;
|
|
66
|
+
/**
|
|
67
|
+
* Remove event listener
|
|
68
|
+
*/
|
|
69
|
+
off(type: 'ready' | 'disconnect' | 'error', callback: (data?: any) => void): void;
|
|
70
|
+
/**
|
|
71
|
+
* Clean up
|
|
72
|
+
*/
|
|
73
|
+
destroy(): void;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=MessageHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MessageHandler.d.ts","sourceRoot":"","sources":["../../src/shared/MessageHandler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAIlD,8BAAsB,cAAc;IAClC,SAAS,CAAC,QAAQ,EAAE,eAAe,CAAY;IAC/C,SAAS,CAAC,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAY;IAClE,SAAS,CAAC,YAAY,EAAE,YAAY,CAAqB;IACzD,SAAS,CAAC,eAAe,EAAE,eAAe,CAAA;IAC1C,SAAS,CAAC,WAAW,EAAE,WAAW,CAAoB;IACtD,SAAS,CAAC,OAAO,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,KAAK,EAAE,OAAO,CAAA;IACxB,SAAS,CAAC,eAAe,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,IAAI,CAAO;gBAE5D,cAAc,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,MAAwB,EAAE,KAAK,GAAE,OAAe;IAM/F;;OAEG;IACH,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,QAAQ,KAAK,IAAI,GAAG,IAAI;IAO9F;;OAEG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAO5C;;OAEG;IACH,SAAS,CAAC,sBAAsB,CAC9B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,GAAG,GACT,OAAO,CAAC,GAAG,CAAC;IA2Bf;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI;IAElF;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,eAAe,IAAI,MAAM;IAE5C;;OAEG;IACH,SAAS,CAAC,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAgCrE;;OAEG;YACW,UAAU;IAyCxB;;OAEG;IACH,OAAO,CAAC,cAAc;IAqBtB;;OAEG;IACH,SAAS,CAAC,oBAAoB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI;IAK5E;;OAEG;IACH,SAAS,CAAC,qBAAqB,IAAI,IAAI;IAOvC;;OAEG;IACH,QAAQ,CAAC,WAAW,IAAI,OAAO;IAE/B;;OAEG;IACH,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIhF;;OAEG;IACH,GAAG,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIjF;;OAEG;IACH,OAAO,IAAI,IAAI;CAOhB"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for handling postMessage communication
|
|
3
|
+
*/
|
|
4
|
+
import { EventEmitter } from '@/shared/EventEmitter';
|
|
5
|
+
import { OriginValidator } from '@/shared/OriginValidator';
|
|
6
|
+
import { IdGenerator } from '@/shared/IdGenerator';
|
|
7
|
+
import { errorFactory } from '@/errors/errorFactory';
|
|
8
|
+
import { DEFAULT_TIMEOUT } from '@/constants';
|
|
9
|
+
export class MessageHandler {
|
|
10
|
+
constructor(allowedOrigins, timeout = DEFAULT_TIMEOUT, debug = false) {
|
|
11
|
+
this.handlers = new Map();
|
|
12
|
+
this.pendingRequests = new Map();
|
|
13
|
+
this.eventEmitter = new EventEmitter();
|
|
14
|
+
this.idGenerator = new IdGenerator();
|
|
15
|
+
this.messageListener = null;
|
|
16
|
+
this.originValidator = new OriginValidator(allowedOrigins, debug);
|
|
17
|
+
this.timeout = timeout;
|
|
18
|
+
this.debug = debug;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Register a handler
|
|
22
|
+
*/
|
|
23
|
+
registerHandler(handlerName, handler) {
|
|
24
|
+
this.handlers.set(handlerName, handler);
|
|
25
|
+
if (this.debug) {
|
|
26
|
+
console.log(`[WebBridge] Handler registered: ${handlerName}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Unregister a handler
|
|
31
|
+
*/
|
|
32
|
+
unregisterHandler(handlerName) {
|
|
33
|
+
this.handlers.delete(handlerName);
|
|
34
|
+
if (this.debug) {
|
|
35
|
+
console.log(`[WebBridge] Handler unregistered: ${handlerName}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Call a handler on the other side with timeout
|
|
40
|
+
*/
|
|
41
|
+
callHandlerWithTimeout(targetOrigin, handlerName, data) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const messageId = this.idGenerator.generate();
|
|
44
|
+
const timeoutHandle = setTimeout(() => {
|
|
45
|
+
this.pendingRequests.delete(messageId);
|
|
46
|
+
reject(errorFactory.timeout(this.timeout));
|
|
47
|
+
}, this.timeout);
|
|
48
|
+
this.pendingRequests.set(messageId, {
|
|
49
|
+
id: messageId,
|
|
50
|
+
timeout: timeoutHandle,
|
|
51
|
+
resolve,
|
|
52
|
+
reject
|
|
53
|
+
});
|
|
54
|
+
const message = {
|
|
55
|
+
type: 'call',
|
|
56
|
+
id: messageId,
|
|
57
|
+
handlerName,
|
|
58
|
+
data
|
|
59
|
+
};
|
|
60
|
+
this.sendMessage(targetOrigin, message);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Handle incoming message
|
|
65
|
+
*/
|
|
66
|
+
handleMessage(message, origin) {
|
|
67
|
+
// Validate origin
|
|
68
|
+
if (!this.originValidator.isOriginAllowed(origin)) {
|
|
69
|
+
if (this.debug) {
|
|
70
|
+
console.warn(`[WebBridge] Message from disallowed origin: ${origin}`);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
switch (message.type) {
|
|
76
|
+
case 'call':
|
|
77
|
+
this.handleCall(message, origin);
|
|
78
|
+
break;
|
|
79
|
+
case 'response':
|
|
80
|
+
this.handleResponse(message);
|
|
81
|
+
break;
|
|
82
|
+
case 'ready':
|
|
83
|
+
this.eventEmitter.emit('ready');
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
if (this.debug) {
|
|
87
|
+
console.warn(`[WebBridge] Unknown message type: ${message.type}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (this.debug) {
|
|
93
|
+
console.error('[WebBridge] Error handling message:', error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Handle incoming call message
|
|
99
|
+
*/
|
|
100
|
+
async handleCall(message, origin) {
|
|
101
|
+
const { id, handlerName, data } = message;
|
|
102
|
+
const handler = this.handlers.get(handlerName);
|
|
103
|
+
if (!handler) {
|
|
104
|
+
this.sendMessage(origin, {
|
|
105
|
+
type: 'response',
|
|
106
|
+
id,
|
|
107
|
+
error: `Handler "${handlerName}" not found`
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
// Execute handler
|
|
113
|
+
const result = await Promise.resolve(handler(data, (response) => {
|
|
114
|
+
this.sendMessage(origin, {
|
|
115
|
+
type: 'response',
|
|
116
|
+
id,
|
|
117
|
+
data: response
|
|
118
|
+
});
|
|
119
|
+
}));
|
|
120
|
+
// If handler returns a value directly (not using callback)
|
|
121
|
+
if (result !== undefined) {
|
|
122
|
+
this.sendMessage(origin, {
|
|
123
|
+
type: 'response',
|
|
124
|
+
id,
|
|
125
|
+
data: result
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
131
|
+
this.sendMessage(origin, {
|
|
132
|
+
type: 'response',
|
|
133
|
+
id,
|
|
134
|
+
error: errorMessage
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Handle incoming response message
|
|
140
|
+
*/
|
|
141
|
+
handleResponse(message) {
|
|
142
|
+
const { id, data, error } = message;
|
|
143
|
+
const pending = this.pendingRequests.get(id);
|
|
144
|
+
if (!pending) {
|
|
145
|
+
if (this.debug) {
|
|
146
|
+
console.warn(`[WebBridge] Received response for unknown request: ${id}`);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
clearTimeout(pending.timeout);
|
|
151
|
+
this.pendingRequests.delete(id);
|
|
152
|
+
if (error) {
|
|
153
|
+
pending.reject(new Error(error));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
pending.resolve(data);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Setup message listener
|
|
161
|
+
*/
|
|
162
|
+
setupMessageListener(handler) {
|
|
163
|
+
window.addEventListener('message', handler);
|
|
164
|
+
this.messageListener = handler;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Remove message listener
|
|
168
|
+
*/
|
|
169
|
+
removeMessageListener() {
|
|
170
|
+
if (this.messageListener) {
|
|
171
|
+
window.removeEventListener('message', this.messageListener);
|
|
172
|
+
this.messageListener = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Add event listener
|
|
177
|
+
*/
|
|
178
|
+
on(type, callback) {
|
|
179
|
+
this.eventEmitter.on(type, callback);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Remove event listener
|
|
183
|
+
*/
|
|
184
|
+
off(type, callback) {
|
|
185
|
+
this.eventEmitter.off(type, callback);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Clean up
|
|
189
|
+
*/
|
|
190
|
+
destroy() {
|
|
191
|
+
this.handlers.clear();
|
|
192
|
+
this.pendingRequests.forEach(({ timeout }) => clearTimeout(timeout));
|
|
193
|
+
this.pendingRequests.clear();
|
|
194
|
+
this.eventEmitter.removeAllListeners();
|
|
195
|
+
this.removeMessageListener();
|
|
196
|
+
}
|
|
197
|
+
}
|