-
-
Notifications
You must be signed in to change notification settings - Fork 526
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: update "headers-polyfill" and "@mswjs/interceptors" to fix "/lib…
…" issue
- Loading branch information
1 parent
a9b8126
commit 184a7d6
Showing
6 changed files
with
136 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# Research | ||
|
||
Mock Service Worker utilizes the Service Worker API to implement the interception of requests. As you may have known, Service Workers aren't exactly designed for that. Over the years of project development there have been numerous challenges to bent workers to behave as one would expect in the context of API mocking. In this paper, I would like to write down my findings and the outcomes of those findings. | ||
|
||
## Challenge: Affecting unrelated projects | ||
|
||
A Service Worker controls a client based on its address. So different clients like `http://localhost:3000` and `https://mswjs.io` can have their own workers installed in your browser, affecting the traffic of respective pages whenever you visit them. | ||
|
||
MSW is a development tool, and during development we often run different projects at the same address like `http://localhost:3000`. This means that by default, our worker will activate for all those projects regardless if they're using the library. While the worker won't do much on its own, and needs a client counterpart that would communicate instructions to it, it will still intercept the traffic, which is unnecessary in the case when you're not mocking anything. | ||
|
||
### Resolution | ||
|
||
To prevent the worker from affecting unrelated projects at the same address, we've introduced a _self-destructive_ worker. The worker is shared between clients, so we can keep the map of client IDs internally in the worker, and whenever that map reaches 0 (read: the last controlled client has been closed), we can unregister the worker from within itself: | ||
|
||
```ts | ||
if (remainingClients.length === 0) { | ||
self.registration.unregister() | ||
} | ||
``` | ||
|
||
> Note that even when you unregister the worker, it will still receive events like "fetch" until the page is reloaded. We're also accounting for that in the "fetch" listener, checking whether there are any `activeClientIds` when handling intercepted requests. | ||
Each client signals about being closed in the "beforeunload" event by sending a message to the `controller.postMessage()`. It is in the "message" event listener of the worker when the self-unregistration happens. | ||
|
||
--- | ||
|
||
## Discovery: Controller, Message and Broadcast channels | ||
|
||
There are [multiple ways to communicate with the worker](https://felixgerschau.com/how-to-communicate-with-service-workers/) but they are quite different in nature. | ||
|
||
### Controller messages | ||
|
||
A client may send any data to the worker via the `navigator.serviceWorker.controller` object. That controlled will post messages to any workers controlling the page but cannot be used to receive messages from the worker. | ||
|
||
```js | ||
// Let the worker know that the current client is being closed. | ||
// We don't need any confirmation from the worker for this event. | ||
window.addEventListener('beforeunload', () => { | ||
navigator.serviceWorker.controller.postMessage({ | ||
type: 'CLIENT_CLOSED', | ||
}) | ||
}) | ||
``` | ||
|
||
Controller messages are great for one-way signaling of events that affect the entire worker. The worker, in turn, can handle these messages in its "message" event: | ||
|
||
```ts | ||
self.addEventListener('message', (event) => { | ||
event.data.type // "CLIENT_CLOSED" | ||
}) | ||
``` | ||
|
||
> Keep in mind that controller messages handling is global, and affect the entire worker. This makes controller messages rather unsuitable to handle request-based data signaling. You should prefer other means below to achieve that. | ||
### `MessageChannel` | ||
|
||
We are using a message channel to send a message to the client _and await_ its response. | ||
|
||
```ts | ||
// Get the client behind the intercepted request. | ||
const client = await self.clients.get(event.clientId) | ||
|
||
const channel = new MessageChannel() | ||
|
||
// Listen to a message from the client. | ||
// You can wrap this in a Promise to pause request | ||
// handling until a certain message is received. | ||
channel.port1.onmessage = (event) => { | ||
console.log('from client:', event.data) | ||
} | ||
|
||
// Send a message to the client. For example, | ||
// a message like "hey, a request has happened!". | ||
client.postMessage('payload', [channel.port2]) | ||
``` | ||
|
||
### `BroadcastChannel` | ||
|
||
Broadcast channel is much similar to the `MessageChannel` with one important distinction: it uses a unique _channel ID_ instead of message channel ports. Whenever two instances of a broadcast channel with the same ID are created, they can communicate with each other even being in different contexts (like two separate tags) or threads (like the client and the worker). | ||
|
||
```ts | ||
self.addEventListener('fetch', (event) => { | ||
const requestId = generateRequestId(event.request) | ||
const operationChannel = new BroadcastChannel(`msw-request-${requestId}`) | ||
|
||
operationChannel.onmessage = (event) => { | ||
console.log('from client:', event.data) | ||
} | ||
|
||
operationChannel.postMessage('to the client') | ||
}) | ||
``` | ||
|
||
Since it's the worker who sends the information about an intercepted request, including the generated request ID, the client can also construct a `BroadcastChannel` with the same ID, and communicate in the context of a particular request. | ||
|
||
> Broadcast channel is fantastic when you need to scope incoming/outgoing messages. In comparison with the controller messages, which are global, broadcasted messages can be abstracted into a function. | ||
```ts | ||
function respondWithStream(operationChannel) { | ||
let streamCtrl | ||
const stream = new ReadableStream({ | ||
start(controller) { | ||
streamCtrl = controller | ||
}, | ||
}) | ||
|
||
operationChannel.onmessage = (event) => { | ||
if (event.data.type === 'RESPONSE_CHUNK') { | ||
return streamCtrl.enqueue(event.data.chunk) | ||
} | ||
|
||
if (event.data.type === 'STREAM_END') { | ||
streamCtl.close() | ||
operationChannel.close() | ||
return new Response(stream) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
> Don't forget to close broadcast channels once they are no longer needed (i.e. request context has been destroyed). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters