-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Experimental service worker support (#2)
- Loading branch information
1 parent
45e563c
commit d165b5d
Showing
8 changed files
with
441 additions
and
10 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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import toCloneable from './toCloneable'; | ||
import { RequestMessage, ResponseMessage } from './Message'; | ||
|
||
/** | ||
* A fetch receiver | ||
* - For received requests, message to service worker. | ||
* - For messaged requests, send to the receiver. <- Intercept here. | ||
*/ | ||
export default class Client { | ||
workerContainer: ServiceWorkerContainer; | ||
|
||
constructor(workerContainer: ServiceWorkerContainer) { | ||
this.workerContainer = workerContainer; | ||
} | ||
|
||
fetchResolveList: (((res: Response) => void) | null)[] = []; | ||
|
||
/** | ||
* For received requests, message to the service worker. | ||
*/ | ||
fetch = async (...args: Parameters<typeof fetch>): Promise<Response> => { | ||
const request = new Request(...args); | ||
const { controller } = this.workerContainer; | ||
if (!controller) { | ||
throw new Error('Server not ready'); | ||
} | ||
controller.postMessage({ | ||
request: await toCloneable(request), | ||
id: this.fetchResolveList.length, | ||
}); | ||
return new Promise<Response>((resolve) => { | ||
this.fetchResolveList.push(resolve); | ||
}); | ||
}; | ||
|
||
onMessage = (event: MessageEvent<RequestMessage | ResponseMessage>): void => { | ||
if (event.data && 'request' in event.data) { | ||
// eslint-disable-next-line no-void | ||
void this.onRequestMessage(event as MessageEvent<RequestMessage>); | ||
} | ||
if (event.data && 'response' in event.data) { | ||
// eslint-disable-next-line no-void | ||
void this.onResponseMessage(event as MessageEvent<ResponseMessage>); | ||
} | ||
}; | ||
|
||
/** | ||
* For messaged requests, send to the receiver. | ||
*/ | ||
onRequestMessage = async ( | ||
event: MessageEvent<RequestMessage>, | ||
): Promise<void> => { | ||
const { data: { request, id } } = event; | ||
const response = await this.fetch(request.url, request); | ||
const { controller } = this.workerContainer; | ||
if (!controller) { | ||
throw new Error('Server not ready'); | ||
} | ||
controller.postMessage({ | ||
response: await toCloneable(response), | ||
id, | ||
}); | ||
}; | ||
|
||
/** | ||
* For messaged responses, treat as a previous received request's response. | ||
*/ | ||
onResponseMessage = async ( | ||
event: MessageEvent<ResponseMessage>, | ||
): Promise<void> => { | ||
const { data: { response, id } } = event; | ||
const resolve = this.fetchResolveList[id]; | ||
if (!resolve) return; | ||
resolve(new Response(response.body, response)); | ||
this.fetchResolveList[id] = null; | ||
}; | ||
|
||
private addedListeners = false; | ||
|
||
isActive(): boolean { | ||
return this.addedListeners; | ||
} | ||
|
||
/** | ||
* Stop receiving worker messages. | ||
*/ | ||
deactivate(): void { | ||
this.workerContainer.removeEventListener('message', this.onMessage); | ||
this.addedListeners = false; | ||
} | ||
|
||
/** | ||
* Start receiving worker messages. | ||
*/ | ||
activate(): void { | ||
this.workerContainer.addEventListener('message', this.onMessage); | ||
this.addedListeners = true; | ||
} | ||
} |
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,13 @@ | ||
import { CloneableRequest, CloneableResponse } from './toCloneable'; | ||
|
||
export interface Message { | ||
id: number; | ||
} | ||
|
||
export interface RequestMessage extends Message { | ||
request: CloneableRequest; | ||
} | ||
|
||
export interface ResponseMessage extends Message { | ||
response: CloneableResponse; | ||
} |
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,104 @@ | ||
/// <reference lib="WebWorker" /> | ||
import toCloneable from './toCloneable'; | ||
import { RequestMessage, ResponseMessage } from './Message'; | ||
|
||
/** | ||
* A fetch forwarder | ||
* - For captured requests, message back to client. | ||
* - For messaged requests, message back the real response. | ||
*/ | ||
export default class Worker { | ||
scope: ServiceWorkerGlobalScope; | ||
|
||
constructor(scope: ServiceWorkerGlobalScope) { | ||
this.scope = scope; | ||
} | ||
|
||
onFetchResolveList: (((res: Response) => void) | null)[] = []; | ||
|
||
/** | ||
* For captured requests, message back to the client. | ||
*/ | ||
onFetch = (event: FetchEvent): void => { | ||
const respondPromise = (async () => { | ||
const client = await this.scope.clients.get(event.clientId); | ||
if (!client) { | ||
throw new Error('No client matched'); | ||
} | ||
client.postMessage({ | ||
request: await toCloneable(event.request), | ||
id: this.onFetchResolveList.length, | ||
}); | ||
return new Promise<Response>((resolve) => { | ||
this.onFetchResolveList.push(resolve); | ||
}); | ||
})(); | ||
event.respondWith(respondPromise); | ||
}; | ||
|
||
onMessage = (event: ExtendableMessageEvent): void => { | ||
if (event.data && 'request' in event.data) { | ||
// eslint-disable-next-line no-void | ||
void this.onRequestMessage(event); | ||
} | ||
if (event.data && 'response' in event.data) { | ||
// eslint-disable-next-line no-void | ||
void this.onResponseMessage(event); | ||
} | ||
}; | ||
|
||
/** | ||
* For messaged requests, respond with real responses. | ||
*/ | ||
onRequestMessage = async ( | ||
event: Omit<ExtendableMessageEvent, 'data'> & { data: RequestMessage }, | ||
): Promise<void> => { | ||
const { source, data: { request, id } } = event; | ||
if (!source) { | ||
throw new Error('Request came from unknown source'); | ||
} | ||
source.postMessage({ | ||
response: await toCloneable(await this.scope.fetch(request.url, request)), | ||
id, | ||
}); | ||
}; | ||
|
||
/** | ||
* For messaged responses, treat as a previous captured request's response. | ||
*/ | ||
onResponseMessage = async ( | ||
event: Omit<ExtendableMessageEvent, 'data'> & { data: ResponseMessage }, | ||
): Promise<void> => { | ||
const { data: { response, id } } = event; | ||
const resolve = this.onFetchResolveList[id]; | ||
if (!resolve) { | ||
throw new Error('Response came for unknown request'); | ||
} | ||
resolve(new Response(response.body, response)); | ||
this.onFetchResolveList[id] = null; | ||
}; | ||
|
||
private addedListeners = false; | ||
|
||
isActive(): boolean { | ||
return this.addedListeners; | ||
} | ||
|
||
/** | ||
* Stop capturing requests and receiving messages. | ||
*/ | ||
deactivate(): void { | ||
this.scope.removeEventListener('fetch', this.onFetch); | ||
this.scope.removeEventListener('message', this.onMessage); | ||
this.addedListeners = false; | ||
} | ||
|
||
/** | ||
* Start capturing requests and receiving messages. | ||
*/ | ||
activate(): void { | ||
this.scope.addEventListener('fetch', this.onFetch); | ||
this.scope.addEventListener('message', this.onMessage); | ||
this.addedListeners = true; | ||
} | ||
} |
Oops, something went wrong.