Skip to content

Commit fbd59d1

Browse files
committed
Add support for Service Workers.
1 parent cb5f812 commit fbd59d1

File tree

3 files changed

+92
-31
lines changed

3 files changed

+92
-31
lines changed

README.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
React component for easy communication with a Web Worker. Leverages the Render Props pattern for ultimate flexibility as
1313
well as the new Context API for ease of use. Just specify the public url to your Web Worker and you'll get access to
14-
any messages or errors it sends, as well as the `postMessage` handler.
14+
any messages or errors it sends, as well as the `postMessage` handler. Also works with Service Workers.
1515

1616
- Zero dependencies
1717
- Choose between Render Props and Context-based helper components
@@ -22,6 +22,7 @@ any messages or errors it sends, as well as the `postMessage` handler.
2222
- Accepts `parser` and `serializer` for automatic message (de)serialization
2323
- Accepts `onMessage` and `onError` callbacks
2424
- Supports custom Worker instance through the `worker` prop **(new in v2)**
25+
- Supports communication with Service Workers **(new in v2.1)**
2526

2627
> This package was modeled after [`<Async>`](https://github.com/ghengeveld/react-async) which helps you deal with Promises in React.
2728
@@ -89,15 +90,33 @@ import WebWorker from "react-webworker"
8990

9091
const myWorker = new Worker("./worker.js") // relative path to the source file, not the public URL
9192

92-
const MyComponent = () => (
93-
<WebWorker worker={myWorker}>
94-
...
95-
</WebWorker>
96-
)
93+
const MyComponent = () => <WebWorker worker={myWorker}>...</WebWorker>
9794
```
9895

9996
The downside to this approach is that `<WebWorker>` will not manage the Worker's lifecycle. This means it will not automatically be terminated when `<WebWorker>` is unmounted.
10097

98+
### Communicating with a Service Worker
99+
100+
Using `<WebWorker>` with a Service Worker is as simple as passing it as a custom worker instance:
101+
102+
```js
103+
const MyComponent = () => <WebWorker worker={navigator.serviceWorker}>...</WebWorker>
104+
```
105+
106+
This will automatically setup a `MessageChannel` to enable bidirectional communication. Your Service Worker could look
107+
like this:
108+
109+
```js
110+
// `ports` is automatically provided with a MessageChannel port
111+
self.onmessage = ({ data, ports: [port] }) => {
112+
console.log("inside the service worker:", data)
113+
port.postMessage(data) // instead of `self.postMessage`
114+
}
115+
```
116+
117+
Note that messages sent to an inactive (not "activated") Service Worker will be silently ignored. Like a custom Worker,
118+
you'll have to deal with the Service Worker lifecycle yourself.
119+
101120
## API
102121

103122
### Props

src/index.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,29 @@ class WebWorker extends React.Component {
3939
const { serializer = x => x } = this.props
4040
const { postMessage } = this.worker || {}
4141
if (!postMessage) throw new Error("Worker not initialized")
42-
this.setState({ lastPostAt: new Date() }, () => postMessage.call(this.worker, serializer(data)))
42+
if (this.worker.state && this.worker.state !== "activated") return
43+
this.setState(
44+
{ lastPostAt: new Date() },
45+
() =>
46+
this.messageChannel
47+
? postMessage.call(this.worker, serializer(data), [this.messageChannel.port2])
48+
: postMessage.call(this.worker, serializer(data))
49+
)
4350
}
4451

4552
componentDidMount() {
4653
const { url, options, worker } = this.props
47-
this.worker = worker || new window.Worker(url, options)
48-
this.worker.onmessage = this.onMessage
49-
this.worker.onerror = this.onError
54+
this.worker = url ? new window.Worker(url, options) : worker.controller || worker
55+
56+
if ("onmessage" in this.worker) {
57+
this.worker.onmessage = this.onMessage
58+
this.worker.onerror = this.onError
59+
} else {
60+
this.messageChannel = new window.MessageChannel()
61+
this.messageChannel.port1.onmessage = this.onMessage
62+
this.messageChannel.port1.onmessageerror = this.onError
63+
}
64+
5065
this.mounted = true
5166
}
5267

src/spec.js

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import WebWorker from "./"
55

66
afterEach(cleanup)
77

8-
const worker = { postMessage: jest.fn(), terminate: jest.fn() }
8+
const worker = { onmessage: null, postMessage: jest.fn(), terminate: jest.fn() }
9+
const serviceWorker = { postMessage: jest.fn() }
10+
const messageChannel = { port1: {}, port2: jest.fn() }
911
window.Worker = jest.fn().mockImplementation(() => worker)
12+
window.ServiceWorker = jest.fn().mockImplementation(() => serviceWorker)
13+
window.MessageChannel = jest.fn().mockImplementation(() => messageChannel)
1014

1115
test("initializes a Worker on mount", () => {
1216
const options = {}
@@ -15,31 +19,35 @@ test("initializes a Worker on mount", () => {
1519
})
1620

1721
test("passes received messages to children as render prop", async () => {
18-
const { getByText } = render(<WebWorker>{({ messages }) => messages.map(m => m.data).join()}</WebWorker>)
22+
const { getByText } = render(
23+
<WebWorker url="/worker.js">{({ messages }) => messages.map(m => m.data).join()}</WebWorker>
24+
)
1925
worker.onmessage({ data: "foo" })
2026
worker.onmessage({ data: "bar" })
2127
worker.onmessage({ data: "baz" })
2228
await waitForElement(() => getByText("foo,bar,baz"))
2329
})
2430

2531
test("passes data of last received message to children as render prop", async () => {
26-
const { getByText } = render(<WebWorker>{({ data }) => data}</WebWorker>)
32+
const { getByText } = render(<WebWorker url="/worker.js">{({ data }) => data}</WebWorker>)
2733
worker.onmessage({ data: "foo" })
2834
worker.onmessage({ data: "bar" })
2935
worker.onmessage({ data: "baz" })
3036
await waitForElement(() => getByText("baz"))
3137
})
3238

3339
test("passes received errors to children as render prop", async () => {
34-
const { getByText } = render(<WebWorker>{({ errors }) => errors.map(e => e.error).join()}</WebWorker>)
40+
const { getByText } = render(
41+
<WebWorker url="/worker.js">{({ errors }) => errors.map(e => e.error).join()}</WebWorker>
42+
)
3543
worker.onerror({ error: "foo" })
3644
worker.onerror({ error: "bar" })
3745
worker.onerror({ error: "baz" })
3846
await waitForElement(() => getByText("foo,bar,baz"))
3947
})
4048

4149
test("passes last received error to children as render prop", async () => {
42-
const { getByText } = render(<WebWorker>{({ error }) => error}</WebWorker>)
50+
const { getByText } = render(<WebWorker url="/worker.js">{({ error }) => error}</WebWorker>)
4351
worker.onerror({ error: "foo" })
4452
worker.onerror({ error: "bar" })
4553
worker.onerror({ error: "baz" })
@@ -49,7 +57,9 @@ test("passes last received error to children as render prop", async () => {
4957
test("passes updatedAt date when a message is received", async () => {
5058
const date = new Date().toISOString().substr(0, 10)
5159
const { getByText, queryByText } = render(
52-
<WebWorker>{({ updatedAt }) => (updatedAt ? updatedAt.toISOString().substr(0, 10) : null)}</WebWorker>
60+
<WebWorker url="/worker.js">
61+
{({ updatedAt }) => (updatedAt ? updatedAt.toISOString().substr(0, 10) : null)}
62+
</WebWorker>
5363
)
5464
expect(queryByText(date)).toBeNull()
5565
worker.onmessage({ data: "foo" })
@@ -59,7 +69,9 @@ test("passes updatedAt date when a message is received", async () => {
5969
test("passes updatedAt date when an error is received", async () => {
6070
const date = new Date().toISOString().substr(0, 10)
6171
const { getByText, queryByText } = render(
62-
<WebWorker>{({ updatedAt }) => (updatedAt ? updatedAt.toISOString().substr(0, 10) : null)}</WebWorker>
72+
<WebWorker url="/worker.js">
73+
{({ updatedAt }) => (updatedAt ? updatedAt.toISOString().substr(0, 10) : null)}
74+
</WebWorker>
6375
)
6476
expect(queryByText(date)).toBeNull()
6577
worker.onerror({ error: "foo" })
@@ -68,29 +80,31 @@ test("passes updatedAt date when an error is received", async () => {
6880

6981
test("invokes onMessage callback with message data when a message is received", async () => {
7082
const onMessage = jest.fn()
71-
render(<WebWorker onMessage={onMessage} />)
83+
render(<WebWorker url="/worker.js" onMessage={onMessage} />)
7284
worker.onmessage({ data: "foo" })
7385
expect(onMessage).toHaveBeenCalledWith("foo")
7486
})
7587

7688
test("invokes onError callback with error when a error is received", async () => {
7789
const onError = jest.fn()
78-
render(<WebWorker onError={onError} />)
90+
render(<WebWorker url="/worker.js" onError={onError} />)
7991
worker.onerror({ error: "foo" })
8092
expect(onError).toHaveBeenCalledWith("foo")
8193
})
8294

8395
test("terminates the worker when unmounted", async () => {
8496
worker.terminate.mockClear()
85-
const { unmount } = render(<WebWorker />)
97+
const { unmount } = render(<WebWorker url="/worker.js" />)
8698
unmount()
8799
expect(worker.terminate).toHaveBeenCalled()
88100
})
89101

90102
test("postMessage sends messages to the worker", async () => {
91103
worker.postMessage.mockClear()
92104
const { getByText } = render(
93-
<WebWorker>{({ postMessage }) => <button onClick={() => postMessage("hello")}>go</button>}</WebWorker>
105+
<WebWorker url="/worker.js">
106+
{({ postMessage }) => <button onClick={() => postMessage("hello")}>go</button>}
107+
</WebWorker>
94108
)
95109
expect(worker.postMessage).not.toHaveBeenCalled()
96110
fireEvent.click(getByText("go"))
@@ -99,7 +113,7 @@ test("postMessage sends messages to the worker", async () => {
99113

100114
test("calling postMessage before having setup a worker will throw", async () => {
101115
render(
102-
<WebWorker>
116+
<WebWorker url="/worker.js">
103117
{({ postMessage }) => {
104118
expect(() => postMessage("hello")).toThrow(new Error("Worker not initialized"))
105119
}}
@@ -110,7 +124,7 @@ test("calling postMessage before having setup a worker will throw", async () =>
110124
test("serializer will prepare messages before sending them to the worker", async () => {
111125
worker.postMessage.mockClear()
112126
const { getByText } = render(
113-
<WebWorker serializer={JSON.stringify}>
127+
<WebWorker url="/worker.js" serializer={JSON.stringify}>
114128
{({ postMessage }) => <button onClick={() => postMessage({ foo: "bar" })}>go</button>}
115129
</WebWorker>
116130
)
@@ -122,7 +136,7 @@ test("serializer will prepare messages before sending them to the worker", async
122136
test("parser will deserialize messages received from the worker", async () => {
123137
const onMessage = jest.fn()
124138
const { getByText } = render(
125-
<WebWorker parser={JSON.parse} onMessage={onMessage}>
139+
<WebWorker url="/worker.js" parser={JSON.parse} onMessage={onMessage}>
126140
{({ data }) => data && data.foo}
127141
</WebWorker>
128142
)
@@ -133,7 +147,7 @@ test("parser will deserialize messages received from the worker", async () => {
133147

134148
test("supports passing a custom Worker instance", () => {
135149
const onMessage = jest.fn()
136-
const customWorker = { postMessage: jest.fn() }
150+
const customWorker = { onmessage: null, postMessage: jest.fn() }
137151
const { getByText } = render(
138152
<WebWorker worker={customWorker} onMessage={onMessage}>
139153
{({ postMessage }) => <button onClick={() => postMessage("hello")}>go</button>}
@@ -153,9 +167,22 @@ test("custom workers don't terminate on unmount", async () => {
153167
expect(customWorker.terminate).not.toHaveBeenCalled()
154168
})
155169

170+
test("supports Service Workers", () => {
171+
const onMessage = jest.fn()
172+
const customWorker = window.ServiceWorker()
173+
const { getByText } = render(
174+
<WebWorker worker={customWorker} onMessage={onMessage}>
175+
{({ postMessage }) => <button onClick={() => postMessage("hello")}>go</button>}
176+
</WebWorker>
177+
)
178+
expect(customWorker.postMessage).not.toHaveBeenCalled()
179+
fireEvent.click(getByText("go"))
180+
expect(customWorker.postMessage).toHaveBeenCalledWith("hello", [messageChannel.port2])
181+
})
182+
156183
test("WebWorker.Data renders with last message data only when a message has been received", async () => {
157184
const { getByText, queryByText } = render(
158-
<WebWorker>
185+
<WebWorker url="/worker.js">
159186
<WebWorker.Data>{data => data}</WebWorker.Data>
160187
</WebWorker>
161188
)
@@ -170,7 +197,7 @@ test("WebWorker.Data renders with last message data only when a message has been
170197

171198
test("WebWorker.Error renders with last error only when an error has been received", async () => {
172199
const { getByText, queryByText } = render(
173-
<WebWorker>
200+
<WebWorker url="/worker.js">
174201
<WebWorker.Error>{error => error}</WebWorker.Error>
175202
</WebWorker>
176203
)
@@ -185,7 +212,7 @@ test("WebWorker.Error renders with last error only when an error has been receiv
185212

186213
test("WebWorker.Pending renders only when no message has been received yet", async () => {
187214
const { getByText, queryByText } = render(
188-
<WebWorker>
215+
<WebWorker url="/worker.js">
189216
<WebWorker.Pending>pending</WebWorker.Pending>
190217
</WebWorker>
191218
)
@@ -196,7 +223,7 @@ test("WebWorker.Pending renders only when no message has been received yet", asy
196223

197224
test("WebWorker.Pending renders only when no error has been received yet", async () => {
198225
const { getByText, queryByText } = render(
199-
<WebWorker>
226+
<WebWorker url="/worker.js">
200227
<WebWorker.Pending>pending</WebWorker.Pending>
201228
</WebWorker>
202229
)
@@ -209,7 +236,7 @@ test("An unrelated change in props does not update the Context", async () => {
209236
let one
210237
let two
211238
const { rerender } = render(
212-
<WebWorker>
239+
<WebWorker url="/worker.js">
213240
<WebWorker.Pending>
214241
{value => {
215242
one = value
@@ -218,7 +245,7 @@ test("An unrelated change in props does not update the Context", async () => {
218245
</WebWorker>
219246
)
220247
rerender(
221-
<WebWorker someProp>
248+
<WebWorker url="/worker.js" someProp>
222249
<WebWorker.Pending>
223250
{value => {
224251
two = value

0 commit comments

Comments
 (0)