async-streamify
Version:
Stream and serialize nested promises and async iterables over HTTP, workers, etc
176 lines (175 loc) • 6.16 kB
JavaScript
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;