Skip to content

Commit b2dfc49

Browse files
committed
chore(message-bus): improve docs and fix lint issues
Signed-off-by: Saulo Vallory <me@saulo.engineer>
1 parent d08cd09 commit b2dfc49

File tree

8 files changed

+286
-65
lines changed

8 files changed

+286
-65
lines changed

packages/message-bus/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
# browser-package
1+
# Figma Message Bus
22

3-
A browser package with ECMAScript support, written in TypeScript `.ts` files.
3+
A TypeScript package providing a robust, type-safe message bus for communication between the Figma plugin main thread and its UI context. It simplifies event handling and command execution across contexts, including support for standard Figma API events and custom events/commands.
4+
5+
Includes utilities for serializing/deserializing complex data types (like `Map`) across the communication channel.
6+
7+
Built with TypeScript for strong typing and ECMAScript support.

packages/message-bus/src/MessageBus.ts

Lines changed: 120 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { CommandRegistry } from './commands';
22
import {
3+
type ArgFreeEventType,
34
type EventRegistry,
5+
FigmaEvent,
46
type FigmaEventDefinition,
57
type FigmaEventRegistry,
68
isFigmaEvent,
@@ -10,7 +12,8 @@ import type { CommandHandlers, DeregisterFn, EventListeners } from './types';
1012
import { JsonReviver, serializeForMessageBus } from './utils';
1113

1214
/**
13-
* A simple message bus implementation which magically works in both the main thread and the plugin UI.
15+
* A simple message bus implementation facilitating communication between
16+
* the main thread and the plugin UI.
1417
* No need to worry about sending messages in the right direction.
1518
*
1619
* @remarks
@@ -24,13 +27,22 @@ import { JsonReviver, serializeForMessageBus } from './utils';
2427
export class MessageBusSingleton<TCommands = unknown, TEvents = unknown> {
2528
private static instance?: MessageBusSingleton<unknown, unknown>;
2629

30+
/** Internal storage for registered command handlers. */
2731
protected $handlers: Partial<CommandHandlers<TCommands>> = {};
2832

33+
/** Internal storage for registered event listeners. */
2934
protected $listeners: Partial<EventListeners<TEvents>> = {};
3035

36+
// Prevent external instantiation
3137
// eslint-disable-next-line @typescript-eslint/no-empty-function
3238
private constructor() {}
3339

40+
/**
41+
* Retrieves the singleton instance of the MessageBus.
42+
* @template T - The type definition for commands the bus will handle.
43+
* @template E - The type definition for events the bus will handle.
44+
* @returns The singleton instance of MessageBusSingleton.
45+
*/
3446
public static getInstance<T = unknown, E = unknown>(): MessageBusSingleton<
3547
T,
3648
E
@@ -41,6 +53,13 @@ export class MessageBusSingleton<TCommands = unknown, TEvents = unknown> {
4153
return MessageBusSingleton.instance as MessageBusSingleton<T, E>;
4254
}
4355

56+
/**
57+
* Registers a handler function for a specific command.
58+
* @template Id - The ID of the command to handle.
59+
* @param command The command ID.
60+
* @param handler The function to execute when the command is received.
61+
* @returns A function to deregister the handler.
62+
*/
4463
public handleCommand<Id extends keyof CommandHandlers<TCommands>>(
4564
command: Id,
4665
handler: CommandHandlers<TCommands>[Id],
@@ -57,6 +76,13 @@ export class MessageBusSingleton<TCommands = unknown, TEvents = unknown> {
5776
});
5877
}
5978

79+
/**
80+
* Sends a command to the appropriate context (main thread or UI).
81+
* @template Id - The ID of the command to send.
82+
* @param command The command ID.
83+
* @param data The payload for the command.
84+
* @returns The result of the command execution (currently always returns `undefined` as execution is asynchronous).
85+
*/
6086
public sendCommand<Id extends keyof CommandRegistry<TCommands>>(
6187
command: Id,
6288
data: CommandRegistry<TCommands>[Id]['message'],
@@ -68,77 +94,143 @@ export class MessageBusSingleton<TCommands = unknown, TEvents = unknown> {
6894
return undefined;
6995
}
7096

97+
/**
98+
* Registers a listener function for a specific event.
99+
* This handles both custom events and standard Figma events.
100+
* @template Id - The ID of the event to listen to.
101+
* @param event The event ID.
102+
* @param listener The function to execute when the event is published.
103+
* @returns A function to deregister the listener.
104+
*/
71105
public listenToEvent<Id extends keyof EventListeners<TEvents>>(
72106
event: Id,
73107
listener: EventListeners<TEvents>[Id],
74108
): DeregisterFn {
75109
this.$listeners[event] = listener as Partial<EventListeners<TEvents>>[Id];
76110

77-
if (isFigmaEvent(event as string)) {
78-
// Only attempt to use Figma API if the global `figma` object exists
111+
const eventString = String(event);
112+
113+
if (isFigmaEvent(eventString)) {
79114
if (typeof figma !== 'undefined') {
80-
figma.on(
81-
event as ArgFreeEventType,
82-
listener as (...args: unknown[]) => void,
83-
);
115+
const typedListener = listener as (...args: any[]) => any;
116+
117+
// Use the string value for the switch and figma.on/off calls
118+
switch (eventString) {
119+
case FigmaEvent.SelectionChanged:
120+
case FigmaEvent.CurrentPageChanged:
121+
case FigmaEvent.OnClose:
122+
case FigmaEvent.TimerStarted:
123+
case FigmaEvent.TimerPaused:
124+
case FigmaEvent.TimerStopped:
125+
case FigmaEvent.TimerDone:
126+
case FigmaEvent.TimerResume:
127+
case FigmaEvent.TimerAdjust:
128+
figma.on(
129+
eventString as ArgFreeEventType,
130+
typedListener as () => void,
131+
);
132+
break;
133+
case FigmaEvent.DocumentChanged:
134+
figma.on(
135+
'documentchange',
136+
typedListener as (evt: DocumentChangeEvent) => void,
137+
);
138+
break;
139+
case FigmaEvent.OnDrop:
140+
figma.on('drop', typedListener as (evt: DropEvent) => boolean);
141+
break;
142+
case FigmaEvent.OnRun:
143+
figma.on('run', typedListener as (evt: RunEvent) => void);
144+
break;
145+
}
146+
84147
return (): void => {
85-
// Also check before using figma.off
86148
if (typeof figma !== 'undefined') {
87-
figma.off(
88-
event as ArgFreeEventType,
89-
listener as (...args: unknown[]) => void,
90-
);
149+
switch (eventString) {
150+
case FigmaEvent.SelectionChanged:
151+
case FigmaEvent.CurrentPageChanged:
152+
case FigmaEvent.OnClose:
153+
case FigmaEvent.TimerStarted:
154+
case FigmaEvent.TimerPaused:
155+
case FigmaEvent.TimerStopped:
156+
case FigmaEvent.TimerDone:
157+
case FigmaEvent.TimerResume:
158+
case FigmaEvent.TimerAdjust:
159+
figma.off(
160+
eventString as ArgFreeEventType,
161+
typedListener as () => void,
162+
);
163+
break;
164+
case FigmaEvent.DocumentChanged:
165+
figma.off(
166+
'documentchange',
167+
typedListener as (evt: DocumentChangeEvent) => void,
168+
);
169+
break;
170+
case FigmaEvent.OnDrop:
171+
figma.off('drop', typedListener as (evt: DropEvent) => boolean);
172+
break;
173+
case FigmaEvent.OnRun:
174+
figma.off('run', typedListener as (evt: RunEvent) => void);
175+
break;
176+
}
91177
}
92178
};
93179
}
94180

95-
// If figma is not defined, warn and return a no-op deregister function
96-
// This code is only reached if `typeof figma === 'undefined'`
181+
// Handle case where Figma API is not available but a Figma event is listened to
97182
console.warn(
98-
`Attempted to listen to Figma event '${String(
99-
event,
100-
)}' in a non-Figma environment.`,
183+
`Attempted to listen to Figma event '${eventString}' in a non-Figma environment.`,
101184
);
102-
return () => {}; // Return a no-op function
185+
// Return a no-op deregister function
186+
return () => {};
103187
}
104188

105-
return evtHandler.on(String(event), (data: unknown) => {
106-
// Parse received data through JSON to reconstruct Map objects
189+
// Handle custom events using the internal event handler system
190+
return evtHandler.on(eventString, (data: unknown) => {
107191
const parsedData =
108192
typeof data === 'string' ? JSON.parse(data, JsonReviver) : data;
109-
110193
listener(parsedData as EventRegistry<TEvents>[Id]['message']);
111194
});
112195
}
113196

197+
/**
198+
* Publishes an event to the appropriate context (main thread or UI).
199+
* This handles both custom events and standard Figma events.
200+
* @template Id - The ID of the event to publish.
201+
* @param event The event ID.
202+
* @param data The payload for the event.
203+
*/
114204
public publishEvent<
115205
Id extends keyof EventRegistry<TEvents> | keyof FigmaEventRegistry,
116206
>(
117207
event: Id,
118208
data: Id extends keyof EventRegistry<TEvents>
119209
? EventRegistry<TEvents>[Id]['message']
120210
: Id extends keyof FigmaEventRegistry
121-
? FigmaEventRegistry[Id] extends FigmaEventDefinition<infer T, infer U>
211+
? FigmaEventRegistry[Id] extends FigmaEventDefinition<infer _T, infer U>
122212
? U
123213
: never
124214
: never,
125215
): void {
126-
// Serialize data to handle Maps and complex objects
127216
const serializedData = serializeForMessageBus(data);
128-
129217
evtHandler.emit(String(event), serializedData);
130218
}
131219
}
132220

221+
/** The singleton instance of the MessageBus. */
133222
const singleton = MessageBusSingleton.getInstance();
134223

135224
// ensure the API is never changed
136-
// -------------------------------
137225
Object.freeze(singleton);
138226

139-
// export the singleton instance only
140-
// -----------------------------
141-
227+
/**
228+
* Retrieves the singleton instance of the MessageBus, typed for specific command and event registries.
229+
* This is the preferred way to access the MessageBus.
230+
* @template TCmdRegistry - The type definition for the command registry.
231+
* @template TEvtRegistry - The type definition for the event registry.
232+
* @returns The singleton `MessageBusSingleton` instance, properly typed.
233+
*/
142234
export function getMessageBus<
143235
TCmdRegistry = unknown,
144236
TEvtRegistry = unknown,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import '@figma/plugin-typings';
22

3+
/**
4+
* Extends the global scope with custom properties.
5+
*/
36
declare global {
7+
/**
8+
* Represents the global object, extended with a `TESTING` flag.
9+
*/
410
var global: typeof globalThis & {
11+
/** Optional flag to indicate if the environment is running in test mode. */
512
TESTING: boolean | undefined;
613
};
714
}

packages/message-bus/src/commands.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
1+
/**
2+
* Defines the structure for a command within the message bus system.
3+
* @template Id - A unique string identifier for the command.
4+
* @template Message - The type of the message payload associated with the command (defaults to undefined).
5+
* @template Result - The type of the result expected after executing the command (defaults to void).
6+
*/
17
export interface CommandDefinition<
28
Id extends string,
39
Message = undefined,
410
Result = void,
511
> {
12+
/** The unique identifier for the command. */
613
$id: Id;
14+
/** Indicates the type of this definition (always 'command'). */
715
$type: 'command';
16+
/** The type of the message payload for this command. */
817
message: Message;
18+
/** The type of the result returned by this command's handler. */
919
result: Result;
1020
}
1121

22+
/**
23+
* Represents a registry mapping command IDs to their definitions.
24+
* @template TCommandMessageMap - An object type where keys are command IDs (strings)
25+
* and values are the corresponding message payloads.
26+
*/
1227
export type CommandRegistry<TCommandMessageMap> = {
28+
// Maps each key K from TCommandMessageMap to a CommandDefinition.
29+
// Ensures that the key K is a string.
1330
[K in keyof TCommandMessageMap]: K extends string
1431
? CommandDefinition<K, TCommandMessageMap[K]>
1532
: never;

0 commit comments

Comments
 (0)