Skip to content

Commit d2e6701

Browse files
committed
refactor: improve typing of Messages util
1 parent c194ddb commit d2e6701

File tree

2 files changed

+66
-29
lines changed

2 files changed

+66
-29
lines changed

src/commons/application/Application.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { useDispatch } from 'react-redux';
33
import { Outlet } from 'react-router-dom';
4-
import Messages, { MessageType, sendToWebview } from 'src/features/vscode/messages';
4+
import Messages, { MessageType, MessageTypeNames, sendToWebview } from 'src/features/vscode/messages';
55

66
import NavigationBar from '../navigationBar/NavigationBar';
77
import Constants from '../utils/Constants';
@@ -82,7 +82,7 @@ const Application: React.FC = () => {
8282
return true;
8383
};
8484

85-
const message = Messages.WebviewStarted();
85+
const message = Messages.ExtensionPing();
8686
sendToWebview(message);
8787

8888
window.addEventListener('message', event => {
@@ -93,11 +93,11 @@ const Application: React.FC = () => {
9393
}
9494
// console.log(`FRONTEND: Message from ${event.origin}: ${JSON.stringify(message)}`);
9595
switch (message.type) {
96-
case 'WebviewStarted':
96+
case MessageTypeNames.ExtensionPong:
9797
console.log('Received WebviewStarted message, will set vsc');
9898
dispatch(VscodeActions.setVscode());
9999
break;
100-
case 'Text':
100+
case MessageTypeNames.Text:
101101
const code = message.code;
102102
console.log(`FRONTEND: TextMessage: ${code}`);
103103
// TODO: Don't change ace editor directly

src/features/vscode/messages.ts

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,95 @@
1-
// This file is copied from https://github.com/source-academy/sa-vscode/blob/main/src/utils/messages.ts
1+
// This file is originally created in https://github.com/source-academy/sa-vscode/blob/main/src/utils/messages.ts
2+
// It also needs to be copied to source-academy/frontend:src/features/vscode/messages.ts
3+
// Ideally it is split into multiple files, but for ease of copying, it is kept as one file.
4+
5+
// ================================================================================
6+
// Message type definitions
7+
// ================================================================================
8+
const Messages = createMessages({
9+
/** Sent from the iframe to the extension */
10+
ExtensionPing: () => ({}),
11+
/** Sent from the extension to the iframe */
12+
ExtensionPong: (token: string | null) => ({ token }),
13+
IsVsc: () => ({}),
14+
NewEditor: (assessmentName: string, questionId: number, code: string) => ({
15+
assessmentName,
16+
questionId,
17+
code,
18+
}),
19+
Text: (code: string) => ({ code }),
20+
});
21+
22+
export default Messages;
23+
24+
// ================================================================================
25+
// Code for type generation
26+
// ================================================================================
27+
28+
// Define BaseMessage to be the base type for all messages, such that all messages have a type field
229
type BaseMessage<T extends string, P extends object> = {
330
type: T;
431
} & P;
532

33+
// A helper function to create messages dynamically from schema (hoisted!)
634
function createMessages<T extends Record<string, (...args: any[]) => object>>(
7-
creators: T
35+
creators: T,
836
): {
9-
[K in Extract<keyof T, string>]: (...args: Parameters<T[K]>) => BaseMessage<K, ReturnType<T[K]>>;
37+
[K in Extract<keyof T, string>]: (
38+
...args: Parameters<T[K]>
39+
) => BaseMessage<K, ReturnType<T[K]>>;
1040
} {
1141
return Object.fromEntries(
1242
Object.entries(creators).map(([key, creator]) => [
1343
key,
1444
(...args: any[]) => ({
1545
type: key,
16-
...creator(...args)
17-
})
18-
])
46+
...creator(...args),
47+
}),
48+
]),
1949
) as any;
2050
}
2151

22-
const Messages = createMessages({
23-
WebviewStarted: () => ({}),
24-
IsVsc: () => ({}),
25-
NewEditor: (assessmentName: string, questionId: number, code: string) => ({
26-
assessmentName,
27-
questionId,
28-
code
29-
}),
30-
Text: (code: string) => ({ code })
31-
});
32-
33-
export default Messages;
34-
35-
// Define MessageTypes to map each key in Messages to its specific message type
36-
export type MessageTypes = {
52+
// Define MessageTypes as a map of each key in Messages to its specific message type
53+
type MessageTypes = {
3754
[K in keyof typeof Messages]: ReturnType<(typeof Messages)[K]>;
3855
};
3956

4057
// Define MessageType as a union of all message types
4158
export type MessageType = MessageTypes[keyof MessageTypes];
4259

43-
export const FRONTEND_ELEMENT_ID = 'frontend';
60+
// Also define MessageTypeNames as an "enum" to avoid hardcoding strings
61+
export const MessageTypeNames = (() =>
62+
({
63+
...Object.keys(Messages)
64+
.filter((k) => isNaN(Number(k)))
65+
.reduce(
66+
(acc, cur) => ({
67+
...acc,
68+
[cur]: cur,
69+
}),
70+
{},
71+
),
72+
}) as {
73+
[k in keyof typeof Messages]: k;
74+
})();
75+
76+
// ================================================================================
77+
// Wrapper functions
78+
// ================================================================================
79+
80+
export const FRONTEND_ELEMENT_ID = "frontend";
4481

4582
export function sendToWebview(message: MessageType) {
46-
window.parent.postMessage(message, '*');
83+
window.parent.postMessage(message, "*");
4784
}
4885
export function sendToFrontend(document: Document, message: MessageType) {
4986
const iframe: HTMLIFrameElement = document.getElementById(
50-
FRONTEND_ELEMENT_ID
87+
FRONTEND_ELEMENT_ID,
5188
) as HTMLIFrameElement;
5289
const contentWindow = iframe.contentWindow;
5390
if (!contentWindow) {
5491
return;
5592
}
5693
// TODO: Don't hardcode this!
57-
contentWindow.postMessage(message, 'http://localhost:8000');
94+
contentWindow.postMessage(message, "http://localhost:8000");
5895
}

0 commit comments

Comments
 (0)