Skip to content

Commit 356c7fc

Browse files
authored
Merge pull request #6 from sass/new-apis
Add more generally useful functionality
2 parents 13e5d5c + ecc296a commit 356c7fc

File tree

4 files changed

+258
-19
lines changed

4 files changed

+258
-19
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## 1.1.0
2+
3+
* Add `SyncMessagePort.receiveMessageIfAvailable()`.
4+
5+
* Add `timeout` and `timeoutValue` options to
6+
`SyncMessagePort.receiveMessage()`.
7+
8+
* Add a `closedValue` option to `SyncMessagePort.receiveMessage()`.
9+
10+
## 1.0.0
11+
12+
* Initial release

lib/index.test.ts

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as fs from 'fs';
66
import * as p from 'path';
77
import {MessagePort, Worker} from 'worker_threads';
88

9-
import {SyncMessagePort} from './index';
9+
import {SyncMessagePort, TimeoutException} from './index';
1010

1111
describe('SyncMessagePort', () => {
1212
describe('sends a message', () => {
@@ -73,6 +73,124 @@ describe('SyncMessagePort', () => {
7373
});
7474
});
7575

76+
describe('receiveMessageIfAvailable()', () => {
77+
it('without a queued message', () => {
78+
const channel = SyncMessagePort.createChannel();
79+
const port = new SyncMessagePort(channel.port1);
80+
expect(port.receiveMessageIfAvailable()).toBe(undefined);
81+
port.close();
82+
});
83+
84+
it('with a queued message', () => {
85+
const channel = SyncMessagePort.createChannel();
86+
const port1 = new SyncMessagePort(channel.port1);
87+
const port2 = new SyncMessagePort(channel.port2);
88+
89+
port1.postMessage('done!');
90+
expect(port2.receiveMessageIfAvailable()?.message).toBe('done!');
91+
port1.close();
92+
});
93+
94+
it('on a closed channel', () => {
95+
const channel = SyncMessagePort.createChannel();
96+
const port1 = new SyncMessagePort(channel.port1);
97+
const port2 = new SyncMessagePort(channel.port2);
98+
99+
port1.close();
100+
expect(port2.receiveMessageIfAvailable()).toBe(undefined);
101+
});
102+
103+
it('bewteen receiving blocking messages', () => {
104+
const channel = SyncMessagePort.createChannel();
105+
const port = new SyncMessagePort(channel.port1);
106+
107+
spawnWorker(
108+
`
109+
// Wait a little bit just to make entirely sure that the parent thread
110+
// is awaiting a message.
111+
setTimeout(() => {
112+
port.postMessage('first');
113+
port.postMessage('second');
114+
115+
setTimeout(() => {
116+
port.postMessage('third');
117+
port.close();
118+
}, 100);
119+
}, 100);
120+
`,
121+
channel.port2,
122+
);
123+
124+
expect(port.receiveMessage()).toEqual('first');
125+
expect(port.receiveMessageIfAvailable()?.message).toEqual('second');
126+
expect(port.receiveMessage()).toEqual('third');
127+
});
128+
});
129+
130+
describe('timeout', () => {
131+
it("returns a value if it's already available", () => {
132+
const channel = SyncMessagePort.createChannel();
133+
const port1 = new SyncMessagePort(channel.port1);
134+
const port2 = new SyncMessagePort(channel.port2);
135+
port1.postMessage('message');
136+
expect(port2.receiveMessage({timeout: 0})).toBe('message');
137+
});
138+
139+
it('returns a value if it becomes available before the timeout', () => {
140+
const channel = SyncMessagePort.createChannel();
141+
const port = new SyncMessagePort(channel.port1);
142+
143+
spawnWorker(
144+
`
145+
port.postMessage('ready');
146+
setTimeout(() => {
147+
port.postMessage('message');
148+
port.close();
149+
}, 100);
150+
`,
151+
channel.port2,
152+
);
153+
154+
expect(port.receiveMessage()).toEqual('ready');
155+
expect(port.receiveMessage({timeout: 200})).toEqual('message');
156+
});
157+
158+
it('throws an error if it times out before a value is available', () => {
159+
const channel = SyncMessagePort.createChannel();
160+
const port = new SyncMessagePort(channel.port1);
161+
expect(() => port.receiveMessage({timeout: 0})).toThrow(TimeoutException);
162+
});
163+
164+
it('returns timeoutValue if it times out before a value is available', () => {
165+
const channel = SyncMessagePort.createChannel();
166+
const port = new SyncMessagePort(channel.port1);
167+
expect(port.receiveMessage({timeout: 0, timeoutValue: 'timed out'})).toBe(
168+
'timed out',
169+
);
170+
});
171+
172+
it('throws an error if the channel closes before the request times out', () => {
173+
const channel = SyncMessagePort.createChannel();
174+
const port = new SyncMessagePort(channel.port1);
175+
176+
spawnWorker(
177+
`
178+
port.postMessage('ready');
179+
setTimeout(() => {
180+
port.close();
181+
}, 100);
182+
`,
183+
channel.port2,
184+
);
185+
186+
expect(port.receiveMessage()).toEqual('ready');
187+
// timeoutValue shouldn't take precedence over this error
188+
expect(() =>
189+
port.receiveMessage({timeout: 10000, timeoutValue: 'timed out'}),
190+
).toThrow();
191+
});
192+
});
193+
76194
describe('with an asynchronous listener', () => {
77195
it('receives a message sent before listening', async () => {
78196
const channel = SyncMessagePort.createChannel();
@@ -127,7 +245,7 @@ describe('SyncMessagePort', () => {
127245
await new Promise(resolve => port2.once('close', resolve));
128246
});
129247

130-
it('receiveMessage() throws an error for a closed port', () => {
248+
it("receiveMessage() throws an error for a port that's already closed", () => {
131249
const channel = SyncMessagePort.createChannel();
132250
const port1 = new SyncMessagePort(channel.port1);
133251
const port2 = new SyncMessagePort(channel.port2);
@@ -136,6 +254,52 @@ describe('SyncMessagePort', () => {
136254
expect(port1.receiveMessage).toThrow();
137255
expect(port2.receiveMessage).toThrow();
138256
});
257+
258+
it('receiveMessage() throws an error when a port closes', () => {
259+
const channel = SyncMessagePort.createChannel();
260+
const port = new SyncMessagePort(channel.port1);
261+
262+
spawnWorker(
263+
`
264+
setTimeout(() => {
265+
port.close();
266+
}, 100);
267+
`,
268+
channel.port2,
269+
);
270+
271+
expect(port.receiveMessage).toThrow();
272+
});
273+
274+
it(
275+
"receiveMessage() returns option.closedValue for a port that's " +
276+
'already closed',
277+
() => {
278+
const channel = SyncMessagePort.createChannel();
279+
const port1 = new SyncMessagePort(channel.port1);
280+
const port2 = new SyncMessagePort(channel.port2);
281+
282+
port1.close();
283+
expect(port1.receiveMessage({closedValue: 'closed'})).toBe('closed');
284+
expect(port2.receiveMessage({closedValue: 'closed'})).toBe('closed');
285+
},
286+
);
287+
288+
it('receiveMessage() throws an error when a port closes', () => {
289+
const channel = SyncMessagePort.createChannel();
290+
const port = new SyncMessagePort(channel.port1);
291+
292+
spawnWorker(
293+
`
294+
setTimeout(() => {
295+
port.close();
296+
}, 100);
297+
`,
298+
channel.port2,
299+
);
300+
301+
expect(port.receiveMessage({closedValue: 'closed'})).toBe('closed');
302+
});
139303
});
140304
});
141305

lib/index.ts

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
MessageChannel,
99
MessagePort,
1010
TransferListItem,
11+
Worker,
1112
receiveMessageOnPort,
1213
} from 'worker_threads';
1314

@@ -36,6 +37,40 @@ enum BufferState {
3637
Closed = 0b10,
3738
}
3839

40+
/**
41+
* Options that can be passed to {@link SyncMessagePort.receiveMessage}.
42+
*/
43+
export interface ReceiveMessageOptions {
44+
/**
45+
* The time (in milliseconds) to wait for a message before returning {@link
46+
* timeoutValue} (if set) or throwing a [TimeoutException] otherwise.
47+
*/
48+
timeout?: number;
49+
50+
/**
51+
* If a message isn't received within {@link timeout} milliseconds, this value
52+
* is returned. Ignored if {@link timeout} is not set.
53+
*/
54+
timeoutValue?: unknown;
55+
56+
/**
57+
* If the underlying channel is closed before calling {@link
58+
* SyncMessagePort.receiveMessage} or while a call is pending, return this
59+
* value.
60+
*/
61+
closedValue?: unknown;
62+
}
63+
64+
/**
65+
* An exception thrown by {@link SyncMessagePort.receiveMessage} if a message
66+
* isn't received within {@link ReceivedMessageOptions.timeout} milliseconds.
67+
*/
68+
export class TimeoutException extends Error {
69+
constructor(message: string) {
70+
super(message);
71+
}
72+
}
73+
3974
/**
4075
* A communication port that can receive messages synchronously from another
4176
* `SyncMessagePort`.
@@ -110,20 +145,36 @@ export class SyncMessagePort extends EventEmitter {
110145
}
111146
}
112147

113-
// TODO(nex3):
114-
// * Add a non-blocking `receiveMessage()`
115-
// * Add a timeout option to `receiveMessage()`
116-
// * Add an option to `receiveMessage()` to return a special value if the
117-
// channel is closed.
148+
/**
149+
* Returns the message sent by the other port, if one is available. This *does
150+
* not* block, and will return `undefined` immediately if no message is
151+
* available. In order to distinguish between a message with value `undefined`
152+
* and no message, a message is return in an object with a `message` field.
153+
*
154+
* This may not be called while this has a listener for the `'message'` event.
155+
* It does *not* throw an error if the port is closed when this is called;
156+
* instead, it just returns `undefined`.
157+
*/
158+
receiveMessageIfAvailable(): {message: unknown} | undefined {
159+
if (this.listenerCount('message')) {
160+
throw new Error(
161+
'SyncMessageChannel.receiveMessageIfAvailable() may not be called ' +
162+
'while there are message listeners.',
163+
);
164+
}
165+
166+
return receiveMessageOnPort(this.port);
167+
}
118168

119169
/**
120170
* Blocks and returns the next message sent by the other port.
121171
*
122172
* This may not be called while this has a listener for the `'message'` event.
123173
* Throws an error if the channel is closed, including if it closes while this
124-
* is waiting for a message.
174+
* is waiting for a message, unless {@link ReceiveMessageOptions.closedValue}
175+
* is passed.
125176
*/
126-
receiveMessage(): unknown {
177+
receiveMessage(options?: ReceiveMessageOptions): unknown {
127178
if (this.listenerCount('message')) {
128179
throw new Error(
129180
'SyncMessageChannel.receiveMessage() may not be called while there ' +
@@ -136,14 +187,14 @@ export class SyncMessagePort extends EventEmitter {
136187
// `receiveMessageOnPort` and the call to `Atomics.wait()`, we won't
137188
// overwrite it. Use `Atomics.compareExchange` so that we don't overwrite
138189
// the "closed" state.
139-
if (
140-
Atomics.compareExchange(
141-
this.buffer,
142-
0,
143-
BufferState.MessageSent,
144-
BufferState.AwaitingMessage,
145-
) === BufferState.Closed
146-
) {
190+
const previousState = Atomics.compareExchange(
191+
this.buffer,
192+
0,
193+
BufferState.MessageSent,
194+
BufferState.AwaitingMessage,
195+
);
196+
if (previousState === BufferState.Closed) {
197+
if (options && 'closedValue' in options) return options.closedValue;
147198
throw new Error("The SyncMessagePort's channel is closed.");
148199
}
149200

@@ -153,20 +204,32 @@ export class SyncMessagePort extends EventEmitter {
153204
// If there's no new message, wait for the other port to flip the "new
154205
// message" indicator to 1. If it's been set to 1 since we stored 0, this
155206
// will terminate immediately.
156-
Atomics.wait(this.buffer, 0, BufferState.AwaitingMessage);
207+
const result = Atomics.wait(
208+
this.buffer,
209+
0,
210+
BufferState.AwaitingMessage,
211+
options?.timeout,
212+
);
157213
message = receiveMessageOnPort(this.port);
158214
if (message) return message.message;
159215

216+
if (result === 'timed-out') {
217+
if ('timeoutValue' in options!) return options.timeoutValue;
218+
throw new TimeoutException('SyncMessagePort.receiveMessage() timed out.');
219+
}
220+
160221
// Update the state to 0b10 after the last message is consumed.
161222
const oldState = Atomics.and(this.buffer, 0, BufferState.Closed);
162223
// Assert the old state was either 0b10 or 0b11.
163224
assert.equal(oldState & BufferState.Closed, BufferState.Closed);
225+
if (options && 'closedValue' in options) return options.closedValue;
164226
throw new Error("The SyncMessagePort's channel is closed.");
165227
}
166228

167229
/** See `MessagePort.close()`. */
168230
close(): void {
169231
Atomics.or(this.buffer, 0, BufferState.Closed);
232+
Atomics.notify(this.buffer, 0);
170233
this.port.close();
171234
}
172235
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sync-message-port",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "A Node.js communication port that can pass messages synchronously between workers",
55
"repository": "sass/sync-message-port",
66
"author": "Google Inc.",

0 commit comments

Comments
 (0)