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
JavaScript
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