UNPKG

async-streamify

Version:

Stream and serialize nested promises and async iterables over HTTP, workers, etc

176 lines (175 loc) 6.16 kB
import BufferedAsyncIterable from "../util/bufferedAsyncIterable.js"; import * as errorKind from "../kinds/error.js"; /** * Deserializes objects that were serialized using AsyncObjectSerializer. * Reconstructs promises and async iterables from their serialized representations. * * @template TTarget - The expected type of the deserialized object * * @example * ```typescript * const serializedStream = new AsyncObjectSerializer({ * value: Promise.resolve(42), * stream: (async function*() { yield 1; yield 2; })() * }); * * const deserializer = new AsyncObjectDeserializer(serializedStream); * const result = await deserializer.deserialize(); * * console.log(await result.value); // 42 * for await (const num of result.stream) { * console.log(num); // 1, 2 * } * ``` */ export class AsyncObjectDeserializer { /** * Creates a new AsyncObjectDeserializer instance * @param iter - The async iterable containing serialized object chunks */ constructor(iter) { Object.defineProperty(this, "iter", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "deserializedRoot", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "promises", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "activeIterators", { enumerable: true, configurable: true, writable: true, value: new Map() }); /** * Optional callback that is called when the root object is first deserialized */ Object.defineProperty(this, "onSetRoot", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.iter = iter; } /** * Processes the serialized stream and reconstructs the original object * with its promises and async iterables * * @returns Promise<TTarget | undefined> The deserialized object */ async deserialize() { for await (const serializedChunk of this.iter) { if (!this.deserializedRoot) { this.deserializedRoot = this.deserializeValue(serializedChunk); if (this.onSetRoot) this.onSetRoot(this.deserializedRoot); continue; } const [id, value] = serializedChunk; const promise = this.promises.get(id); if (promise) { if ("$resolve" in value) { promise.resolve(this.deserializeValue(value["$resolve"])); } else if ("$reject" in value) { promise.reject(errorKind.maybeDeserialize(value["$reject"])); } else { throw new Error("Unexpected promise state: " + JSON.stringify(value)); } this.promises.delete(id); continue; } const iter = this.activeIterators.get(id); if (iter) { const { done, value: iterValue } = value; if (done) { iter.close(); } else { iter.push(this.deserializeValue(iterValue)); } continue; } } return this.deserializedRoot; } /** * Recursively deserializes a value, reconstructing promises, async iterables, * and nested objects from their serialized form * * @param serializedValue - The serialized value to deserialize * @returns The deserialized value */ deserializeValue(serializedValue) { // Return primitives as-is: null, undefined, numbers, strings, etc. if (typeof serializedValue !== "object" || serializedValue === null) { return serializedValue; } if (Array.isArray(serializedValue)) { return serializedValue.map((value) => this.deserializeValue(value)); } const keys = Object.keys(serializedValue); if (keys.length === 1) { if ("$promise" in serializedValue) { const id = serializedValue["$promise"]; return new Promise((resolve, reject) => { this.promises.set(id, { resolve, reject }); }); } if ("$asyncIterator" in serializedValue) { const id = serializedValue["$asyncIterator"]; const iter = new BufferedAsyncIterable(); this.activeIterators.set(id, iter); return iter; } } if (serializedValue.constructor === Object) { return Object.fromEntries(Object.entries(serializedValue).map(([key, value]) => [ key, this.deserializeValue(value), ])); } return serializedValue; } } /** * Helper function to deserialize an object stream and return a promise that * resolves with the root object as soon as it's available * * @template T - The expected type of the deserialized object * @param iter - The async iterable containing serialized object chunks * @returns Promise<T> A promise that resolves with the deserialized root object * * @example * ```typescript * const serializedStream = new AsyncObjectSerializer({ * name: "test", * value: Promise.resolve(42) * }); * * const obj = await deserialize<{ name: string, value: Promise<number> }>(serializedStream); * console.log(obj.name); // "test" * console.log(await obj.value); // 42 * ``` */ export function deserialize(iter) { const deserializer = new AsyncObjectDeserializer(iter); return new Promise((resolve) => { deserializer.onSetRoot = resolve; deserializer.deserialize(); // don't await }); } export default deserialize;