UNPKG

life

Version:

Life.js is the first fullstack framework to build agentic web applications. It is minimal, extensible, and typesafe. Well, everything you love.

296 lines (293 loc) 9.78 kB
import { canon } from "./chunk-6CBODJTF.mjs"; import { attempt, failure, newId, obfuscateLifeError, resultSchema, success } from "./chunk-ZHBK6UTM.mjs"; import { __name } from "./chunk-2D3UJWOA.mjs"; // transport/types.ts import z from "zod"; var rpcRequestSchema = z.object({ type: z.literal("request"), id: z.string(), name: z.string(), input: z.unknown().optional() }); var rpcResponseSchema = z.object({ type: z.literal("response"), id: z.string(), result: resultSchema.transform((result) => result) }); var rpcMessageSchema = z.discriminatedUnion("type", [rpcRequestSchema, rpcResponseSchema]); // transport/base.ts var TransportClientBase = class { static { __name(this, "TransportClientBase"); } #provider; #obfuscateErrors; #procedures = /* @__PURE__ */ new Map(); #resolveResults = /* @__PURE__ */ new Map(); #telemetry = null; #rpcUnsubscribe; constructor({ provider, telemetry, obfuscateErrors = false }) { this.#provider = provider; this.#obfuscateErrors = obfuscateErrors; this.#telemetry = telemetry ?? null; } async sendText(topic, text) { try { const [errWriter, writer] = await this.streamText(topic); if (errWriter) return failure(errWriter); await writer.write(text); await writer.close(); return success(); } catch (error) { return failure({ code: "Unknown", cause: error }); } } receiveText(topic, callback, onError) { try { const [errReceive, unsubscribe] = this.receiveStreamText( topic, async (iterator, participantId) => { const [err] = await attempt(async () => { let result = ""; for await (const chunk of iterator) result += chunk; await callback(result, participantId); }); if (err) onError?.(err); }, onError ); if (errReceive) return failure(errReceive); return success(unsubscribe); } catch (error) { return failure({ code: "Unknown", cause: error }); } } async sendObject(topic, obj) { try { const [errCanon, serialized] = canon.stringify(obj); if (errCanon) return failure(errCanon); const [errSend] = await this.sendText(topic, serialized); if (errSend) return failure(errSend); return success(); } catch (error) { return failure({ code: "Unknown", cause: error }); } } receiveObject(topic, callback, onError) { try { const [err, unsubscribe] = this.receiveText( topic, async (text, participantId) => { try { const [errParse, deserialized] = canon.parse(text); if (errParse) return onError?.(errParse); const [errCallback] = await attempt( async () => await callback(deserialized, participantId) ); if (errCallback) return onError?.(errCallback); } catch (error) { onError?.(error); } }, onError ); if (err) return failure(err); return success(unsubscribe); } catch (error) { return failure({ code: "Unknown", cause: error }); } } /** * Register a remote procedure. * @param procedure - The procedure to register */ register(procedure) { this.#procedures.set(procedure.name, procedure); } /** * Call a remote procedure. * @param name - The name of the procedure to call * @param inputs - The parameters to pass to the procedure * @returns A promise that resolves to the response from the procedure */ async call({ name, schema, input, timeoutMs = 1e4 }) { const id = newId("rpc"); try { let parsedInput; if (schema?.input) { const { error: errInput, data: parsedInput_ } = schema.input.safeParse(input); if (errInput) return failure({ code: "Validation", cause: errInput }); parsedInput = parsedInput_; } const resultPromise = new Promise((resolve) => { this.#resolveResults.set(id, (res) => { resolve( res ); }); }); const timeoutPromise = new Promise((resolve) => { setTimeout(() => { resolve( failure({ code: "Timeout", message: `RPC call to procedure '${name}' timed out after ${timeoutMs}ms.` }) ); }, timeoutMs); }); const [errCall] = await this.sendObject("rpc", { type: "request", id, name, ...parsedInput ? { input: parsedInput } : {} }); if (errCall) return failure(errCall); const [err, data] = await Promise.race([resultPromise, timeoutPromise]); if (err) return failure(err); if (schema?.output) { const { error: errOutput, data: parsedOutput } = schema.output.safeParse(data); if (errOutput) return failure({ code: "Validation", cause: errOutput }); return success(parsedOutput); } return success(); } catch (error) { return failure({ code: "Unknown", cause: error }); } finally { this.#resolveResults.delete(id); } } // Initialize RPC async #onRPCMessage(rawMessage) { try { const { data: parsedMessage, error: messageError } = rpcMessageSchema.safeParse(rawMessage); if (messageError) return this.#telemetry?.log.error({ message: "Invalid RPC message.", error: messageError }); if (parsedMessage.type === "response") { const resolveResult = this.#resolveResults.get(parsedMessage.id); if (resolveResult) await resolveResult(parsedMessage.result); return; } let procedure; const requestMessage = parsedMessage; const sendResult = /* @__PURE__ */ __name(async (result) => { let [error, data] = result; if (error) this.#telemetry?.log.error({ message: `Failed to respond to RPC request for handler '${requestMessage.name}'.`, error }); if (this.#obfuscateErrors && error) error = obfuscateLifeError(error); const [errSend] = await this.sendObject("rpc", { type: "response", id: requestMessage.id, result: error ? failure(error) : success(data) }); if (errSend) return this.#telemetry?.log.error({ message: "Failed to send RPC response.", error: errSend }); }, "sendResult"); procedure = this.#procedures.get(parsedMessage.name); if (!procedure) return await sendResult( failure({ code: "NotFound", message: `Procedure not found: '${parsedMessage.name}'.` }) ); const { data: input, error: inputError } = procedure.schema?.input ? procedure.schema.input.safeParse(parsedMessage.input) : { data: void 0, error: null }; if (inputError) return await sendResult( failure({ code: "Validation", message: `Invalid input parameters for procedure '${parsedMessage.name}'.`, cause: inputError }) ); const [err, rawOutput] = await procedure.execute(input); if (err) return await sendResult(failure(err)); const { data: output, error: outputError } = procedure.schema?.output ? await procedure.schema.output.safeParseAsync(rawOutput) : { data: rawOutput, error: null }; if (outputError) { return await sendResult( failure({ code: "Validation", message: `Invalid output from procedure '${parsedMessage.name}'.`, cause: outputError }) ); } return await sendResult(success(output)); } catch (error) { this.#telemetry?.log.error({ message: "Unknown error while handling RPC message.", error }); } } #startRPC() { try { const [errReceive, unsubscribe] = this.receiveObject("rpc", this.#onRPCMessage.bind(this)); if (errReceive) return failure(errReceive); this.#rpcUnsubscribe = unsubscribe; return success(); } catch (error) { return failure({ code: "Unknown", cause: error }); } } #stopRPC() { try { this.#rpcUnsubscribe?.(); this.#resolveResults.clear(); this.#procedures.clear(); return success(); } catch (error) { return failure({ code: "Unknown", cause: error }); } } on = /* @__PURE__ */ __name((...args) => this.#provider.on(...args), "on"); joinRoom = /* @__PURE__ */ __name(async (...args) => { const [errJoin] = await this.#provider.joinRoom(...args); if (errJoin) return failure(errJoin); const [errStart] = this.#startRPC(); if (errStart) return failure(errStart); return success(); }, "joinRoom"); leaveRoom = /* @__PURE__ */ __name(async (...args) => { const [errLeave] = await this.#provider.leaveRoom(...args); if (errLeave) return failure(errLeave); const [errStop] = this.#stopRPC(); if (errStop) return failure(errStop); return success(); }, "leaveRoom"); streamText = /* @__PURE__ */ __name((...args) => this.#provider.streamText(...args), "streamText"); receiveStreamText = /* @__PURE__ */ __name((...args) => this.#provider.receiveStreamText(...args), "receiveStreamText"); enableMicrophone = /* @__PURE__ */ __name((...args) => this.#provider.enableMicrophone(...args), "enableMicrophone"); playAudio = /* @__PURE__ */ __name((...args) => this.#provider.playAudio(...args), "playAudio"); streamAudioChunk = /* @__PURE__ */ __name((...args) => this.#provider.streamAudioChunk(...args), "streamAudioChunk"); }; export { TransportClientBase }; //# sourceMappingURL=chunk-HZQWVS5S.mjs.map