@pulse-editor/shared-utils 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/package.json +13 -0
- package/src/inter-module-communication.ts +137 -0
- package/src/main.ts +5 -0
- package/src/message-receiver.ts +110 -0
- package/src/message-sender.ts +100 -0
- package/tsconfig.json +28 -0
package/package.json
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
messageTimeout,
|
|
3
|
+
IMCMessage,
|
|
4
|
+
IMCMessageTypeEnum,
|
|
5
|
+
ReceiverHandlerMap,
|
|
6
|
+
} from "@pulse-editor/types";
|
|
7
|
+
import { MessageReceiver } from "./message-receiver";
|
|
8
|
+
import { MessageSender } from "./message-sender";
|
|
9
|
+
|
|
10
|
+
export class InterModuleCommunication {
|
|
11
|
+
private thisWindow: Window | undefined;
|
|
12
|
+
private otherWindow: Window | undefined;
|
|
13
|
+
|
|
14
|
+
/* Wait current module to finish tasks to return a response or acknowledgement. */
|
|
15
|
+
private thisPendingTasks:
|
|
16
|
+
| Map<
|
|
17
|
+
string,
|
|
18
|
+
{
|
|
19
|
+
controller: AbortController;
|
|
20
|
+
}
|
|
21
|
+
>
|
|
22
|
+
| undefined;
|
|
23
|
+
|
|
24
|
+
/* Wait the other module to return a response or acknowledgement. */
|
|
25
|
+
private otherPendingMessages:
|
|
26
|
+
| Map<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
resolve: (result: any) => void;
|
|
30
|
+
reject: () => void;
|
|
31
|
+
}
|
|
32
|
+
>
|
|
33
|
+
| undefined;
|
|
34
|
+
|
|
35
|
+
private receiver: MessageReceiver | undefined;
|
|
36
|
+
private sender: MessageSender | undefined;
|
|
37
|
+
|
|
38
|
+
private moduleName: string;
|
|
39
|
+
|
|
40
|
+
private receiverHandlerMap: ReceiverHandlerMap | undefined;
|
|
41
|
+
|
|
42
|
+
private listener: ((event: MessageEvent) => void) | undefined;
|
|
43
|
+
|
|
44
|
+
constructor(moduleName: string) {
|
|
45
|
+
this.moduleName = moduleName;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Initialize a receiver to receive message. */
|
|
49
|
+
public initThisWindow(window: Window) {
|
|
50
|
+
this.thisWindow = window;
|
|
51
|
+
this.receiverHandlerMap = new Map();
|
|
52
|
+
this.thisPendingTasks = new Map();
|
|
53
|
+
|
|
54
|
+
const receiver = new MessageReceiver(
|
|
55
|
+
this.receiverHandlerMap,
|
|
56
|
+
this.thisPendingTasks,
|
|
57
|
+
this.moduleName
|
|
58
|
+
);
|
|
59
|
+
this.receiver = receiver;
|
|
60
|
+
|
|
61
|
+
this.listener = (event: MessageEvent<IMCMessage>) => {
|
|
62
|
+
if (!receiver) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Receiver not initialized at module " + this.moduleName
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const message = event.data;
|
|
69
|
+
const win = event.source as Window;
|
|
70
|
+
receiver.receiveMessage(win, message);
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener("message", this.listener);
|
|
73
|
+
console.log("Adding IMC listener in " + this.moduleName);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Initialize a sender to send message ot the other window. */
|
|
77
|
+
public initOtherWindow(window: Window) {
|
|
78
|
+
this.otherWindow = window;
|
|
79
|
+
this.otherPendingMessages = new Map();
|
|
80
|
+
|
|
81
|
+
const sender = new MessageSender(
|
|
82
|
+
window,
|
|
83
|
+
messageTimeout,
|
|
84
|
+
this.otherPendingMessages,
|
|
85
|
+
this.moduleName
|
|
86
|
+
);
|
|
87
|
+
this.sender = sender;
|
|
88
|
+
|
|
89
|
+
if (!this.receiverHandlerMap) {
|
|
90
|
+
throw new Error("You must initialize the current window first.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add an acknowledgement handler in current window's receiver for results of sent messages.
|
|
94
|
+
// The current window must be initialized first. i.e. call initThisWindow() before initOtherWindow().
|
|
95
|
+
this.receiverHandlerMap.set(
|
|
96
|
+
IMCMessageTypeEnum.Acknowledge,
|
|
97
|
+
async (senderWindow: Window, message: IMCMessage) => {
|
|
98
|
+
const pendingMessage = this.otherPendingMessages?.get(message.id);
|
|
99
|
+
if (pendingMessage) {
|
|
100
|
+
pendingMessage.resolve(message.payload);
|
|
101
|
+
this.otherPendingMessages?.delete(message.id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public close() {
|
|
108
|
+
if (this.listener) {
|
|
109
|
+
window.removeEventListener("message", this.listener);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public async sendMessage(
|
|
114
|
+
type: IMCMessageTypeEnum,
|
|
115
|
+
payload?: any,
|
|
116
|
+
abortSignal?: AbortSignal
|
|
117
|
+
): Promise<any> {
|
|
118
|
+
if (!this.sender) {
|
|
119
|
+
throw new Error("Sender not initialized");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return await this.sender.sendMessage(type, payload, abortSignal);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public updateReceiverHandlerMap(
|
|
126
|
+
receiverHandlerMap: ReceiverHandlerMap
|
|
127
|
+
): void {
|
|
128
|
+
if (!this.receiver) {
|
|
129
|
+
throw new Error("Receiver not initialized");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.receiverHandlerMap?.clear();
|
|
133
|
+
receiverHandlerMap.forEach((value, key) => {
|
|
134
|
+
this.receiverHandlerMap?.set(key, value);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMCMessage,
|
|
3
|
+
IMCMessageTypeEnum,
|
|
4
|
+
ReceiverHandlerMap,
|
|
5
|
+
} from "@pulse-editor/types";
|
|
6
|
+
|
|
7
|
+
export class MessageReceiver {
|
|
8
|
+
private handlerMap: ReceiverHandlerMap;
|
|
9
|
+
private pendingTasks: Map<
|
|
10
|
+
string,
|
|
11
|
+
{
|
|
12
|
+
controller: AbortController;
|
|
13
|
+
}
|
|
14
|
+
>;
|
|
15
|
+
private moduleName: string;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
listenerMap: ReceiverHandlerMap,
|
|
19
|
+
pendingTasks: Map<
|
|
20
|
+
string,
|
|
21
|
+
{
|
|
22
|
+
controller: AbortController;
|
|
23
|
+
}
|
|
24
|
+
>,
|
|
25
|
+
moduleInfo: string
|
|
26
|
+
) {
|
|
27
|
+
this.handlerMap = listenerMap;
|
|
28
|
+
this.pendingTasks = pendingTasks;
|
|
29
|
+
this.moduleName = moduleInfo;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public receiveMessage(senderWindow: Window, message: IMCMessage) {
|
|
33
|
+
// Not handling messages from self
|
|
34
|
+
if (this.moduleName === message.from) return;
|
|
35
|
+
|
|
36
|
+
// Log the message in dev mode
|
|
37
|
+
if (process.env.NODE_ENV === "development") {
|
|
38
|
+
console.log(
|
|
39
|
+
`Module ${this.moduleName} received message from module ${message.from}:\n ${JSON.stringify(
|
|
40
|
+
message
|
|
41
|
+
)}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Abort the task if the message type is Abort
|
|
46
|
+
if (message.type === IMCMessageTypeEnum.Abort) {
|
|
47
|
+
const id = message.id;
|
|
48
|
+
const pendingTask = this.pendingTasks.get(id);
|
|
49
|
+
|
|
50
|
+
if (pendingTask) {
|
|
51
|
+
console.log("Aborting task", id);
|
|
52
|
+
pendingTask.controller.abort();
|
|
53
|
+
this.pendingTasks.delete(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handler = this.handlerMap.get(message.type);
|
|
60
|
+
if (handler) {
|
|
61
|
+
// Create abort controller to listen for abort signal from sender.
|
|
62
|
+
// Then save the message id and abort controller to the pending tasks.
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const signal = controller.signal;
|
|
65
|
+
this.pendingTasks.set(message.id, {
|
|
66
|
+
controller,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const promise = handler(senderWindow, message, signal);
|
|
70
|
+
promise
|
|
71
|
+
.then((result) => {
|
|
72
|
+
// Don't send the result if the task has been aborted
|
|
73
|
+
if (signal.aborted) return;
|
|
74
|
+
|
|
75
|
+
// Acknowledge the sender with the result if the message type is not Acknowledge
|
|
76
|
+
if (message.type !== IMCMessageTypeEnum.Acknowledge) {
|
|
77
|
+
this.acknowledgeSender(senderWindow, message.id, result);
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.catch((error) => {
|
|
81
|
+
// Send the error message to the sender
|
|
82
|
+
const errMsg: IMCMessage = {
|
|
83
|
+
id: message.id,
|
|
84
|
+
type: IMCMessageTypeEnum.Error,
|
|
85
|
+
payload: error.message,
|
|
86
|
+
from: this.moduleName,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
senderWindow.postMessage(errMsg, "*");
|
|
90
|
+
})
|
|
91
|
+
.finally(() => {
|
|
92
|
+
this.pendingTasks.delete(message.id);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private acknowledgeSender(
|
|
98
|
+
senderWindow: Window,
|
|
99
|
+
id: string,
|
|
100
|
+
payload: any
|
|
101
|
+
): void {
|
|
102
|
+
const message: IMCMessage = {
|
|
103
|
+
id,
|
|
104
|
+
type: IMCMessageTypeEnum.Acknowledge,
|
|
105
|
+
payload: payload,
|
|
106
|
+
from: this.moduleName,
|
|
107
|
+
};
|
|
108
|
+
senderWindow.postMessage(message, "*");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { IMCMessage, IMCMessageTypeEnum } from "@pulse-editor/types";
|
|
2
|
+
|
|
3
|
+
export class MessageSender {
|
|
4
|
+
private targetWindow: Window;
|
|
5
|
+
private timeout: number;
|
|
6
|
+
|
|
7
|
+
private pendingMessages: Map<
|
|
8
|
+
string,
|
|
9
|
+
{ resolve: (result: any) => void; reject: () => void }
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
private moduleName: string;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
targetWindow: Window,
|
|
16
|
+
timeout: number,
|
|
17
|
+
pendingMessages: Map<
|
|
18
|
+
string,
|
|
19
|
+
{ resolve: (result: any) => void; reject: () => void }
|
|
20
|
+
>,
|
|
21
|
+
moduleInfo: string
|
|
22
|
+
) {
|
|
23
|
+
this.targetWindow = targetWindow;
|
|
24
|
+
this.timeout = timeout;
|
|
25
|
+
|
|
26
|
+
this.pendingMessages = pendingMessages;
|
|
27
|
+
this.moduleName = moduleInfo;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async sendMessage(
|
|
31
|
+
handlingType: IMCMessageTypeEnum,
|
|
32
|
+
payload?: any,
|
|
33
|
+
abortSignal?: AbortSignal
|
|
34
|
+
): Promise<any> {
|
|
35
|
+
// Generate a unique id for the message using timestamp
|
|
36
|
+
const id = new Date().getTime().toString();
|
|
37
|
+
const message: IMCMessage = {
|
|
38
|
+
id,
|
|
39
|
+
type: handlingType,
|
|
40
|
+
payload: payload,
|
|
41
|
+
from: this.moduleName,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
// If the signal is already aborted, reject immediately
|
|
46
|
+
if (abortSignal?.aborted) {
|
|
47
|
+
return reject(new Error("Request aborted"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const abortHandler = () => {
|
|
51
|
+
this.pendingMessages.delete(id);
|
|
52
|
+
// Notify the target window that the request has been aborted
|
|
53
|
+
this.targetWindow.postMessage(
|
|
54
|
+
{
|
|
55
|
+
id,
|
|
56
|
+
type: IMCMessageTypeEnum.Abort,
|
|
57
|
+
payload: JSON.stringify({
|
|
58
|
+
status: "Task aborted",
|
|
59
|
+
data: null,
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
"*"
|
|
63
|
+
);
|
|
64
|
+
reject(new Error("Request aborted"));
|
|
65
|
+
};
|
|
66
|
+
// Attach abort listener
|
|
67
|
+
abortSignal?.addEventListener("abort", abortHandler);
|
|
68
|
+
|
|
69
|
+
// Send message
|
|
70
|
+
this.pendingMessages.set(id, {
|
|
71
|
+
resolve,
|
|
72
|
+
reject,
|
|
73
|
+
});
|
|
74
|
+
this.targetWindow.postMessage(message, "*");
|
|
75
|
+
|
|
76
|
+
// Check timeout
|
|
77
|
+
const timeoutId = setTimeout(() => {
|
|
78
|
+
this.pendingMessages.delete(id);
|
|
79
|
+
abortSignal?.removeEventListener("abort", abortHandler);
|
|
80
|
+
reject(new Error("Communication with Pulse Editor timeout."));
|
|
81
|
+
}, this.timeout);
|
|
82
|
+
|
|
83
|
+
// Ensure cleanup on resolution
|
|
84
|
+
const currentMessage = this.pendingMessages.get(id);
|
|
85
|
+
if (currentMessage) {
|
|
86
|
+
currentMessage.resolve = (result) => {
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
abortSignal?.removeEventListener("abort", abortHandler);
|
|
89
|
+
resolve(result);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
currentMessage.reject = () => {
|
|
93
|
+
clearTimeout(timeoutId);
|
|
94
|
+
abortSignal?.removeEventListener("abort", abortHandler);
|
|
95
|
+
reject();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"types": [
|
|
10
|
+
"react",
|
|
11
|
+
"node"
|
|
12
|
+
],
|
|
13
|
+
"baseUrl": ".",
|
|
14
|
+
"paths": {
|
|
15
|
+
"*": [
|
|
16
|
+
"node_modules/*",
|
|
17
|
+
"dist/*"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"src/**/*"
|
|
23
|
+
],
|
|
24
|
+
"exclude": [
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist",
|
|
27
|
+
]
|
|
28
|
+
}
|