-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: sandpack service worker (#8481)
- Loading branch information
Showing
11 changed files
with
715 additions
and
4 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,111 @@ | ||
import { invariant } from 'outvariant'; | ||
|
||
import { debug, getServiceWorker, preventStaleTermination } from './utils'; | ||
import { | ||
CHANNEL_NAME, | ||
IPreviewInitMessage, | ||
IPreviewReadyMessage, | ||
IPreviewResponseMessage, | ||
IWorkerInitMessage, | ||
} from './types'; | ||
import { DeferredPromise } from './promise'; | ||
|
||
// Create a message channel for communication with the Service Worker. | ||
const workerChannel = new MessageChannel(); | ||
|
||
const workerReadyPromise = new DeferredPromise<ServiceWorker>(); | ||
|
||
workerReadyPromise.then(worker => { | ||
debug('[relay] worker is ready, initializing MessageChannel...'); | ||
|
||
// Always post the initial MessageChannel message to the worker | ||
// as soon as the worker is ready. This is done once. | ||
const workerInitMessage: IWorkerInitMessage = { | ||
$channel: CHANNEL_NAME, | ||
$type: 'worker/init', | ||
}; | ||
worker.postMessage(workerInitMessage, [workerChannel.port2]); | ||
|
||
return worker; | ||
}); | ||
|
||
const parentPortPromise = new DeferredPromise<MessagePort>(); | ||
window.addEventListener( | ||
'message', | ||
(event: MessageEvent<IPreviewInitMessage>) => { | ||
if (event.data.$type === 'preview/init') { | ||
const parentPort = event.ports[0]; | ||
parentPort.onmessage = async (evt: MessageEvent) => { | ||
if ( | ||
typeof evt.data === 'object' && | ||
evt.data.$channel === CHANNEL_NAME && | ||
evt.data.$type === 'preview/response' | ||
) { | ||
const msg: IPreviewResponseMessage = evt.data; | ||
workerChannel.port1.postMessage(msg); | ||
} | ||
}; | ||
parentPortPromise.resolve(parentPort); | ||
} | ||
} | ||
); | ||
|
||
workerChannel.port1.onmessage = async event => { | ||
const data = event.data; | ||
|
||
// console.debug("incoming message from the worker", event.data); | ||
|
||
if (data.$channel === CHANNEL_NAME) { | ||
// Pause the message handling until the parent has taken control of the preview. | ||
const port = await parentPortPromise; | ||
|
||
// Route all data to the parent. | ||
const message = data; | ||
port.postMessage(message); | ||
} | ||
}; | ||
|
||
export async function startServiceWorker() { | ||
const worker = await getServiceWorker().catch(error => { | ||
console.error( | ||
'[relay] Failed to ensure the relay has a Service Worker registered. See details below.' | ||
); | ||
console.error(error); | ||
}); | ||
|
||
await navigator.serviceWorker.ready; | ||
|
||
invariant( | ||
worker, | ||
'[relay] Failed to retrieve the worker instance: worker not found' | ||
); | ||
preventStaleTermination(worker); | ||
|
||
if (process.env.NODE_ENV === 'development') { | ||
window.addEventListener('beforeunload', async () => { | ||
const registrations = await navigator.serviceWorker.getRegistrations(); | ||
|
||
for (const registration of registrations) { | ||
// eslint-disable-next-line no-await-in-loop | ||
await registration.unregister(); | ||
} | ||
|
||
debug('[relay] Unregister all SW'); | ||
}); | ||
} | ||
|
||
workerReadyPromise.resolve(worker); | ||
|
||
debug('[relay] Worker ready'); | ||
|
||
// Wait until the parent sends the init event | ||
// via the MessageChannel, acknowledging that it recognized the relay. | ||
const parentPort = await parentPortPromise; | ||
debug('[relay] Parent port received', parentPort); | ||
|
||
const readyMessage: IPreviewReadyMessage = { | ||
$channel: CHANNEL_NAME, | ||
$type: 'preview/ready', | ||
}; | ||
parentPort.postMessage(readyMessage); | ||
} |
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,118 @@ | ||
/** | ||
* Copied from https://github.com/open-draft/deferred-promise | ||
* because the current Babel configuration doesn't support static block in classes | ||
*/ | ||
|
||
type PromiseState = 'pending' | 'fulfilled' | 'rejected' | ||
|
||
type Executor<Value> = ConstructorParameters<typeof Promise<Value>>[0] | ||
type ResolveFunction<Value> = Parameters<Executor<Value>>[0] | ||
type RejectFunction<Reason> = Parameters<Executor<Reason>>[1] | ||
|
||
type DeferredPromiseExecutor<Input = never, Output = Input> = { | ||
(resolve?: ResolveFunction<Input>, reject?: RejectFunction<any>): void | ||
|
||
resolve: ResolveFunction<Input> | ||
reject: RejectFunction<any> | ||
result?: Output | ||
state: PromiseState | ||
rejectionReason?: unknown | ||
} | ||
function createDeferredExecutor< | ||
Input = never, | ||
Output = Input | ||
>(): DeferredPromiseExecutor<Input, Output> { | ||
const executor = <DeferredPromiseExecutor<Input, Output>>(( | ||
resolve, | ||
reject | ||
) => { | ||
executor.state = 'pending' | ||
|
||
executor.resolve = (data) => { | ||
if (executor.state !== 'pending') { | ||
return | ||
} | ||
|
||
executor.result = data as Output | ||
|
||
const onFulfilled = <Value>(value: Value) => { | ||
executor.state = 'fulfilled' | ||
return value | ||
} | ||
|
||
// eslint-disable-next-line | ||
return resolve( | ||
data instanceof Promise ? data : Promise.resolve(data).then(onFulfilled) | ||
) | ||
} | ||
|
||
executor.reject = (reason) => { | ||
if (executor.state !== 'pending') { | ||
return | ||
} | ||
|
||
queueMicrotask(() => { | ||
executor.state = 'rejected' | ||
}) | ||
|
||
// eslint-disable-next-line | ||
return reject((executor.rejectionReason = reason)) | ||
} | ||
}) | ||
|
||
return executor | ||
} | ||
|
||
export class DeferredPromise<Input, Output = Input> extends Promise<Input> { | ||
executor: DeferredPromiseExecutor; | ||
|
||
public resolve: ResolveFunction<Output>; | ||
public reject: RejectFunction<Output>; | ||
|
||
constructor(executor: Executor<Input> | null = null) { | ||
const deferredExecutor = createDeferredExecutor(); | ||
super((originalResolve, originalReject) => { | ||
deferredExecutor(originalResolve, originalReject); | ||
// eslint-disable-next-line | ||
executor?.(deferredExecutor.resolve, deferredExecutor.reject); | ||
}); | ||
|
||
this.executor = deferredExecutor; | ||
this.resolve = this.executor.resolve; | ||
this.reject = this.executor.reject; | ||
} | ||
|
||
public get state() { | ||
return this.executor.state; | ||
} | ||
|
||
public get rejectionReason() { | ||
return this.executor.rejectionReason; | ||
} | ||
|
||
public then<ThenResult = Input, CatchResult = never>( | ||
onFulfilled?: (value: Input) => ThenResult | PromiseLike<ThenResult>, | ||
onRejected?: (reason: any) => CatchResult | PromiseLike<CatchResult> | ||
) { | ||
return this.decorate(super.then(onFulfilled, onRejected)); | ||
} | ||
|
||
public catch<CatchResult = never>( | ||
onRejected?: (reason: any) => CatchResult | PromiseLike<CatchResult> | ||
) { | ||
return this.decorate(super.catch(onRejected)); | ||
} | ||
|
||
public finally(onfinally?: () => void | Promise<any>) { | ||
return this.decorate(super.finally(onfinally)); | ||
} | ||
|
||
decorate<ChildInput>( | ||
promise: Promise<ChildInput> | ||
): DeferredPromise<ChildInput, Output> { | ||
return Object.defineProperties(promise, { | ||
resolve: { configurable: true, value: this.resolve }, | ||
reject: { configurable: true, value: this.reject }, | ||
}) as DeferredPromise<ChildInput, Output>; | ||
} | ||
} |
Oops, something went wrong.