UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

230 lines (199 loc) • 8.2 kB
import {type MessagePort, type Worker} from 'worker_threads' type StreamReporter<TPayload = unknown> = {emit: (payload: TPayload) => void; end: () => void} type EventReporter<TPayload = unknown> = (payload: TPayload) => void type EventReceiver<TPayload = unknown> = () => Promise<TPayload> type StreamReceiver<TPayload = unknown> = () => AsyncIterable<TPayload> type EventKeys<TWorkerChannel extends WorkerChannel> = { [K in keyof TWorkerChannel]: TWorkerChannel[K] extends WorkerChannelEvent<any> ? K : never }[keyof TWorkerChannel] type StreamKeys<TWorkerChannel extends WorkerChannel> = { [K in keyof TWorkerChannel]: TWorkerChannel[K] extends WorkerChannelStream<any> ? K : never }[keyof TWorkerChannel] type EventMessage<TPayload = unknown> = {type: 'event'; name: string; payload: TPayload} type StreamEmissionMessage<TPayload = unknown> = {type: 'emission'; name: string; payload: TPayload} type StreamEndMessage = {type: 'end'; name: string} type WorkerChannelMessage = EventMessage | StreamEmissionMessage | StreamEndMessage /** * Represents the definition of a "worker channel" to report progress from the * worker to the parent. Worker channels can define named events or streams and * the worker will report events and streams while the parent will await them. * This allows the control flow of the parent to follow the control flow of the * worker 1-to-1. */ export type WorkerChannel< TWorkerChannel extends Record< string, WorkerChannelEvent<unknown> | WorkerChannelStream<unknown> > = Record<string, WorkerChannelEvent<unknown> | WorkerChannelStream<unknown>>, > = TWorkerChannel export type WorkerChannelEvent<TPayload = void> = {type: 'event'; payload: TPayload} export type WorkerChannelStream<TPayload = void> = {type: 'stream'; payload: TPayload} export interface WorkerChannelReporter<TWorkerChannel extends WorkerChannel> { event: { [K in EventKeys<TWorkerChannel>]: TWorkerChannel[K] extends WorkerChannelEvent<infer TPayload> ? EventReporter<TPayload> : void } stream: { [K in StreamKeys<TWorkerChannel>]: TWorkerChannel[K] extends WorkerChannelStream<infer TPayload> ? StreamReporter<TPayload> : void } } export interface WorkerChannelReceiver<TWorkerChannel extends WorkerChannel> { event: { [K in EventKeys<TWorkerChannel>]: TWorkerChannel[K] extends WorkerChannelEvent<infer TPayload> ? EventReceiver<TPayload> : void } stream: { [K in StreamKeys<TWorkerChannel>]: TWorkerChannel[K] extends WorkerChannelStream<infer TPayload> ? StreamReceiver<TPayload> : void } // TODO: good candidate for [Symbol.asyncDispose] when our tooling better supports it dispose: () => Promise<number> } /** * A simple queue that has two primary methods: `push(message)` and * `await next()`. This message queue is used by the "receiver" of the worker * channel and this class handles buffering incoming messages if the worker is * producing faster than the parent as well as returning a promise if there is * no message yet in the queue when the parent awaits `next()`. */ class MessageQueue<T> { resolver: ((result: IteratorResult<T>) => void) | null = null queue: T[] = [] push(message: T) { if (this.resolver) { this.resolver({value: message, done: false}) this.resolver = null } else { this.queue.push(message) } } next(): Promise<IteratorResult<T>> { if (this.queue.length) { return Promise.resolve({value: this.queue.shift()!, done: false}) } return new Promise((resolve) => (this.resolver = resolve)) } end() { if (this.resolver) { this.resolver({value: undefined, done: true}) } } } function isWorkerChannelMessage(message: unknown): message is WorkerChannelMessage { if (typeof message !== 'object') return false if (!message) return false if (!('type' in message)) return false if (typeof message.type !== 'string') return false const types: string[] = ['event', 'emission', 'end'] satisfies WorkerChannelMessage['type'][] return types.includes(message.type) } /** * Creates a "worker channel receiver" that subscribes to incoming messages * from the given worker and returns promises for worker channel events and * async iterators for worker channel streams. */ export function createReceiver<TWorkerChannel extends WorkerChannel>( worker: Worker, ): WorkerChannelReceiver<TWorkerChannel> { const _events = new Map<string, MessageQueue<EventMessage>>() const _streams = new Map<string, MessageQueue<StreamEmissionMessage>>() const errors = new MessageQueue<{type: 'error'; error: unknown}>() const eventQueue = (name: string) => { const queue = _events.get(name) ?? new MessageQueue() if (!_events.has(name)) _events.set(name, queue) return queue } const streamQueue = (name: string) => { const queue = _streams.get(name) ?? new MessageQueue() if (!_streams.has(name)) _streams.set(name, queue) return queue } const handleMessage = (message: unknown) => { if (!isWorkerChannelMessage(message)) return if (message.type === 'event') eventQueue(message.name).push(message) if (message.type === 'emission') streamQueue(message.name).push(message) if (message.type === 'end') streamQueue(message.name).end() } const handleError = (error: unknown) => { errors.push({type: 'error', error}) } worker.addListener('message', handleMessage) worker.addListener('error', handleError) return { event: new Proxy({} as WorkerChannelReceiver<TWorkerChannel>['event'], { get: (target, name) => { if (typeof name !== 'string') return target[name as keyof typeof target] const eventReceiver: EventReceiver = async () => { const {value} = await Promise.race([eventQueue(name).next(), errors.next()]) if (value.type === 'error') throw value.error return value.payload } return eventReceiver }, }), stream: new Proxy({} as WorkerChannelReceiver<TWorkerChannel>['stream'], { get: (target, prop) => { if (typeof prop !== 'string') return target[prop as keyof typeof target] const name = prop // alias for better typescript narrowing async function* streamReceiver() { while (true) { const {value, done} = await Promise.race([streamQueue(name).next(), errors.next()]) if (done) return if (value.type === 'error') throw value.error yield value.payload } } return streamReceiver satisfies StreamReceiver }, }), dispose: () => { worker.removeListener('message', handleMessage) worker.removeListener('error', handleError) return worker.terminate() }, } } /** * Creates a "worker channel reporter" that sends messages to the given * `parentPort` to be received by a worker channel receiver. */ export function createReporter<TWorkerChannel extends WorkerChannel>( parentPort: MessagePort | null, ): WorkerChannelReporter<TWorkerChannel> { if (!parentPort) { throw new Error('parentPart was falsy') } return { event: new Proxy({} as WorkerChannelReporter<TWorkerChannel>['event'], { get: (target, name) => { if (typeof name !== 'string') return target[name as keyof typeof target] const eventReporter: EventReporter = (payload) => { const message: EventMessage = {type: 'event', name, payload} parentPort.postMessage(message) } return eventReporter }, }), stream: new Proxy({} as WorkerChannelReporter<TWorkerChannel>['stream'], { get: (target, name) => { if (typeof name !== 'string') return target[name as keyof typeof target] const streamReporter: StreamReporter = { emit: (payload) => { const message: StreamEmissionMessage = {type: 'emission', name, payload} parentPort.postMessage(message) }, end: () => { const message: StreamEndMessage = {type: 'end', name} parentPort.postMessage(message) }, } return streamReporter }, }), } }